JavaScript 继承的六大模式详解与对比
本文系统梳理了 JavaScript 中六种经典的继承模式,包括原型链、借用构造函数、组合继承、原型式继承、寄生式继承以及寄生组合式继承。重点分析了各模式的实现原理、优缺点及适用场景,特别指出组合继承虽常用但存在调用两次超类型构造函数的问题,并推荐寄生组合式继承为引用类型最理想的继承范式,结合代码示例深入解析其实现机制。
原型链与借用构造函数
1. 原型链
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;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
其实现机制的核心在于重写原型对象,具体做法是用一个新类型的实例替换原有的原型对象。
2. 借用构造函数
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
//继承了SuperType
SuperType.call(this);
}
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"
若仅使用借用构造函数,则无法解决构造函数模式的固有缺陷——即所有方法均在构造函数内部定义,导致函数复用无从谈起。此外,超类型原型中定义的方法在子类型中不可见,这使得所有类型只能依赖构造函数模式。鉴于上述局限,借用构造函数技术极少被单独使用。
3. 组合继承
组合继承模式
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
在此示例中,SuperType 构造函数定义了两个属性:name 和 colors。同时,SuperType 的原型定义了一个方法 sayName()。SubType 构造函数在调用 SuperType 构造函数时传入了 name 参数,随后又定义了自身属性 age。接着,将 SuperType 的实例赋值给 SubType 的原型,并在新原型上定义方法 sayAge()。如此设计,使得两个不同的 SubType 实例既能拥有各自独立的属性(包括 colors 属性),又能共享相同的方法。
组合继承克服了原型链和借用构造函数各自的缺陷,融合了二者的优势,成为 JavaScript 中最常用的继承模式。此外,instanceof 和 isPrototypeOf() 方法也可用于识别基于组合继承创建的对象。
4. 原型式继承
原型式与寄生式继承
function object(o){
function F(){}
F.prototype = o;
return new F();
}
在 object() 函数内部,首先创建一个临时性的构造函数,然后将传入的对象作为该构造函数的原型,最后返回该临时类型的一个新实例。从本质上看,object() 对传入的对象执行了一次浅复制。参考以下示例。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
克罗克福德提倡的原型式继承要求必须存在一个可作为其他对象基础的现有对象。若具备此类对象,可将其传递给 object() 函数,随后根据具体需求对返回的对象进行修改。例如,此处以 person 对象作为基础传入 object() 函数,函数返回一个新对象。该新对象以 person 为原型,因此其原型中包含基本类型值属性和引用类型值属性。这意味着 person.friends 不仅属于 person 所有,也会被 anotherPerson 和 yetAnotherPerson 共享。实际上,这相当于创建了 person 对象的两个副本。
ECMAScript 5 通过新增 Object.create() 方法规范了原型式继承。该方法接收两个参数:一个用作新对象原型的对象,以及(可选的)一个用于为新对象定义额外属性的对象。当仅传入一个参数时,Object.create() 的行为与 object() 方法相同。
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() 方法的第二个参数相同:每个属性均通过自身的描述符定义。以此方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"
支持 Object.create() 方法的浏览器包括 IE9+、Firefox 4+、Safari 5+、Opera 12+ 和 Chrome。
在无需大规模创建构造函数,仅需让一个对象与另一个对象保持相似性的场景下,原型式继承完全胜任。但需注意,包含引用类型值的属性始终会共享相应值,这与使用原型模式时的行为一致。
5. 寄生式继承
寄生式(parasitic)继承是与原型式继承紧密相关的思路,同样由克罗克福德推广。其思路与寄生构造函数和工厂模式类似:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后像正常返回对象一样返回结果。以下代码展示了寄生式继承模式的实现。
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
在此示例中,createAnother() 函数接收一个参数,即作为新对象基础的原始对象。随后,将该对象(original)传递给 object() 函数,将返回的结果赋值给 clone。接着为 clone 对象添加一个新方法 sayHi(),最后返回 clone 对象。可如下调用 createAnother() 函数:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
该示例中的代码基于 person 返回了一个新对象——anotherPerson。新对象不仅具备 person 的所有属性和方法,还拥有自己的 sayHi() 方法。 在主要关注对象而非自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前文示例中使用的 object() 函数并非必需;任何能返回新对象的函数均可适用于此模式。
使用寄生式继承为对象添加函数时,由于无法做到函数复用,会导致效率降低;这一点与构造函数模式类似。
6. 寄生组合式继承
前文提及,组合继承是 JavaScript 最常用的继承模式,但它也存在不足。组合继承的最大问题在于,无论何种情况,都会调用两次超类型构造函数:一次是在创建子类型原型时,另一次是在子类型构造函数内部。确实,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。再回顾以下组合继承示例。
寄生组合式继承
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); //第二次调用SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
加粗字体的行中是调用 SuperType 构造函数的代码。在第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors;它们均为 SuperType 的实例属性,只不过此时位于 SubType 的原型中。当调用 SubType 构造函数时,会再次调用 SuperType 构造函数,此次在新对象上创建了实例属性 name 和 colors。于是,这两个属性便屏蔽了原型中的两个同名属性。图 6-6 展示了上述过程。 如图 6-6 所示,存在两组 name 和 colors 属性:一组在实例上,另一组在 SubType 原型中。这就是调用两次 SuperType 构造函数的结果。好在我们已找到解决此问题的方法——寄生组合式继承。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示。
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
此示例中的 inheritPrototype() 函数实现了寄生组合式继承的最简形式。该函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认 constructor 属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inherit-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() 仍可正常使用。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
YUI 的 YAHOO.lang.extend() 方法采用了寄生组合式继承,从而使该模式首次出现在一个应用极为广泛的 JavaScript 库中。欲了解有关 YUI 的更多信息,请访问 http://developer. yahoo.com/yui/。
以上所述即为本文的全部内容,希望对大家学习 JavaScript 继承有所帮助。
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01
