《JavaScript高级程序设计》第六章【面向对象的程序设计】 包括对象、创建对象、继承
- 一、理解对象
- 二、创建对象
- 1. 工厂模式
- 2. 构造函数模式
- 3. 原型模式
- 4. 组合使用构造函数模式和原型模式【使用最广泛】
- 5. 动态原型模式
- 6. 寄生构造函数模式
- 7. 稳妥构造函数模式
- 三、继承
- 1. 原型链
- 2. 借用构造函数
- 3. 组合继承【最常用】
- 4. 原型式继承
- 5. 寄生式继承
- 6. 寄生组合式继承
一、理解对象
ECMAScript中有两种属性:数据属性和访问器属性。
二、创建对象
1. 工厂模式
使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这种方法后来被构造函数模式所取代。
2. 构造函数模式
可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。但是它的每个成员都无法得到复用,包括函数。
但是这样说好像也不准确——如果是通过一个指针指向构造函数外部的函数的话,应该算是复用?
1 function Person(name,age){ 2 this.name = name; 3 this.age = age; 4 this.sayName = sayName; //一个指向函数的指针,所以所有的实例共享同一函数 5 this.sayAge = function(){ 6 console.log(this.age); 7 } 8 } 9 function sayName(){ 10 console.log(this.name); 11 } 12 var person1 = new Person('Jack'); 13 var person2 = new Person('Amy'); 14 person1.sayName(); 15 person2.sayName(); 16 console.log(person1.sayName === person2.sayName); //true 17 console.log(person1.sayAge == person2.sayAge); //false
但是这种方法:1)sayName函数在全局作用域中定义,但实际只被某个对象调用,名不副实
2)没有封装性
3. 原型模式
使用构造函数的prototype属性来指定那些应该共享的属性和方法。
4. 组合使用构造函数模式和原型模式【使用最广泛】
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的熟悉。且这种模式还支持构造函数传递参数
1 function Person(name){ 2 this.name = name; 3 } 4 Person.prototype = { 5 constructor : Person, //因为这里通过对象字面量重写了整个原型对象,但constructor会指向Object。所以要特意设置 6 sayName : function(){ 7 console.log(this.name); 8 } 9 } 10 var person1 = new Person('Nick'); 11 var person2 = new Person('Amy'); 12 console.log(person1.sayName == person2.sayName); //true
5. 动态原型模式
1 function Person(name){ 2 this.name = name; 3 if(typeof this.sayName != "function"){ //这段代码只会在在初次调用构造函数时才会执行 4 Person.prototype.sayName = function(){ 5 console.log(this.name); 6 } 7 } 8 } 9 var person = new Person('Jack');
可以使用instanceof操作符来确定实例类型
注意:使用动态函数模型时,不能使用对象字面量重写原型。
在已经创建了实例的情况下重写原型,就会切断现有实例和新原型之间的关系。
举个栗子:
1 function Person(name){ 2 this.name = name; 3 if(typeof this.sayName != "function"){ //这段代码只会在在初次调用构造函数时才会执行 4 Person.prototype.sayName = function(){ 5 console.log(this.name); 6 } 7 } 8 } 9 var person = new Person('Jack'); 10 Person.prototype = { 11 sayHi : function(){ 12 console.log("hi"); 13 } 14 } 15 person.sayName(); //Jack 16 //person.sayHi(); //Uncaught TypeError: person.sayHi is not a function 17 18 var person2 = new Person('Amy'); 19 person2.sayHi(); //hi 20 person2.sayName(); //Amy
打印出person和person2:
这里我的理解是:调用构造函数会为实例增加一个指向最初原型的[[prototype]]指针,而把原型修改为另一个对象,则会切断构造函数与最初原型之间的关系。因为实例中的指针只会指向原型,而不指向构造函数,此时修改的是构造函数的原型,而之前创建的实例仍然指向之前的原型对象。故其仍然用于sayName()函数。然而,为什么后来创建的实例person2也会拥有sayName()函数呢?不是之前的构建已经切断了吗?这里我猜测原因是因为在创建实例person2时,if逻辑检测到Person中没有sayName()函数,于是又增加了这样一个函数。为了验证猜想,我在if逻辑里打印一句话,这样只要进入循环就会打印出来这句话,果然打印了两次,猜想得证。
6. 寄生构造函数模式
基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
常用于为对象创建构造函数。
一个栗子:创建一个具有额外方法的特殊数组
1 function SpecialArray(){ 2 //创建数组 3 var values = new Array(); 4 5 //添加值 6 values.push.apply(values, arguments); //这里比较疑惑的是为什么要用apply,既然values是一个数组,那么直接调用push不可以吗? 7 //values.push(arguments); //[object Arguments] 8 9 //添加方法 10 values.toPipedString = function(){ 11 return this.join("|"); 12 }; 13 14 //返回数组 15 return values; 16 } 17 var color = new SpecialArray("red", "blue", "green"); 18 console.log(color.toPipedString()); //red|blue|green
上述代码中遇到了一个问题:如第6.7行所示,为什么要用apply而不能直接使values.push(arguments)呢?
——换成直接使用push后输出是[object Arguments]。然后查了下发现arguments果然是一个对象,那为什么apply中可以直接用?猜想应当是apply内部实现对arguments进行了解析。
arguments并不是一个数组,而是一个伪数组,具有length属性. 这里也可以直接用[].push来代替.
关于寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没什么不同。为此,不能依赖instanceof操作符来确定对象类型。
所以,可以用其他,不要用这个方法。
7. 稳妥构造函数模式
稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象适用在一些安全的环境中(禁止this和now),或在防止数据被其他应用程序改动时使用。
function Person(name){ var o = new Object(); //在这里定义私有变量和函数 o.sayName = function(){ console.log(name); }; return o; } var person = Person("nick"); person.sayName(); //nick
这种方法与上一个方法的区别在于:新创建对象的实例方法不引用this,不使用new操作符调用构造函数。
这种方法除了调用sayName()函数外,没有别的办法可以访问到其数据成员,所以具有安全性。
instanceof操作符对这种方法也无效。
三、继承
两种继承方式:接口继承和实现继承。ECMAScript只支持实现继承,且其实现继承主要是依靠原型链来实现的。
1. 原型链
原型链实现继承的基本思想:用原型链让一个引用类型继承另一个引用类型的属性和方法。
构造函数,原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,如果让原型对象等于另一个对象的实例,则原型对象将包含一个指向另一个原型的指针,另一个原型中也包含一个指向另一个构造函数的指针。(这都是什么鬼!)
实现原型链模式:
function Parent(){ this.name = "parent"; } Parent.prototype.getName = function(){ return this.name; } function Child(){ this.subName = 'child'; } //继承了Parent Child.prototype = new Parent(); Child.prototype.getSubName = function(){ return this.subName; }; var child = new Child(); console.log(child.getName()); //parent console.log(child.getSubName()); //child //两种确定原型和实例的关系的方法 //1)instanceof console.log(child instanceof Object); //true console.log(child instanceof Parent); //true console.log(child instanceof Child); //true //isPrototypeOf() console.log(Object.prototype.isPrototypeOf(child)); //true console.log(Parent.prototype.isPrototypeOf(child)); //true console.log(Child.prototype.isPrototypeOf(child)); //true
子类型有时候需要重写超类中的某个方法,或者需要添加超类中不存在的某个方法,给原型添加方法的代码一定要放到替换原型的语句之后。否则,子类的方法会被覆盖。因为原型指针指向了父类的原型。
1 function Parent(name){ 2 this.name = name; 3 } 4 Parent.prototype.sayName = function(){ 5 console.log("Parent: my name is " + this.name); 6 } 7 function Child(name,age){ 8 this.name = name; 9 } 10 Child.prototype.sayName = function(){ 11 console.log("Child: my name is "+ this.name); 12 }; 13 Child.prototype = new Parent(); 14 var child = new Child('Amy'); 15 16 child.sayName(); //Parent: my name is Amy
原型链继承的问题:原先的实例属性也会变成原型属性。且不能向构造函数传递参数
2. 借用构造函数
apply()和call()
3. 组合继承【最常用】
使用原型链实现对原型属性和方法的继承,而通过构造函数实现对实例属性的继承。
instanceof和isPrototype()也能够识别基于组合继承创建的对象
1 function Parent(name){ 2 this.name = name; 3 } 4 Parent.prototype.sayName = function(){ 5 console.log(this.name); 6 } 7 function Child(name,age){ 8 Parent.call(this,name); 9 this.age = age; 10 } 11 Child.prototype = new Parent(); 12 Child.prototype.sayAge = function(){ 13 console.log(this.age); 14 } 15 var child = new Child('Jack',27); 16 child.sayName(); //Jack 17 child.sayAge(); //27
4. 原型式继承
思想:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。如下:
1 function object(o){ 2 function F(){} //创建一个临时的构造函数 3 F.prototype = o; //将传入的对象作为这个构造函数的原型 4 return new F(); //返回这个临时类型的一个实例 5 }
这种方法要求必须有一个对象可以作为另一个对象的基础。
ECMAScript5中新增了一个函数实现了原型式继承:Object.create()。
Object.create()函数接收两个参数:一个用于新对象原型,一个为新对象定义额外属性的对象。
这种方法也可能会使引用类型值的属性共享
1 var Person = { 2 name: "Nick", 3 friends: [1,2,3,4] 4 }; 5 var anotherPerson = Object.create(Person); 6 anotherPerson.name = "Joe"; 7 anotherPerson.friends.push(5); 8 9 var yetAnotherPerson = Object.create(Person); 10 yetAnotherPerson.name = "Amy"; 11 yetAnotherPerson.friends.push(6); 12 13 console.log(Person.friends); //[1,2,3,4,5,6] 14 15 var person2 = Object.create(Person, { 16 name: { 17 value: "Lee" 18 } 19 }); 20 console.log(Person.name); //Nick 21 console.log(person2.name); //Lee 22 console.log(person2); //name:Nick在其原型之中。原型链会先找实例属性
5. 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,然后返回对象
1 function createAnothor(o){ 2 var clone = Object.create(o); 3 clone.sayHi = function(){ 4 console.log("hi"); 5 }; 6 return clone; 7 } 8 var person = { 9 name : "Nick", 10 } 11 var anotherPerson = createAnothor(person); 12 anotherPerson.sayHi();
这种模式用于主要考虑对象不是自定义类型和构造函数的情况下。
这种方法来为对象添加函数,也不能做到函数复用。
6. 寄生组合式继承
之前提到的组合函数的不足在于:无论什么情况下,都会调用两次超类型构造函数。一次是在创建子类原型的时候,一次是在子类构造函数内部。
所谓寄生式组合继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
1 function inheritPrototype(Child, Parent){ 2 var prototype = Object.create(Parent.prototype); //创建对象 3 prototype.constructor = Child; //增强对象 4 Child.prototype = prototype; //指定对象 5 } 6 function Parent(name){ 7 this.name = name; 8 } 9 Parent.prototype.sayName = function(){ 10 console.log(this.name); 11 }; 12 function Child(name,age){ 13 Parent.call(this,name); 14 this.age = age; 15 } 16 inheritPrototype(Child,Parent); 17 Child.prototype.sayAge = function(){ 18 console.log(this.age); 19 } 20 var child = new Child('Jack',29); 21 child.sayName(); 22 child.sayAge(); 23 console.log(child); //这时,child的__proto__中就不会有从父类继承来的name和age属性了
用寄生组合式继承打印出来的child:
很干净。而如果用组合继承的话:
可以看到其原型中有一个多余的name属性。