一、原型链
JavaScript 中原型链是实现继承的主要方法。其主要的思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现原型链有一种基本模式,其代码如下。
function SuperType() { this.property = true; } Super.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 继承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function() { return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); // true;
这种继承的本质是重写原型对象,代之以一个新类型的实例。上面这段代码对应的对象关系图如下:
原型链实现的继承的主要问题是:a:当原型包含引用类型的值时,此属性会被所有实例共享; b:在创建子类型的实例时,不能向超类型的构造函数中传递参数。
二、借用构造函数
借用构造函数也叫伪造对象或经典继承。其思想是在子类型的构造函数的内部调用超类型构造函数(这里的调用是指以普通函数的调用方式调用,而不是使用new操作符)。
function SuperType(name) { this.name = name; } function SubType() { // 继承了SuperType, 同时传递了参数 SuperType.call(this, "Haha"); // 实例属性 this.age = 34; } var instance = new SubType(); console.log(instance.name); // "Haha"
这样以后,SubType的每个实例都会有自己的实例属性的副本了。同时也可以在子类型构造函数中向超类型构造函数传递参数。同时,带来的问题是:方法都在构造函数中定义,函数复用无从谈起。而且在超类型的原型中定义的方法,对子类型而言是不可见的。
三、组合继承
组合继承也叫做伪经典继承,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(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() { console.log(this.age); }; var instance1 = new SubType("Haha", 23); var instance2 = new SubType("Hehe", 45);
两个不同的SubType实例即分别拥有了自己的属性,又可以使用相同的方法。这里的缺点是:实例和实例的原型对象中,都存有父类的实例属性,不是特别完美。这个问题的解决方案是寄生组合式继承。
四、原型式继承
基本思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o) { function F() {} F.prototype = o; return new F(); } var person = { name: "Haha", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob");
这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。ECMAScript 5中通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。这种方式定义的属性会覆盖原型对象上的同名属性。
五、寄生式继承
寄生式继承的思路与计生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
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);
这种方式使用了原型式继承,并在自己的实例对象上,又添加了自己的方法。在主要考虑对象而不是自定义类型和构造函数的情况下,继承式继承是一种有用的模式。示例代码中的object()函数不是必需的;任何能够返回新对象的函数都适用此模式。
六、寄生组合式继承
前面的第三种组合式继承模式最大的问题是:无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。解决办法是使用寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。背后的思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。所以,我们可以使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
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.syaName = function() { console.log(this.name); }; inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function() { console.log(this.age); };
这个例子的高效率体现在它只调用了一次SuperType构造函数,并且避免了在SubType.prototype上创建不必要的、多余的属性。同时,保持了原型链不变。能正常使用instanceof和isPrototypeOf()。这就是关于JavaScript中对象继承的完美的解决方案。
七、总结
当我们仔细去分析前面的六种方法的时候,其中的某几个模式之间的差异是非常小的。其实所有的这些模式无非就是利用了JavaScirpt的那些底层特性:构造函数、原型链、不同对象中属性的访问性的差异。