JS-继承的演进之路(从原型链到组合继承)

0 阅读3分钟

前言

JavaScript 的继承不像类类型语言(如 Java)那样直观。它是通过原型链机制实现的。理解各种继承方式的演进过程,不仅能帮你应对面试,更能让你深入理解 JS 的底层设计。

一、 原型链继承(Prototype Chain Inheritance)

这是最基本的继承方式。

  • 实现Child.prototype = new Father();

优点

  • 父类构造函数内部的属性、方法以及父类原型上的方法,子类都能访问。

缺点(致命伤)

  1. 引用属性共享:父类中如果有引用类型属性(如 Array),一个子类修改了它,所有子类都会跟着变。
  2. 无法传参:创建子类实例时,无法向父类构造函数传递参数。

二、 借用构造函数继承(Constructor Stealing)

为了解决传参和引用共享问题,出现了这种方式。

  • 实现:在子类构造函数中使用 Father.call(this, ...args)

优点

  1. 独立性:每个子类实例都有自己的属性副本,互不干扰。
  2. 可传参:可以向父类构造函数传递参数。

缺点

  • 方法无法复用:只能继承父类构造函数内的属性,无法继承父类原型(prototype)上的方法

三、 组合继承(Combination Inheritance)⭐

这是最常用的继承方式,它结合了前两者的优点。

  • 核心思想:先将子类构造函数的protype指向为父类构造函数的一个实例,大致步骤和原型链继承一样,只是多了在子类构造函数里面调用父类构造函数的过程,可总结为用构造函数继承属性,用原型链继承方法。
  • 深度分析:组合继承子类的实例属性定义在构造函数中,而方法则定义在构造函数的新原型中,同时将新原型的constructor指向构造函数(为什么需要将construct指向构造函数,因为如果不指向构造函数的话,通过new类构造函数 构造出来这个实例对象是没有constructor属性的,我们要找子类constructor时,会通过原型链直接找到父类的constructor属性,而父类的constructor属性是指向父类构造函数的)
  • 原理如下图所示:

image.png

代码实战

function Father(name) {
    this.name = name;
    this.nums = [1, 2, 3, 4];
}
Father.prototype.sayName = function() {
    console.log(this.name);
}

function Child(name, age) {
    // 1. 继承属性(构造函数继承)
    Father.call(this, name); 
    this.age = age;
}

// 2. 继承方法(原型链继承)
Child.prototype = new Father();
// 3. 修复 constructor 指向
Child.prototype.constructor = Child; 

Child.prototype.sayAge = function() {
    console.log(this.age);
}

const child1 = new Child('ouyang', 16);
child1.nums.push(5); 
console.log(child1.nums); // [1, 2, 3, 4, 5]

const child2 = new Child('hudie', 18); 
console.log(child2.nums); // [1, 2, 3, 4] (互不影响!)

五、 面试模拟题

Q1:为什么 JavaScript 的继承要设计原型链?

参考回答: 原型链实现了属性和方法的共享。如果不使用原型链,每个实例都要单独存储一份方法,会造成极大的内存浪费。原型链允许所有实例共享同一套方法(存储在 prototype 中),同时通过构造函数保证每个实例有独立的属性。

Q2:new Father()Object.create(Father.prototype) 在继承时有什么区别?

参考回答:

  1. new Father() 会执行 Father 构造函数内部的代码,并将属性挂载到原型上。
  2. Object.create(Father.prototype) 创建一个新对象,其原型指向 Father.prototype,但不会执行 Father 的构造函数。这避免了子类原型上出现不必要的父类实例属性,性能更优。

Q3:ES6 的 extends 继承和组合继承有什么区别?

参考回答:

  1. 底层机制:ES5 继承是先创建子类实例 this,再将父类属性添加到 this 上;ES6 继承是先创建父类实例 this(通过 super()),再用子类构造函数修改 this
  2. 静态方法继承:ES6 的 extends 可以继承父类的静态方法(static),而 ES5 的组合继承需要手动处理才能实现。