skip to content
月与羽

继承

/ 10 min read


核心概念:原型链 (Prototype Chain)

在理解继承之前,必须先理解原型链。

  1. 每个对象都有一个原型:每个 JavaScript 对象(除了少数例外)都有一个内部私有属性 [[Prototype]],你可以通过 __proto__ (非标准但常用) 或 Object.getPrototypeOf() 来访问它。这个属性指向另一个对象,我们称之为该对象的“原型”。
  2. 原型也是对象:这个原型对象也有自己的原型,依此类推,直到一个对象的原型为 null
  3. 形成链条:由 [[Prototype]] 串联起来的这一系列对象,就构成了“原型链”。
  4. 属性查找机制:当你试图访问一个对象的属性时,JavaScript 引擎会:
    • 首先在该对象自身上查找。
    • 如果找不到,就沿着原型链向上,到它的原型对象上查找。
    • 如果还找不到,就继续向上,直到原型链的顶端 (null)。如果最终还是找不到,则返回 undefined

继承的本质就是利用原型链,让一个对象可以访问到另一个对象的属性和方法。


1. 原型链继承 (最基本的继承方式)

这是最直接、最核心的继承方式。

思路:让子构造函数的原型对象 (Child.prototype) 等于父构造函数的一个实例。

// 父构造函数
function Animal() {
this.species = '动物';
}
Animal.prototype.eat = function() {
console.log('正在吃东西');
};
// 子构造函数
function Cat() {
this.name = '';
}
// 核心:实现继承
// 创建一个 Animal 的实例,并将其赋值给 Cat 的原型
Cat.prototype = new Animal();
// 实例化子对象
const myCat = new Cat();
console.log(myCat.species); // 输出: '动物' (从原型链上继承的)
myCat.eat(); // 输出: '正在吃东西' (从原型链上继承的)
console.log(myCat.name); // 输出: '猫' (自身属性)

优点

  • 简单,易于理解,完美诠释了原型链的工作原理。

缺点

  1. 引用类型属性共享问题:如果父构造函数的属性包含引用类型(如数组或对象),那么所有子实例都会共享这个引用。一个子实例修改了这个属性,会影响到所有其他实例。

    function Animal() {
    this.features = ['', '四条腿'];
    }
    Cat.prototype = new Animal();
    const cat1 = new Cat();
    const cat2 = new Cat();
    cat1.features.push('会叫');
    console.log(cat2.features); // 输出: ['毛', '四条腿', '会叫'] -> cat2 被污染了!
  2. 无法向父构造函数传递参数:在创建子实例时,无法向父构造函数传递参数,因为父构造函数是在 Cat.prototype = new Animal() 时就被调用了。


2. 构造函数继承 (或称“借用构造函数”)

为了解决原型链继承的缺点,这种方式应运而生。

思路:在子构造函数内部,使用 .call().apply() 方法调用父构造函数,并将 this 指向子实例。

function Animal(name) {
this.name = name;
this.features = ['', '四条腿'];
}
function Cat(name) {
// 核心:借用父构造函数
Animal.call(this, name); // `this` 指向 new Cat() 创建的实例
}
const cat1 = new Cat('咪咪');
const cat2 = new Cat('小白');
cat1.features.push('会卖萌');
console.log(cat1.name); // '咪咪'
console.log(cat1.features); // ['毛', '四条腿', '会卖萌']
console.log(cat2.name); // '小白'
console.log(cat2.features); // ['毛', '四条腿'] (未被污染)

优点

  1. 完美解决了引用类型属性共享的问题。
  2. 可以在创建子实例时向父构造函数传递参数。

缺点

  • 无法继承父原型上的方法:父构造函数原型(Animal.prototype)上定义的方法对子实例是不可见的。

    Animal.prototype.eat = function() { console.log('正在吃'); };
    const myCat = new Cat('Tom');
    // myCat.eat(); // TypeError: myCat.eat is not a function

3. 组合继承 (最经典的继承方式)

这是将前面两种方法组合起来的产物,集两者之长,也是 JavaScript 中最常用的继承模式之一。

思路

  1. 使用构造函数继承来继承父类的实例属性(并解决引用共享问题)。
  2. 使用原型链继承来继承父类的原型方法。
