继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和实现继承接口继承只实现方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要依靠原型链来实现的。
原型链
别忘记默认的原型
所有的函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是搜游自定义类型都会继承toString()、valueOf()、hasOwnProperty()、isPrototypeOf()、propertyIsEnumerable()、toLocalString()、toString()方法的根本原因。
谨慎地定义方法
子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了 SuperType SubType.prototype = new SuperType(); //添加新方法 SubType.prototype.getSubValue = function(){ return this.subproperty; }; //重写超类型中的方法 SubType.prototype.getSuperValue = function(){ return false; }; var instance = new SubType(); alert(instance.getSuperValue()); //false
|
当通过 SubType 的实例调用 getSuperValue() 时,调用的就是这个重新定义的方法;但通过 SuperType 的实例调用 getSuperValue()时,还会继续调用原来的那个方法。要额外注意的是,必须在用 SuperType 的实例替换原型之后,再定义这两个方法。
在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样就会重写原型链。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了 SuperType SubType.prototype = new SuperType(); //使用字面量添加新方法,会导致上一行代码无效 SubType.prototype = { geSubValue : function (){ return this.subproperty; }, someOtherMethod : function (){ return false; } }; var instance = new SubType(); alert(instance.getSuperValue()); //error!
|
以上代码展示了刚刚把 SuperType 的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个Object 的实例,而非 SuperType 的实例,因此我们设想中的原型已经被切断了---SubType 和 SuperType 之间已经没有关系了。
原型链的问题
最主要的问题来自包含引用类型值的原型。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ } //继承了SuperType SubType.prototype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" var instance2 = new SubType(); alert(instance2.colors); //"red,blue,green,black"
|
原型链的第二个问题是:没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上由于原型中包含引用类型值所带来的问题,实践中会单独使用原型链。
借用构造函数
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫借用构造函数的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ //继承SuperType SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.color); //"red,blue,green,black" var instance2 = new SubType(); alert(instance2.color); //"red,blue,green"
|
“借调”了超类型的构造函数,实际上是在(未来将要)新创建的SubType实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会有自己的 colors 属性的副本了
传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function SuperType(name){ this.name = name; } function SubType(){ //继承了SuperType,同时还传递了参数 SuperType.call(this,"Nicholas"); //实例属性 this.age = 29; } var instance = new SubType(); alert(instance.name); //"Nicholas"; alert(instance.age); //29
|
为了确保 SuperType 构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
借用构造函数的问题
方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。
组合继承
组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function SuperType(name){ this.name = name; this.colors = ["red","blue","green"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ //继承属性 SuperType.call(this,name); this.age = age; } //继承方法 SubType.prototype = new SuperType(); SubType.Prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); } var instance1 = new SubType("Nicholas", 29); instance1.color.push("black"); alert(instance1.color); //"red,blue,green,black" instance1.sayName(); //"Nicholas" instance1.sayAge(); //29 var instance2 = new SubType("Greg", 27); alert(instance2.color); //"red,blue,green" instance2.sayName(); //"Greg" instance2.sayAge(); //27
|
组合继承避免了原型链和构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof 和 isPrototypeOf() 也能够用于识别组合继承创建的对象。
原型式继承
ECMAScript5 通过新增 Object.create() 方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
|
Object.create() 方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
1 2 3 4 5 6 7 8 9 10 11 12
| var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg" } }); alert(anotherPerson.name); //"Greg"
|
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类型的情况下,原型式继承是完全可以胜任的。不过,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,它与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function createAnother(original) { var clone = object(original); //通过调用函数创建一个新对象 clone.sayHi = function(){ //以某种方式来增强这个对象 alert("hi") }; return clone; //返回这个对象 } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anothorPerson = createAnother(person); anotherPerson.sayHi(); //"hi"
|
新对象不仅具有 person 的所有属性和方法,而且还有自己sayHi()方法
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object() 函数不是必需的;任何能够返回新对象的函数都适用于此模式。
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
寄生组合式继承
组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ SuperTpye.call(this, name); //第二次调用 SuperType() this.age = age; } SubType.prototype = new SuperType(); //第一次调用 SuperType() SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); };
|
第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors; 它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
好在已经找到了解决这个问题的方法---寄生组合式继承。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function inheritPrototype(subType, superType){ var prototype = Object(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subTye.prototype = prototype; //指定对象 } function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this.name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); };
|
这个例子的高效率体现在它值调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的,多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。