本文系统讲解JavaScript的六种继承模式。首先分析原型链与借用构造函数的局限性,随后介绍结合两者的组合继承。接着探讨原型式与寄生式继承,最终引出最高效的寄生组合式继承,解析其如何避免构造函数重复调用并优化性能。
关于JavaScript继承机制的讲解一直备受关注,本文将直接切入核心内容,不再赘述背景。
既然关注继承问题,说明读者已具备JavaScript面向对象的基础知识。若对工厂模式、构造函数模式、原型模式、混合模式及动态原型模式尚存疑问,可参考相关基础教程。本文将重点阐述实现JavaScript继承的主要技术路径。
原型链
原型链
在JavaScript中,实现继承最直观的方法是利用原型链。具体做法是将子类型的原型对象指向父类型的实例,即执行“子类型.prototype = new 父类型()”这一操作。具体实现代码如下:
// 为父类型创建构造函数
function SuperType() {
this.name = ['wuyuchang', 'Jack', 'Tim'];
this.property = true;
}
// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
return this.property;
}
// 为子类型创建构造函数
function SubType() {
this.test = ['h1', 'h2', 'h3', 'h4'];
this.subproperty = false;
}
// 实现继承的关键步骤,子类型的原型指向父类型的实例
SubType.prototype = new SuperType();
// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
/* 以下为测试代码示例 */
var instance1 = new SubType();
instance1.name.push('wyc');
instance1.test.push('h5');
alert(instance1.getSuerperValue()); // true
alert(instance1.getSubValue()); // false
alert(instance1.name); // wuyuchang,Jack,Tim,wyc
alert(instance1.test); // h1,h2,h3,h4,h5
var instance2 = new SubType();
alert(instance2.name); // wuyuchang,Jack,Tim,wyc
alert(instance2.test); // h1,h2,h3,h4
上述代码展示了通过原型链实现的简单继承结构,但测试结果表明其中存在明显缺陷。参考《面向对象JS基础讲解》一文可知,原型链的第一个问题在于:由于子类型的原型是父类型的实例,父类型的属性会包含在子类型原型中,导致引用类型值的原型属性被所有实例共享。例如代码中的 `instance1.name.push('wyc')` 即可验证此共享现象。第二个问题则是:在创建子类型实例时,无法向超类型的构造函数传递参数。鉴于这些限制,实际开发中极少单独使用原型链。
借用构造函数
借用构造函数
为了克服原型链的上述两个缺陷,开发者采用了一种称为“借用构造函数”的技术。其核心思路是在子类型的构造函数内部调用父类型的构造函数。鉴于函数本质是在特定环境中执行代码的对象,可以通过 `apply()` 或 `call()` 方法来执行构造函数。具体代码如下:
// 为父类型创建构造函数
function SuperType(name) {
this.name = name;
this.color = ['pink', 'yellow'];
this.property = true;
this.testFun = function() {
alert('http://tools.jb51.net/');
}
}
// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
return this.property;
}
// 为子类型创建构造函数
function SubType(name) {
SuperType.call(this, name);
this.test = ['h1', 'h2', 'h3', 'h4'];
this.subproperty = false;
}
// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
/* 以下为测试代码示例 */
var instance1 = new SubType(['wuyuchang', 'Jack', 'Nick']);
instance1.name.push('hello');
instance1.test.push('h5');
instance1.color.push('blue');
instance1.testFun(); // http://tools.jb51.net/
alert(instance1.name); // wuyuchang,Jack,Nick,hello
// alert(instance1.getSuerperValue()); // error 报错
alert(instance1.test); // h1,h2,h3,h4,h5
alert(instance1.getSubValue()); // false
alert(instance1.color); // pink,yellow,blue
var instance2 = new SubType('wyc');
instance2.testFun(); // http://tools.jb51.net/
alert(instance2.name); // wyc
// alert(instance2.getSuerperValue()); // error 报错
alert(instance2.test); // h1,h2,h3,h4
alert(instance2.getSubValue()); // false
alert(instance2.color); // pink,yellow
从上述代码可见,子类型 `SubType` 的构造函数通过调用 `SuperType.call(this, name)` 实现了属性的继承,并支持在创建子类型实例时向父类型传递参数。然而,新的问题随之产生:尽管在父类型构造函数中定义了 `testFun` 方法,并在原型中定义了 `getSuperValue` 方法,但实例化子类型后,无法调用父类型原型中的 `getSuperValue` 方法,只能访问构造函数内的 `testFun`。这类似于仅使用构造函数模式创建对象,导致函数无法复用。因此,借用构造函数技术也很少单独使用。
组合继承
组合继承(原型链+借用构造函数)
顾名思义,组合继承融合了原型链与借用构造函数的优势。其实现逻辑简单明了:结合两者的优点,即通过原型链继承方法,通过构造函数继承属性。具体代码实现如下:
// 为父类型创建构造函数
function SuperType(name) {
this.name = name;
this.color = ['pink', 'yellow'];
this.property = true;
this.testFun = function() {
alert('http://tools.jb51.net/');
}
}
// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
return this.property;
}
// 为子类型创建构造函数
function SubType(name) {
SuperType.call(this, name);
this.test = ['h1', 'h2', 'h3', 'h4'];
this.subproperty = false;
}
SubType.prototype = new SuperType();
// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
/* 以下为测试代码示例 */
var instance1 = new SubType(['wuyuchang', 'Jack', 'Nick']);
instance1.name.push('hello');
instance1.test.push('h5');
instance1.color.push('blue');
instance1.testFun(); // http://tools.jb51.net/
alert(instance1.name); // wuyuchang,Jack,Nick,hello
alert(instance1.getSuerperValue()); // true
alert(instance1.test); // h1,h2,h3,h4,h5
alert(instance1.getSubValue()); // false
alert(instance1.color); // pink,yellow,blue
var instance2 = new SubType('wyc');
instance2.testFun(); // http://tools.jb51.net/
alert(instance2.name); // wyc
alert(instance2.getSuerperValue()); // true
alert(instance2.test); // h1,h2,h3,h4
alert(instance2.getSubValue()); // false
alert(instance2.color); // pink,yellow
上述代码通过 `SuperType.call(this, name)` 继承父类型属性,并通过 `SubType.prototype = new SuperType()` 继承父类型方法。这种方式有效解决了原型链与借用构造函数各自的缺陷,成为JavaScript中最常用的实例继承方法。然而,组合继承并非完美:在通过原型链继承方法时,实际上已经继承了父类型的属性(尽管此时引用类型仍共享)。随后在子类型构造函数中再次调用父类型构造函数以继承属性并覆盖原型中的属性,这导致构造函数被调用两次,显得冗余。那么,如何解决这一问题?在探讨解决方案前,先介绍以下两种模式。
原型式继承
原型式继承
原型式继承的实现方式与普通继承不同,它不严格依赖构造函数,而是利用原型机制基于已有对象创建新对象,且无需创建自定义类型。具体代码如下:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
代码示例:
/* 原型式继承 */
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name : 'wuyuchang',
friends : ['wyc', 'Nicholas', 'Tim']
}
var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Bob');
var anotherPerson2 = object(person);
anotherPerson2.name = 'Jack';
anotherPerson2.friends.push('Rose');
alert(person.friends); // wyc,Nicholas,Tim,Bob,Rose
寄生式继承
寄生式继承
/* 寄生式继承 */
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert('hi');
}
return clone;
}
使用示例:
/* 原型式继承 */
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
/* 寄生式继承 */
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert('hi');
}
return clone;
}
var person = {
name : 'wuyuchang',
friends : ['wyc', 'Nicholas', 'Rose']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi();
寄生组合式继承
寄生组合式继承
前文已提及组合模式实现继承的缺点,现在我们将解决该问题。其思路是:构造函数用于继承属性,原型链混成形式用于继承方法,即在继承方法时不再实例化父类型的构造函数。具体代码如下:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
/* 寄生组合式继承 */
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
使用时,只需将组合模式中的 `SubType.prototype = new SuperType()` 替换为 `inheritPrototype(subType, superType)` 即可。寄生组合式继承的高效之处在于它仅调用一次父类型构造函数,避免了创建不必要或多余的属性。同时,原型链保持不变,因此仍能正常使用 `instanceof` 和 `isPrototypeOf()`。这是目前最理想的继承方式,也是当前技术转型的主要方向(例如YUI库也采用了此模式)。
本文内容参考《JavaScript高级程序设计第3版》,代码经过改写与注释优化,旨在提升可读性。如果您对JavaScript继承有独到见解,欢迎留言分享,供其他读者参考!