function Animal(name) {
this.name = name;
this.features = ['', '四条腿'];
}
Animal.prototype.eat = function() {
console.log(this.name + ' 正在吃东西');
};
function Cat(name, color) {
// 1. 借用构造函数继承属性
Animal.call(this, name);
this.color = color;
}
// 2. 原型链继承方法
Cat.prototype = new Animal();
// 修正 constructor 指向(可选但推荐)
Cat.prototype.constructor = Cat;
// 在子原型上添加自己的方法
Cat.prototype.meow = function() {
console.log('喵喵~');
};
const myCat = new Cat('Tom', '灰色');
myCat.eat(); // 'Tom 正在吃东西'
myCat.meow(); // '喵喵~'
console.log(myCat.features); // ['毛', '四条腿']
const anotherCat = new Cat('Jerry', '白色');
myCat.features.push('抓老鼠');
console.log(anotherCat.features); // ['毛', '四条腿'] (互不影响)

优点

  • 结合了前两种方法的优点,既能继承方法,又能传递参数,还能解决引用共享问题。

缺点

  • 调用了两次父构造函数
    • 一次是在 Cat.prototype = new Animal() 时。
    • 另一次是在 Animal.call(this, name) 时。 这导致子类的原型上有一份多余的、无用的父类实例属性。

4. 寄生组合式继承 (更优化的组合继承)

这是对组合继承的优化,解决了调用两次父构造函数的问题。被认为是 ES6 class 出现之前最理想的继承方案

思路:不再通过调用父构造函数来设置子原型,而是创建一个干净的、只链接到父原型的对象。

function inheritPrototype(subType, superType) {
// 1. 创建一个继承自父原型的新对象
const prototype = Object.create(superType.prototype);
// 2. 修正新对象的 constructor 指向
prototype.constructor = subType;
// 3. 将这个新对象赋值给子原型
subType.prototype = prototype;
}
// ------ 父子构造函数定义同上 ------
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(this.name + ' 正在吃东西');
};
function Cat(name, color) {
Animal.call(this, name);
this.color = color;
}
// 核心:用寄生组合式继承来连接原型
inheritPrototype(Cat, Animal);
Cat.prototype.meow = function() {
console.log('喵喵~');
};
const myCat = new Cat('Tom', '灰色');
myCat.eat(); // 'Tom 正在吃东西'
myCat.meow(); // '喵喵~'

优点

  • 完美!只调用一次父构造函数,避免了在子原型上创建不必要的属性,同时保持了原型链的完整。

5. ES6 Class 继承 (extends)

ES6 引入了 class 关键字,它本质上是上面寄生组合式继承语法糖。它让继承的写法更像传统的面向对象语言,更清晰、更简洁。

// 父类
class Animal {
constructor(name) {
this.name = name;
this.features = ['', '四条腿'];
}
eat() {
console.log(`${this.name} 正在吃东西`);
}
}
// 子类使用 `extends` 关键字实现继承
class Cat extends Animal {
constructor(name, color) {
// 核心:调用 super() 来执行父类的构造函数
// 必须在 `this` 之前调用
super(name);
this.color = color;
}
meow() {
console.log('喵喵~');
}
}
const myCat = new Cat('Tom', '灰色');
myCat.eat(); // 'Tom 正在吃东西'
myCat.meow(); // '喵喵~'
const anotherCat = new Cat('Jerry', '白色');
myCat.features.push('抓老鼠');
console.log(anotherCat.features); // ['毛', '四条腿'] (互不影响)

super 关键字

  • constructor 中,super() 作为函数调用,代表父类的构造函数。
  • 在普通方法中,super 作为对象使用,指向父类的原型(super.eat() 就是调用 Animal.prototype.eat())。

优点

  • 语法清晰、简洁,符合传统面向对象开发者的习惯。
  • 内置实现了最理想的寄生组合式继承。

总结

继承方式解决了什么问题引入了什么新问题
原型链继承实现了基本的继承引用类型共享、无法传参
构造函数继承解决了引用共享和传参问题无法继承原型方法
组合继承解决了以上所有问题调用了两次父构造函数,有性能浪费
寄生组合式继承解决了调用两次的问题写法复杂,需要封装辅助函数
ES6 class 继承完美方案无明显缺点,是现代 JS 的标准实践

对于现代 JavaScript 开发,直接使用 ES6 的 classextends 是最推荐、最简单、最可靠的继承方式。但理解前面几种经典的原型继承模式,对于深入掌握 JavaScript 的底层原理至关重要。