1 <script> 2 /***Object构造函数写法****/ 3 //创建一个object的实例,然后再为它添加属性和方法,这是早期的写法 4 var person=new Object; 5 person.name='gupeng'; 6 person.age=26; 7 person.job='it'; 8 person.sayName=function(){ 9 alert(this.name); 10 }; 11 12 /******对象字面量的写法(单例模式)********************************************************************************** 13 *与上面的例子中的person对象是一样的,都有相同的属性和方法,这些属性在创建的时候都带有一些特性值(characteristic) 14 */ 15 var person={ 16 name:'gupeng', 17 age:26, 18 job:'it', 19 sayName:function(){ 20 alert(this.name); 21 } 22 }; 23 24 /*******************工厂模式******************************************************************************* 25 *虽然object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码 26 *解决方法:使用工厂模式的一种变体 27 ****/ 28 //以函数来封装,以特定接口创建对象的细节 29 function createPerson(name,age,job){ 30 var o=new Object(); 31 o.name=name; 32 o.age=age; 33 o.job=job; 34 o.sayName=function(){ 35 alert(this.name); 36 }; 37 return o; 38 }; 39 var person1=createPerson('peter',26,'it'); 40 // console.log(person1); 41 /*** 42 *函数createPerson()能够根据接受的参数来创建一个包含所有必要信息的Person对象.可以无数次的调用这个函数 43 *而每次它都会返回一个包含三个属性一个方法的对象.工厂模式虽然解决了创建多个对象的问题,但却没有解决对象识别的问题,即怎样知道一个对象类型. 44 ***/ 45 46 /*******构造函数模式****145页*************************************************************************************************/ 47 /*** 48 *ECMA中的构造函数可用来创建特定类型的对象,像object和Array这样的原生构造函数,在运行时会自动出现在执行环境. 49 *此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法 50 ***/ 51 function Person(name,age,job){ 52 this.name=name; 53 this.age=age; 54 this.job=job; 55 this.sayName=function(){ 56 alert(this.name); 57 }; 58 }; 59 var person1=new Person('peter',26,'web'); //person1保存着Person的一个实例,这个对象都有一个constructor(构造函数)属性,该属性指向Person 60 var person2=new Person('lily',26,'web'); //person2保存着Person的一个实例,这个对象都有一个constructor(构造函数)属性,该属性指向Person 61 console.log(person1.constructor==Person); //true 62 /***** 63 *在这个例子中,Person()函数取代了createPerson()函数.不同之处:1没有显示的创建对象;2直接将属性和方法赋给了this对象;3没有return语句.4函数名首字母大写开头, 64 *构造函数本身也是函数.只不过可以用来创建对象而已. 65 *要创建Person的新实例,必须使用new操作符,以这种方式调用构造函数实际上会经历以下4个步骤: 66 *1创建一个新对象; 2将构造函数的作用域赋给新对象(因此this就指向了这个新对象); 3执行构造函数中的代码(为这个新对象添加属性); 4返回新对象 67 *对象的constructor属性最早是用来标识对象类型的,但是检测对象类型,还是instanceof操作符更可靠一些,这个例子中创建的所有对象既是Object的实例,也是Person的实例 68 *console.log(person1 instanceof Object); //true 69 *创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,这正是构造函数胜过工厂模式的地方 70 *在这个例子中,person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object 71 *以这种方式定义的构造函数是定义在Global对象中的(在浏览器中是window对象); 72 *构造函数与其他函数的唯一区别,在于调用它们的方式不同,不过构造函数毕竟也是函数,不存在定义构造函数的特殊语法; 73 *任何函数,只要通过new操作符来调用,那它就可以作为构造函数 74 ****/ 75 //当做构造函数调用 76 var p1=new Person('gupeng',26,'web'); 77 //p1.sayName(); 78 79 //作为普通函数调用 80 Person('gerg',27,'doctor'); 81 // window.sayName();//当在全局作用域中调用一个函数时,this对象总是指向global对象,在浏览器中是window 82 83 //在另一个对象的作用域中调用 84 var o=new Object(); 85 Person.call(o,'kristen',25,'nurse');//使用call或apply在某个特殊对象的作用域中调用Person()函数,这里是在o的作用域中调用的,因此调用后,o就拥有了所有属性和方法 86 87 /********构造函数的问题**146页*************************************************************************/ 88 /** 89 *使用构造函数的主要问题:每个方法都要在每个实例上重新创建一遍,在前面的例子中,person1和person2都有一个名为sayName的方法,但那两个方法不是同一个Function的实例 90 *在ECMA中的函数也是对象,因此定义一个函数,也就是实例化了一个对象 91 *不同实例上的同名函数是不相等的 92 **/ 93 //创建两个完成相同任务的Function实例的确没有必要,况且有this对象在,根本不用在执行代码前就把函数绑定在特定对象上面 94 //通过把函数定义转移到构造函数外部来解决这个问题 95 function Person(name,age,job){ 96 this.name=name; 97 this.age=age; 98 this.job=job; 99 this.sayName=sayName; 100 }; 101 function sayName(){ 102 alert(this.name); 103 }; 104 var p1=new Person('gupeng',26,'web'); 105 /*******147页 106 *在这个例子中,我们把sayName()函数的定义转移到了构造函数外部,而在构造函数内部,我们将sayName属性设置成等于全局的sayName函数 107 *由于sayName包含的是一个指向函数的指针,因此p1对象就共享了在全局作用域中定义的同一个sayName()函数 108 *在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实; 109 *如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了; 110 这些问题可以通过使用原型模式来解决 111 **/ 112 113 /******原型模式***147页****************************************************************/ 114 /*** 115 *我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法 116 *prototype就是通过调用构造函数而创建的那个对象实例的原型对象(实例的原型对象) 117 *使用原型对象的好处是:让所有对象实例共享它所包含的属性和方法; 118 *不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中 119 **/ 120 function Person(){ //将属性和方法直接添加到了Person的prototype属性中 121 //构造函数变成了空函数,但仍然可以通过构造函数创建新对象 122 }; 123 Person.prototype.name='Nicholas'; 124 Person.prototype.age=29; 125 Person.prototype.job='soft'; 126 Person.prototype.sayName=function(){ 127 alert(this.name); 128 }; 129 var p5=new Person(); //需要格外注意的一点:虽然这个实例本身不包含属性和方法,但可以调用p5.sayName()方法.这是通过查找对象属性的过程实现的 130 var p6=new Person(); 131 // console.log( p5.sayName()==p6.sayName() ) //true 132 /***理解原型对象-148页***** 133 1理解原型对象: 134 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象. 135 在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针. 136 就拿前面的例子来说:Person.prototype.constructor指向Person 137 创建了自定义构造函数后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从object继承而来的; 138 当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象;ECMA-262第五版中管这个指针叫[[Prototype]],虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox,Safari和Chrome在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本则是完全不可见的.不过要明确的重要一点是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间. 139 虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系(指向构造函数的原型),从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true: alert( Person.prototype.isPrototypeOf( p5 ) ); //true 140 ECMA5中增加了一个新方法:Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值: 如下 141 alert( Object.getPrototypeOf(p5)==Person.prototype ); 结果为true; 142 alert( Object.getPrototypeOf(p5).name ); 结果为Nicholas; 143 使用Object.getPrototypeOf()可以方便的取得一个对象的原型,而这在利用原型实现继承的情况下是非常重要的:ie9+,ff3.5+,safari5+,opera12+和chrome支持 144 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性.搜索首先从对象实例本身开始,如在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性.如果在原型对象中找到了这个属性,则返回该属性的值. 145 ****/ 146 /****** 147 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值,如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性重名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性,来看下面的例子 148 *******/ 149 function Person(){ //将属性和方法直接添加到了Person的prototype属性中 150 //构造函数变成了空函数,但仍然可以通过构造函数创建新对象 151 }; 152 Person.prototype.name='Nicholas'; 153 Person.prototype.age=29; 154 Person.prototype.job='soft'; 155 Person.prototype.sayName=function(){ 156 alert(this.name); 157 }; 158 var p5=new Person(); //需要格外注意的一点:虽然这个实例本身不包含属性和方法,但可以调用p5.sayName()方法.这是通过查找对象属性的过程实现的 159 var p6=new Person(); 160 p5.name='greg'; 161 console.log('给实例添加重名属性会屏蔽原型中的属性:'+p5.name);//使用delete操作符可以完全删除实例属性,从而能够重新访问原型中的属性 162 delete p5.name; 163 console.log('使用delete删除后访问到了原型对象的属性:'+p5.name);//使用delete操作符可以完全删除实例属性,从而能够重新访问原型中的属性 164 //使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中(这个方法继承自Object),只在给定属性存在于对象实例中才返回true 165 166 p6.name='gupeng'; 167 console.log('检测属性是否存在实例中,是返回true: '+p6.hasOwnProperty( 'name' ) ); //true 168 //通过使用hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了 169 //有两种方式使用in操作符,单独使用和在for-in循环中使用,在单独使用是时,in操作符会在通过对象能够访问给定属性值时返回true,无论该属性是存在于实例中还是原型中 170 console.log("name" in p6); //true 171 //使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性 172 //要取得对象上所有可枚举的实例属性,可以使用ECMA5的Object.keys()方法,接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组,代码如下: 173 var p6keys=Object.keys(p6); 174 console.log(p6keys); //["name"],注意是可枚举的实例的属性 175 var keys=Object.keys( Person.prototype ); //输出原型的可枚举的属性 176 console.log(keys); //["name", "age", "job", "sayName"] 177 //如果你想得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法:ie9以上版本支出该方法 178 var keys2=Object.getOwnPropertyNames(Person.prototype); 179 console.log(keys2); // ["constructor", "name", "age", "job", "sayName"] 结果中包含了不可枚举的constructor属性, 180 181 //最简单的原型语法: 182 function Person(){ 183 }; 184 Person.prototype={ //会改变constructor指向,指向了Object 185 constructor:Person,//如不指定constructor指向,则会指向Object 186 name:'gupeng', 187 age:27, 188 job:'it', 189 email:'18600668868@163.com', 190 sayName:function(){ 191 console.log(this.name); 192 } 193 }; 194 /*** 195 在上面的代码中,将Person.prototype设置为等于一个以对象字面量形式创建的新对象,最终结果相同,但有一个例外:constructor属性不再指向Person了.前面曾经介绍过,没创建一个函数,就会同时创建它的prototype对象,这个对象自动获得constructor属性.而我们在这里使用的语法,本质上完全重写了默认的prototype对象,,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数,,此时尽管instanceof操作符能返回正确的结果,但通过constructor已经无法确定对象的类型了 196 代码如下: 197 ***/ 198 var p7=new Person(); 199 console.log( p7 instanceof Person ); //true 200 console.log( p7 instanceof Object ); //true 201 console.log( p7.constructor==Person ); //true,不在函数内部指定constructor指向,会返回false 202 console.log( p7.constructor==Object ); //false,不在函数内部指定constructor指向,会返回true 203 //以上代码特意包含了一个constructor属性,并将它的值设置为Person,从而确保了通过该属性能够访问到适当的值 204 //注意:以上这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true,默认情况下,原生的constructor是不可枚举的 205 //兼容E5的JS引擎,可以使用Object.defineProperty(),代码如下: 206 function Person(){ 207 }; 208 Person.prototype={ 209 name:'gupeng', 210 age:27, 211 job:'it', 212 email:'18600668868@163.com', 213 sayName:function(){ 214 console.log(this.name); 215 } 216 }; 217 //重设构造函数,只用于与E5兼容的浏览器 218 Object.defineProperty(Person.prototype,'constructor',{ 219 enumerable:false, 220 value:Person 221 }); 222 223 var p8=new Person; 224 console.log(p8.constructor==Person); //true 225 226 /****原型的动态型156页 227 *由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来,即使是先创建了实例后修改了原型也是一样的 228 ***/ 229 var p9=new Person; //创建一个Person的实例,并将其保存在p9中 230 Person.prototype.sayHi=function(){ 231 console.log( 'hi' ); 232 }; 233 // p9.sayHi(); 234 /**** 235 * 以上代码中先创建了Person的一个实例,并将其保存在p9中,然后,下一条语句在Person.prototype中添加了一个方法sayHi();即使p9实例是在添加新方法之前创建的,但它仍然可以访问这个新方法.其原因可以归结为实例与原型之间的松散连接关系,,当调用sayHi()方法时,首先会在实例中搜索名为sayHi的属性,在没有找到的情况下,会继续搜索原型,因为实例与原型之间的连接只不过是指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在那里的函数 236 尽管可以随时为原型添加属性和方法,并且能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了.我们知道,在调用构造函数时,会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系. 237 请记住:实例中的指针仅指向原型,而不指向构造函数,看下面的例子: 238 ***/ 239 function Person1(){ 240 }; 241 // var p10=new Person1; 写在这里会报错 242 Person1.prototype={ 243 constructor:Person,//如不指定constructor指向,则会指向Object 244 name:'gupeng', 245 age:27, 246 job:'it', 247 email:'18600668868@163.com', 248 sayName:function(){ 249 console.log(this.name); 250 } 251 }; 252 // p10.sayName(); //报错:Uncaught TypeError(请求错误): p10.sayName is not a function 253 var p11=new Person1; //注意创建实例的顺序 254 // p11.sayName(); // 255 256 /****原生对象的原型157页 257 原型模式的重要性不仅体现在创建自定义类型方法,就连所有原生的引用类型,都是采用这种模式创建的.所有原生引用类型(Object,Array,String,等等)都在其构造函数的原型上定义了方法; 258 通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法,可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法 259 ****/ 260 261 /***原型对象的问题-158页 262 原型模式也不是没有缺点.首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值.虽然这会在某种程度上带来一些不方便,但还不是原型最大的为题,原型模式最大的问题是其共享的本质导致的; 263 原型中所有属性是被很多实例共享的,这种共享对于函数非常合适,对于那些包含基本值的属性倒也说得过去,毕竟在前面的例子中,通过在实例上添加一个同名属性,可以屏蔽原型中的对应属性;然而,对于包含引用类型值的属性来说,问题就比较突出了,来看下面的例子: 264 ****/ 265 function Person2(){ 266 }; 267 Person2.prototype={ 268 constructor:Person,//如不指定constructor指向,则会指向Object 269 name:'gupeng', 270 age:27, 271 job:'it', 272 email:'18600668868@163.com', 273 friends:['lucy','lily'], 274 sayName:function(){ 275 console.log(this.name); 276 } 277 }; 278 var p12=new Person2; 279 var p13=new Person2; 280 p12.friends.push('peter'); 281 //console.log( p12.friends ); //["lucy", "lily", "peter"] 282 //console.log( p13.friends ); //["lucy", "lily", "peter"] 283 /**以上代码详解如下: 284 在此,Person2.prototype对象有一个名为friends的属性,该属性包含了一个字符串数组,然后创建了Person2的两个实例,接着修改了P12.friends引用的数组,想数组添加了字符串,由于friends数组存在于Person2.prototype而非P12中,所以刚刚提到的修改也会通过P13.friends(与P12.friends指向同一个数组)反映出来.可是实例一般都是要有属于自己的全部属性的,而这个问题正是我们很少看到有人单独使用原型模式的原因所在 285 **/ 286 287 /********组合使用构造函数模式和原型模式-159页 288 创建自定义类型的最常见方式,就是组合使用构造函数与原型模式. 289 构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性.结果每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存.另外这种混合模式还支持向构造函数传递参数;可谓是集两种模式之长,下面的代码重写了前面的例子: 290 ******************/ 291 function Person(name,age,job){ 292 this.name=name; 293 this.job=job; 294 this.age=age; 295 this.friends=['lily','lucy']; 296 }; 297 Person.prototype={ 298 constructor:Person, 299 sayName:function(){ 300 console.log(this.name); 301 } 302 }; 303 var p1=new Person('peter',27,'web'); 304 var p2=new Person('bruce',29,'doctor'); 305 //console.log(p1); 306 //console.log(p2); 307 //p1.friends.push('rose'); 308 //console.log(p1.friends); //["lily", "lucy", "rose"] 309 //console.log(p2.friends); //["lily", "lucy"] 310 //console.log(p1.friends==p2.friends); //false 311 //console.log(p1.sayName()==p2.sayName()); //true 312 //这种构造函数与原型混成的模式,是目前使用最广泛,认同度最高的一种创建自定义类型的方法,可以说,这是用来定义引用类型的一种默认模式 313 314 /*****动态原型模式 315 有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑,动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数函数中,而通过在构造函数中初始化原型(仅在必要额情况下),又保持了同时使用构造函数和原型的优点,换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型. 316 请看以下的例子: 317 **************/ 318 function Person(name,age,job){ 319 this.name=name; 320 this,age=age; 321 this.job=job; 322 if(typeof this.sayName !='function'){ 323 Person.prototype.sayName=function(){ 324 console.log(this.name); 325 }; 326 }; 327 }; 328 var p1=new Person('peter',26,'it'); 329 // p1.sayName(); 330 /** 331 代码详解:注意构造函数中if的部分,这里只在sayName方法不存在的情况下,才会将它添加到原型中,这段代码只会在初次调用构造函数时才会执行.此后原型已经完成初始化,不需要再做什么修改了.不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映,因此,这种方法确实可以说很完美.其中if语句检查的可以是初始化之后应该存在的任何属性和方法,不必要用一大堆if语句检查每一个属性和方法只要检查其中一个即可,对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型. 332 注意:在使用动态原型模式时,不能使用对象字面量重写原型,如果在已经创建了实例的情况下重写原型,那么会切断现有实例与新原型之间的联系 333 **/ 334 335 /*************寄生构造函数模式-160页 336 通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式.这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装对象的代码,然后返回新创建的对象,但从表面上看,这个函数又很像典型的构造函数. 337 请看下面的例子: 338 ************************/ 339 function Person(name,age,job){ 340 var o=new Object; 341 o.name=name; 342 o.age=age; 343 o.job=job; 344 o.sayName=function(){ 345 console.log(this.name); 346 }; 347 return o; 348 }; 349 var p1=new Person('lily',30,'nurse'); 350 // p1.sayName(); //lily 351 /**代码详解: 352 在这个例子中,Person函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象,除了使用new操作符并把使用的包装函数叫做构造函数外,这个模式跟工厂模式其实是一模一样的,构造函数在不返回值的情况下,默认会返回新对象实例,而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值 353 这个模式可以在特殊情况下用来为对象创建构造函数,假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式 354 **/ 355 function SpecialArray(){ 356 //第一步:创建数组 357 var values=new Array; 358 //第二部:添加值 359 values.push.apply(values,arguments); 360 //第三步:添加方法 361 values.toPipedString=function(){ 362 return this.join('|'); 363 }; 364 //第四步:返回数组 365 return values; 366 } 367 </script>
posted on
浙公网安备 33010602011771号