JavaScript中的继承
本篇文章的内容主要来自于《JavaScript权威指南(第六版)》和《JavaScript高级程序设计(第三版)》,基于ES5;
JavaScript中没有像Java中的接口或者父类的概念。但是像其他面向对象语言一样,它也有继承的概念,只是实现方式不同:JavaScript中的继承是通过原型链来实现的。我门在JavaScript中的原型与原型链中提到过。现在我们详细讨论下。
我们先看看JavaScript中的几种继承方式。
一、完全使用原型链
我们都知道,JavaScript对象会继承原型链上的属性。当我们通过对象o访问属性x,如果o中没有,就去原型链中一级一级找,最终返回属性值或属性x;当我们给x赋值时,如果o中有属性x,则改变x的值;如果没有,就给o新增一个属性x并赋值,不会影响到原型链,从而也不会影响到有相同原型链的其他对象。但是这存在一个问题:当原型链上的某个属性为引用类型时,当我们不修改这个引用的指向,而通过这个引用修改引用所指的对象时。修改会影响到有相同原型链的所有对象实例:
//本代码片段来自于《JavaScript高级程序设计(第三版)》167页
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
完全使用原型链还有一个问题:不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类的构造函数传递参数。
二、借用构造函数
使用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
//本代码片段来自于《JavaScript高级程序设计(第三版)》168页
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
在SubType的构造函数中调用了SuperType,每一次创建一个SubType的实例,都会执行一次SuperType和SubType的初始化方法。所以每个对象实例都具有SubType和SupType定义的属性的副本,相互之间不会有任何影响。而且可以看到,这种方式在创建对象时可以传递参数。
然而,这种方式也有缺点:
第一,超类的原型对象不在子类的原型链中,所以在超类原型型定义的属性,对子类型而言是不可见的;
第二,方法在构造参数中定义,每创建一个对象,都会重新生成方法,因此方法无法重用:
function SubType(){ this.func=function(){ console.info("func") } } var instance1 = new SubType(); var instance2 = new SubType(); instance1.func()//func instance2.func()//func console.info(instance1.func===instance2.func)//false
三、组合继承(最常见模式)
指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
//本代码片段来自于《JavaScript高级程序设计(第三版)》168页
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.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Nicholas"; instance1.sayAge(); //29 var instance2 = new SubType("Greg", 27); alert(instance2.colors); //"red,blue,green" instance2.sayName(); //"Greg"; instance2.sayAge(); //27
这是JavaScript中最常见的继承方式。但是也有缺点,原型对象中定义了不必要的属性,在SuperType构造函数中定义的属性存在于SuperType和SubType对象实例中,而SuperType实例作为SubType实例的原型对象,SuperType实例中的这些属性在SubType实例中始终被覆盖。这个缺点将在下面说到的寄生式组合继承中解决。
四、原型式继承
//本代码片段来自于《JavaScript高级程序设计(第三版)》169页
function object(o){ function F(){} F.prototype = o; return new F(); }
这种继承方式以一个已有的对象作为原型创建对象,这种方式与Object.create()。显然这种方式与完全使用原型链的方式有相同的缺点,在只想让一个对象与另一个对象保持类似的情况下,可以使用。同时它也是下面说到的另一种继承方式的基础:寄生式继承。
五、寄生式继承
//本代码片段来自于《JavaScript高级程序设计(第三版)》171页
function createAnother(original){ var clone = object(original); //通过调用函数创建一个新对象 clone.sayHi = function(){ //以某种方式来增强这个对象 alert("hi"); }; return clone; //返回这个对象 } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi
在不考虑自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
六、寄生组合式继承 (最理想的模式)
前面我们说到组合式继承在原型对象中定义了不必要的属性。寄生式组合式继承可以避免这个问题:
//本代码片段来自于《JavaScript高级程序设计(第三版)》173页
function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subType.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); };
可以看到通过把SubType的原型指向一个对象,这个对象本身除了一个指向SubType的构造函数外,没有任何属性,并且的原型是SuperType。实现了对SuperType的继承,并且没有定义不必要的属性。
总结
可以看到JavaScript中的继承主要依赖原型链,为了克服原型链的某些缺点,我们使用了构造函数。最后一种方式是最理想的方式,组合式继承最常见,但是其他方式也都各有其实用的范围。
在寄生式组合继承中我最初想为什么不直接把SubType的原型指定为为SuperType的原型,不是更方便吗?但是仔细一想,如果有不止一个SubType,那么一个SubType在原型中定义的属性就会在其他所有的SubType中都可见!!而且原型的constructor也指向了错误的类型!算是自己一个错误的尝试吧,记下来给自己个提醒!