以下为学习《JavaScript 高级程序设计》》(第 3 版) 与网易云课堂上的【撩课-零基础玩转JavaScript】所做笔记。
目录:
2、工厂模式
3、构造函数模式
② 构造函数的问题
4、原型模式
① 理解原型对象
③ 更简单的原型语法
④ 原型的动态性
⑤ 原生对象的原型
⑥ 原型对象的问题
6、动态原型模式
7、寄生构造函数模式
8、稳妥构造函数模式
缺点:使用同一个接口创建很多对象,会产生大量的重复代码
1 <script> 2 //使用 Object 构造函数创建单个对象 3 var person = new Object(); 4 person.name = "xiaoxu"; 5 person.age = 20; 6 person.a = function(){ 7 console.log("今tia很开心"); 8 } 9 console.log( person.name ); //输出:xiaoxu 10 person.a(); //输出:今tia很开心 11 //使用对象字面量创建单个对象 12 var dog = { 13 name : "旺财", 14 age : 2, 15 word : function(){ 16 console.log("汪汪"); 17 } 18 } 19 console.log( dog.name ); //输出:旺财 20 dog.word(); //输出:汪汪 21 </script>
优点:解决了创建多个相似对象的问题
缺点:没有解决对象识别的问题(即怎么知道一个对象的类型)
1 <script> 2 function createPerson(name, age, job){ 3 var o = new Object(); 4 o.name = name; 5 o.age = age; 6 o.sex = sex; 7 o.sayName = function(){ 8 console.log( o.name ); 9 }; 10 return o; 11 } 12 //函数 createPerson() 能够根据接受的参数来构建一个包含所有必要信息的 Person 对象 13 var person1 = createPerson( "小许", 20, "male" ); 14 var person2 = createPerson( "乐乐", 1, "female" ); 15 </script>
优点:创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型(这是胜过工厂模式的地方)
1 /* 2 创建构造函数: 1、没有显示地创建对象 3 2、直接将属性和方法赋给了 this 对象 4 3、没有 return 语句 5 4、构造函数始终以一个大写字母开头 6 */ 7 function Person(name, age, sex){ 8 this.name = name; 9 this.age = age; 10 this.sex = sex; 11 this.sayName = function(){ 12 console.log( this.name ); 13 }; 14 } 15 16 var person1 = new Person( "xiaoxu", 20, "male" ); 17 var person2 = new Person( "旺财", 1 ,"female" ); 18 19 //函数实例都有一个 constructor(构造函数)属性,该属性指向函数 20 console.log( person1.constructor == Person ); //输出:true 21 console.log( person2.constructor == Person ); //输出:true 22 23 //本例中创建的所有对象既是 object 的实例,也是 Person 的实例 24 //创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型(这是胜过工厂模式的地方) 25 console.log( person1 instanceof Object ); //输出:true 26 console.log( person1 instanceof Person ); //输出:true 27 console.log( person2 instanceof Object ); //输出:true 28 console.log( person2 instanceof Person ); //输出:true
构造函数没有 return 语句,所以如果创建构造函数新实例的时候不使用 new 操作符,函数的返回值会是 undefined。
创建构造函数新实例时,必须使用 new 操作符。使用 new 操作符调用构造函数实际上会经历 4 个步骤:
1、创建1个新对象
2、将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
3、执行构造函数中的代码(为这个新对象添加属性)
4、返回新对象
图示如下。实例化的时候,第一步,在堆区创建一个新的空对象,新对象地址放在栈区,让地址指向空的新对象,第二步,this 指向地址,地址指向空的新对象,第三步,执行构造函数,给这个空的新对象添加属性和方法,第四步,返回新对象,返回新对象相当于返回 this。
使用 new 操作符调用函数,函数就当作构造函数使用。不使用 new 操作符调用,函数就当作普通函数使用。不使用 new 操作符调用,函数的属性和方法都被添加给 window 对象。
1 <script> 2 //以这种方式定义的构造函数是定义在 Global 对象(在浏览器中是 window 对象)中的。 3 function Person(name){ 4 this.name = name; 5 this.sayName = function(){ 6 console.log( this.name ); 7 } 8 } 9 //使用 new 操作符调用 10 //当作构造函数调用 11 var person1 = new Person("xiaoxu"); 12 person1.sayName(); //输出:xiaoxu 13 //不使用 new 操作符调用 14 //当作普通函数调用 15 Person("乐乐"); 16 window.sayName(); //输出:乐乐 17 //在另一个对象的作用域中调用 18 var o = new Object(); 19 Person.call(o, "Sammi"); 20 o.sayName(); //输出:Sammi 21 </script>
使用构造函数的主要问题是每个方法都要在每个实例上都要创建一遍。不同实例上的同名函数是不相等的,但是没有必要创建 2 个完成相同任务的 Function对象。可以通过把函数定义转移到构造函数外部来解决这个问题,但是这样又有2个缺点:①在全局作用域中定义的函数实际上只能被某个对象调用 ②如果对象需要定义很多方法,那么需要定义很多全局函数,那么这个自定义的引用类型就毫无封装性可言了。这个问题可通过使用原型模式来解决。
1 <script> 2 //使用构造函数的主要问题是每个方法都要在每个实例上都要创建一遍。 3 function Person(name, age, job){ 4 this.name = name; 5 this.age = age; 6 this.job = job; 7 /* 8 this.sayName = function(){ 9 console.log( this.name ); 10 }*/ 11 //下面与上面声明函数在逻辑上是等价的 12 //ECMAScript 中的函数是对象,因此每定义一个函数也就是实例化了一个对象, 13 this.sayName = new Function( "console.log(this.name)" ); 14 } 15 //不同实例上的同名函数是不相等的 16 var person1 = new Person(); 17 var person2 = new Person(); 18 console.log( person1.sayName == person2.sayName ); //输出:false 19 20 //创建 2 个完成同样任务的 Function 实例没有必要,可以通过把函数定义转移到构造函数外部来解决这个问题。 21 function Person2(name, age){ 22 this.name = name; 23 this.age = age; 24 //将 sayName 属性设置成等于全局的 sayName 函数 25 this.sayName = sayName; 26 } 27 function sayName(){ 28 console.log( this.name ); 29 } 30 var person1 = new Person2(); 31 var person2 = new Person2(); 32 //由于 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的 sayName()函数 33 console.log( person1.sayName == person2.sayName ); //输出:true 34 </script>
优点:可以让所有对象实例共享它所包含的属性和方法
1 <script> 2 function Person(){ 3 } 4 5 /* 6 创建的每个函数都有 1 个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 7 这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。 8 prototype 就是通过调用构造函数而创建的那个对象的实例的原型对象,使用原型对象可以让 9 所有对象实例共享它所包含的属性和方法。 10 */ 11 Person.prototype.name = "xiaoxu"; 12 Person.prototype.age = 22; 13 Person.prototype.sayName = function(){ 14 console.log( this.name ); 15 }; 16 17 var person1 = new Person(); 18 person1.sayName(); //输出:xiaoxu 19 var person2 = new Person(); 20 person2.sayName(); //输出:xiaoxu 21 22 //将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数。 23 console.log( person1.sayName == person2.sayName ); //输出:true 24 </script>
以上面使用的 Person 构造函数和 Person.prototype 创建实例的代码为例, 下图展示了各个对象之间的关系。Person.prototype.constructor 指向 Person。
在所有实现中都没有办法访问到 [[Prototype]],但可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系。使用 Object.getPrototypeOf() 返回 [[Prototype]] 的值。
1 //isPrototypeOf() 2 //如果[[Prototype]]指向调用 isPrototype() 方法的对象(Person.prototype), 那么这个方法就返回 true。 3 console.log( Person.prototype.isPrototypeOf( person1 ) ); //输出:true 4 console.log( Person.prototype.isPrototypeOf( person2 ) ); //输出:true 5 //Object.getPrototypeOf() 6 console.log( Object.getPrototypeOf[person1] == Person.prototype ); //输出:false 7 console.log( Object.getPrototypeOf( person1 ).name ); //输出: xiaoxu
自定义的构造函数可以通过 __proto__ 访问到函数的原型对象。
1 function Person(){ } 2 Person.prototype.name = "xiaoxu"; 3 Person.prototype.sayName = function(){ 4 console.log( this.name ); 5 }; 6 var person1 = new Person(); 7 console.log(person1.__proto__); //输出: Person { name: 'xiaoxu', sayName: [Function] }
当代码读取某个对象的某个属性时,先从对象实例本身开始搜索给定名字的属性,如果找到则返回属性的值,如果找不到则在原型对象属性中查找给定名字的属性,如果找到则返回属性的值。比如:在调用 person1.sayName() 时,先查找实例 person1 有无 sayName 属性,若实例 person1 无, 则继续搜索 person1 的原型有无 sayName 属性。
1 <script> 2 function Person(){ 3 } 4 Person.prototype.name = "xiaoxu"; 5 var person1 = new Person(); 6 var person2 = new Person(); 7 //可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值。 8 //在实例中添加一个属性,该属性与实例原型中的一个属性同名,那么实例的属性将会屏蔽原型中的属性 9 person1.name = "zeng"; 10 console.log( person1.name ); //输出:zeng 11 console.log( person2.name ); //输出:xiaoxu 12 //使用 delete 操作符可以完全删除实例属性,删除之后可以重新访问原型中的属性 13 delete person1.name; 14 console.log( person1.name ); //输出:xiaoxu 15 </script>
使用 hasOwnProperty() 可以判断访问的是实例属性还是原型属性。
1 <script> 2 function Person(){ 3 } 4 Person.prototype.name = "xiaoxu"; 5 var person1 = new Person(); 6 var person2 = new Person(); 7 person1.name = "zeng"; 8 //实例属性返回 true 9 console.log( person1.hasOwnProperty("name")); //输出:true 10 //原型属性返回 false 11 console.log( person2.hasOwnProperty("name")); //输出:false 12 </script>
1 <script> 2 function Person(){ 3 } 4 Person.prototype.name = "xiaoxu"; 5 Person.prototype.age = 22; 6 Person.prototype.sayName = function(){ 7 console.log( this.name ); 8 }; 9 var person1 = new Person(); 10 var person2 = new Person(); 11 person1.name = "li"; 12 13 //单独使用 in 操作符 14 //in 操作符只要通过对象能够访问到属性就返回 true,无论是实例属性还是原型属性 15 //下面person1.name是实例属性,person2.name是原型属性 16 console.log( "name" in person1 ); //输出:true 17 console.log( person1.hasOwnProperty("name") ); //输出: true 18 console.log( "name" in person2 ); //输出:true 19 console.log( person2.hasOwnProperty("name") ); //输出: false 20 //同时使用 hasOwnProperty()方法和 in操作符可以确定属性是存在对象中还是存在原型中 21 function hasPrototypeProperty( object, name ){ 22 //为原型属性则返回 true, 否则返回 false 23 return !object.hasOwnProperty(name) && (name in object); 24 } 25 console.log( hasPrototypeProperty( person1, "name" ) ); //输出:false 26 console.log( hasPrototypeProperty( person2, "name" ) ); //输出:true 27 28 //在 for-in 循环中使用 in 操作符 29 //返回所有能够通过对象访问的、可枚举(enumerated)属性 30 //所有开发人员定义的属性都是可枚举的 31 //屏蔽了原型中不可枚举属性(即将 [[Enumerable]])标记为 false 属性)的实例属性也会在 for-in 循环中返回 32 var o = { 33 //原型的 toSrting() 方法的不可枚举属性[[Enumerable]]为false, 这里创建同名为toString()的实例属性,屏蔽了原型中的同名属性 34 toString : function(){ 35 return "My Object"; 36 } 37 }; 38 for( var prop in o ){ 39 if( prop == "toString" ){ 40 console.log( "Found toString" ); 41 } 42 } //输出:Found toString 43 44 //Object.keys() 45 //取得对象上所有可枚举的实例属性,返回一个数组 46 var keys = Object.keys( Person.prototype); //取得原型对象上所有可枚举的属性 47 console.log( keys ); //输出:(3) ["name", "age", "sayName"] 48 var person3 = new Person(); 49 person1.name = "dongxu"; 50 var person1Keys = Object.keys( person1 ); //取得实例对象person1上所有可枚举的属性 51 console.log( person1Keys ); //输出:["name"] 52 53 //Object.getOwnPropertyNames() 54 //得到所有实例属性,无论其是否可枚举 55 var keys = Object.getOwnPropertyNames( Person.prototype ); 56 console.log( keys ); //输出:(4) ["constructor", "name", "age", "sayName"] 57 //注意:结果中包含不可枚举的 constructor 属性 58 59 //Object.keys() 跟 Object.getOwnPropertyNames() 方法都可以用来替代 for-in 循环 60 </script>
1 <script> 2 function Person(){ 3 } 4 /* 5 Person.prototype.name = "xiaoxu"; 6 Person.prototype.age = 22; 7 Person.prototype.sayName = function(){ 8 console.log( this.name ); 9 }; 10 */ 11 //如上,每添加一个属性和方法都要敲一遍 Person.prototype, 为减少输入,可将 Person.prototype 设置为等于一个对象字面量形式的新对象 12 Person.prototype = { 13 name : "xiaoxu", 14 age : 22, 15 sayName : function(){ 16 console.log( this.name ); 17 } 18 }; 19 //此时本质上完全重写了默认的 prototype 对象,因此 constructor 属性变成了新对象的 constructor 属性,constructor 属性不再指向 Person 20 var person1 = new Person(); 21 console.log( person1 instanceof Object ); //输出:true 22 console.log( person1 instanceof Person ); //输出:true 23 console.log( person1.constructor instanceof Object ); //输出:true 24 console.log( person1.constructor instanceof Person ); //输出:false 25 </script>
如果 constructor 的值很重要,可以特意将它设置回适当的值。
1 <script> 2 function Person(){ 3 } 4 Person.prototype = { 5 constructor : Person, 6 name : "xiaoxu", 7 age : 22, 8 sayName : function(){ 9 console.log( this.name ); 10 } 11 }; 12 //重设 constructor 属性会导致它的 [[Enumerable]]特性被设置为 true,默认情况下原生的constructor 属性是不可枚举的。 13 //如果使用兼容 ECMAScript 5 的 JavaScript 引擎,可以用 Object.defineProperty() 重设 constructor 14 Object.defineProperty( Person.prototype, "constructor",{ 15 enumerable : false, 16 //重设 constructor 属性指向 prototype 所在函数的指针 17 value : Person 18 }); 19 </script>
可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来。
1 <script> 2 function Person(){} 3 var p1 = new Person(); 4 Person.prototype.sayHi = function(){ 5 console.log( "hi" ); 6 }; 7 p1.sayHi(); //输出:hi 8 </script>
但是,如果是重写整个原型对象,情况就不一样了,重写相当于将原型改为另一个对象就等于切断了构造函数与最初原型之间的联系。实例中的指针仅指向原型,而不指向构造函数。
1 <script> 2 function Person(){} 3 var p1 = new Person(); 4 Person.prototype = { 5 constructor : Person, 6 name : "xiaoxu", 7 sayName : function(){ 8 console.log( this.name ); 9 } 10 } 11 p1.sayName(); //报错 Uncaught TypeError: p1.sayName is not a function 12 </script>
根据个人理解,画出重写原型对象前后、重设constructor前后各个对象之间的关系:
所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
1 <script> 2 console.log( Array.prototype.sort ); //输出:ƒ sort() { [native code] } 3 console.log( String.prototype.substring ); //输出:ƒ substring() { [native code] } 4 </script>
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法。还可以修改原生对象的原型。但是不建议在产品化的程序中修改原生对象的原型。
1 <script> 2 //给基本包装类型 String 添加一个名为 someFn() 的方法 3 String.prototype.someFn = function( text ){ 4 return this+text; 5 }; 6 var msg = "hello"; 7 console.log(msg.someFn("123")); //输出:hello123 8 </script>
所有实例在默认情况下都取得相同的属性。原型中所有属性是被很多实例共享的。因此,很少有人单独使用原型模式。
1 <script> 2 function Person(){} 3 Person.prototype = { 4 constructor : Person, 5 name : "xiaoxu", 6 age : 22, 7 friends : ["mona", "nana"], 8 sayName : function(){ 9 console.log( this.name ); 10 } 11 } 12 var p1 = new Person(); 13 var p2 = new Person(); 14 //修改了 p1.friends 引用的数组之后 p2.friends 引用的数组也被修改了,因为实例共享一个数组 15 p1.friends.push( "zeng" ); 16 console.log( p1.friends ); //输出:(3) ["mona", "nana", "zeng"] 17 console.log( p1.friends ); //输出:(3) ["mona", "nana", "zeng"] 18 console.log( p1.friends == p2.friends ); //输出:true 19 </script>
是在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。这是用来定义引用类型的一种默认模式。
1 <script> 2 //构造函数用于定义实例属性 3 function Person(name, age){ 4 this.name = name; 5 this.friends = ["mona", "nana"]; 6 } 7 //原型模式用于定义方法和共享的属性 8 Person.prototype = { 9 constructor : Person, 10 sayName : function (){ 11 console.log( this.name ); 12 } 13 } 14 var p1 = new Person("xiaoxu", 22); 15 var p2 = new Person("Sammi", 33); 16 //数组friends是在构造函数中定义的,所以实例之间不共享数组friends, p1.friends跟p2.friends分别引用不同的数组。 17 p1.friends.push("miko"); 18 console.log( p1.friends ); //输出:(3) ["mona", "nana", "miko"] 19 console.log( p2.friends ); //输出:(2) ["mona", "nana"] 20 console.log( p1.friends == p2.friends ); //输出:false 21 //方法 sayName() 是用原型模式定义的,所以实例之间共享方法 sayName() 22 console.log( p1.sayName == p2.sayName ); //输出:true 23 </script>
动态原型模式把所有信息都封装在构造函数中,通过在构造函数中初始化原型保持了同时使用构造函数和原型的优点。
1 <script> 2 function Person(name, age){ 3 this.name = name; 4 this.age = age; 5 //下面这段代码仅仅在初次调用构造函数时才会执行,此后原型已经完成初始化。 6 if( typeof this.sayName != "function" ){ 7 //如果对原型进行修改,能够立即在所有实例中得到反映 8 //注意:不能使用对象字面量重写原型,否则会切断现有实例与新原型之间的联系。 9 Person.prototype.sayName = function(){ 10 console.log( this.name ); 11 }; 12 } 13 } 14 var friend = new Person("xiaoxu", 22); 15 //第一次调用 sayName() 时会初始化构造函数里的原型 16 friend.sayName(); //输出:xiaoxu 17 </script>
该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。返回的对象与构造函数外部创建的对象没有什么不同,所以在可以使用其他模式的情况下,不要使用这种模式。
1 <script> 2 function Person(name, age){ 3 //创建一个函数,该函数的作用仅仅是封装创建对象的代码 4 var o = new Object(); 5 o.name = name; 6 o.age = age; 7 o.sayName = function(){ 8 console.log( this.name ); 9 }; 10 //返回新创建的对象 11 return o; 12 } 13 var p1 = new Person( "xiaoxu", 22 ); 14 p1.sayName(); //输出:xiaoxu 15 16 //这个模式可以在特殊的情况下为对象创建构造函数,如果想创建一个具有额外方法的特殊数组,由于不能直接修改 Array 构造函数,因此可以使用这个模式。 17 //创建一个名为 NameArray 的构造函数 18 function NameArray(){ 19 //创建数组 20 var values = new Array(); 21 //添加值。用push()方法(用构造函数接收到的所有参数)初始化数组的值 22 values.push.apply(values, arguments); 23 //添加方法 24 values.toPipedString = function(){ 25 return values.join('|'); 26 } 27 //返回数组。以函数的形式返回数组 28 return values; 29 } 30 var names = new NameArray('xiaoxu', 'mona'); 31 console.log( names.toPipedString()); //输出:xiaoxu|mona 32 </script>
稳妥对象指的是没有公共属性,其方法也不引用 this 的对象。稳妥对象最适合放在一些安全的环境中(这些环境禁用 this 跟 new), 或者在防止数据被其他应用程序改动时使用。
1 <script> 2 function Person(name, age){ 3 //创建要返回的对象 4 var o = new Object(); 5 6 //可以在这里定义私有变量和函数 7 //添加方法 8 o.sayName = function(){ 9 console.log( name ); 10 }; 11 12 //返回对象 13 return o; 14 } 15 16 //变量 friend 中保存的是一个稳妥对象,除了调用 sayName() 方法外,没有别的方法可以访问传入够早函数中的原始数据。 17 //稳妥构造函数与寄生构造函数有2点不同: 18 //1. 新创建对象的实例方法不引用 this 19 //2. 不使用 new 操作符调用构造函数 20 var friend = Person( "xiaoxu", 22 ); 21 friend.sayName(); //输出:xiaoxu 22 </script>