出处:ObjectX 博客

原作者:ObjectX


通过 call 借用父类构造函数

原理

call 只会执行 Parent1 构造函数,并将 this 绑定到 Child1 实例上。因此,Parent1 构造函数内部的属性会被拷贝到 Child1 的实例中,但 Parent1.prototype 上的方法不会被继承

代码示例

function Parent1() {
  this.name = 'parent1';
}
 
function Child1() {
  Parent1.call(this);
  this.type = 'child1';
}
 
console.log(new Child1());

解释

call 只会执行 Parent1 构造函数,并将 this 绑定到 Child1 实例上。因此,Parent1 构造函数内部的属性会被拷贝到 Child1 的实例中,但 Parent1.prototype 上的方法不会被继承

缺点

由于 call 只是在 Child1 的构造函数内执行 Parent1,所以 Parent1.prototype 上的方法不会被继承

原型链继承

原理

通过 Child2.prototype = new Parent2(); 让 Child2 继承 Parent2,这样 Child2 的实例可以访问 Parent2.prototype 上的方法

代码示例

function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3];
}
 
Parent2.prototype.greet = function() {
    console.log('Hello from prototype');
};
 
function Child2() {
    this.type = 'child2';
}
Child2.prototype = new Parent2();
 
const c1 = new Child2();
const c2 = new Child2();
c1.play.push(4);
console.log(c1.play); // [1, 2, 3, 4]
console.log(c2.play); // [1, 2, 3, 4] (预期每个实例应有独立的 play 数组)

解释

由于 Child2.prototype = new Parent2();Child2 的实例可以访问 Parent2.prototype 上的方法,同时 Child2 的所有实例会共享 Parent2 构造函数内定义的引用类型属性。

缺点

  • Child2.prototype = new Parent2(); 会导致 Parent2 的所有实例共享 play 数组
  • 修改一个实例的 play 也会影响其他实例

组合继承

原理

  • 通过 Parent3.call(this) 继承实例属性(避免父类构造函数的引用类型属性共享)
  • 通过 Child3.prototype = new Parent3() 继承原型属性和方法

代码示例

function Parent3() {
    this.name = 'parent3';
    this.play = [1, 2, 3];
}
 
function Child3() {
    Parent3.call(this);
    this.type = 'child3';
}
Child3.prototype = new Parent3();
 
const s3 = new Child3();
const s4 = new Child3();
s3.play.push(4);
console.log(s3.play); // [1, 2, 3, 4]
console.log(s4.play); // [1, 2, 3]

解释

通过 Parent3.call(this); 继承实例属性,Child3.prototype = new Parent3(); 继承方法。这样每个实例都有自己的 play 数组,不会互相影响

缺点

  • Child3.prototype = new Parent3(); 这一行会导致 Parent3 的构造函数被执行两次

组合继承的优化 1

原理

直接让 Child4.prototype 指向 Parent4.prototype,这样减少了一次 Parent4 的构造函数调用

代码示例

function Parent4() {
    this.name = 'parent4';
    this.play = [1, 2, 3];
}
 
function Child4() {
    Parent4.call(this);
    this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
 
const s3 = new Child4();
const s4 = new Child4();
console.log(s3);

解释

这样做的好处是减少了一次 Parent4 的构造函数调用,但 Child4 的 constructor 会错误地指向 Parent4

缺点

  • Child4.prototype.constructor 指向 Parent4,而不是 Child4
  • 还有 child 原型对象是同一个,还是有引用问题
  • 在 JavaScript 中,继承机制是通过设置子类(子构造函数)的原型对象来实现的。当你执行 Child4.prototype = Parent4.prototype; 时,Child4.prototype 被赋值为 Parent4.prototype,这意味着 Child4 的原型对象指向 Parent4 的原型
  • 然而,在这种情况下,Child4.prototype.constructor 的值也会被改变。因为原型对象 Parent4.prototype 中有一个 constructor 属性,指向的是 Parent4 构造函数。由于 Child4.prototype 被指向 Parent4.prototype,所以 Child4.prototype.constructor 会继承 Parent4.prototype.constructor,而不再是 Child4 构造函数

最推荐的方式:优化 2

原理

使用 Object.create 创建 Child5.prototype,避免 Parent5 的构造函数被执行两次,并修正 constructor

代码示例

function Parent5() {
    this.name = 'parent5';
    this.play = [1, 2, 3];
}
 
function Child5() {
    Parent5.call(this);
    this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
 
const child = new Child5();
console.log(child);
console.log(child.constructor); // Child5

解释

  • Object.create(Parent5.prototype) 创建一个新的对象,并让其 __proto__ 指向 Parent5.prototype这里就实现了原型链
  • Child5.prototype.constructor = Child5; 修正 constructor 指向

优势

  • 解决了组合继承的两次调用父函数的问题
  • 继承了父类的原型方法
  • constructor 指向正确

结论

  1. call 继承:只能继承父类构造函数的属性,不能继承原型方法
  2. 原型链继承:原型上的方法能被继承,但会共享父类的引用类型属性
  3. 组合继承:通过 call 继承实例属性,通过 new Parent() 继承原型方法,但会导致父类构造函数执行两次
  4. 优化 1(原型赋值):直接将 Parent.prototype 赋值给 Child.prototype,但 constructor 会指向错误
  5. 优化 2(推荐):使用 Object.create 继承原型,避免两次调用构造函数,同时修正 constructor,是 ES5 继承的最佳方案