JavaScript 主要使用基于原型链的继承(Prototypal Inheritance),这与传统的基于类的继承(Class-based Inheritance)不同。ES6 引入了 class 语法糖,但其底层机制仍然是基于原型的。
-
核心概念:
- 原型对象: 每个 JavaScript 对象(除了
null)在创建时都会关联到另一个对象,这个对象就是它的原型对象。对象从其原型对象继承属性和方法。 - 原型链: 当试图访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 引擎会沿着对象的
__proto__(非标准,但浏览器实现) 或其构造函数的prototype属性(标准方式Object.getPrototypeOf(obj))去查找它的原型对象。如果原型对象上也没有,就继续查找原型对象的原型对象,如此层层向上,直到找到该属性或到达原型链的尽头(null)。这种链式结构就是原型链。 - 构造函数: 使用
new关键字调用的函数。构造函数通常用于创建特定类型的对象实例。构造函数的prototype属性指向一个对象,该对象将成为由该构造函数创建的所有实例的原型对象 (instance.__proto__ === Constructor.prototype)。
- 原型对象: 每个 JavaScript 对象(除了
-
ES5 及之前实现继承的主要方式:
-
原型链继承: 让子类的原型对象 (
SubType.prototype) 等于父类的实例 (new SuperType())。核心代码: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; }; const instance = new SubType(); console.log(instance.getSuperValue()); // true (从原型链找到)-
缺点:
- 引用类型共享问题: 如果父类实例属性包含引用类型值(如数组、对象),这些引用会被所有子类实例共享。修改一个实例的引用类型属性会影响所有其他实例。
- 不能向父类构造函数传参: 在创建子类实例时,无法向父类构造函数传递参数 (
new SuperType()发生在定义时,而不是实例化SubType时)。
-
-
借用构造函数继承: 在子类构造函数内部调用父类构造函数 (
SuperType.call(this, args))。核心代码:function SuperType(name) { this.name = name; this.colors = ['red', 'blue']; } function SubType(name, age) { SuperType.call(this, name); // "借用"父类构造函数初始化实例属性 this.age = age; } const instance1 = new SubType('Alice', 25); instance1.colors.push('green'); // 只影响 instance1 const instance2 = new SubType('Bob', 30); console.log(instance2.colors); // ['red', 'blue'] (不共享)- 优点: 解决了引用类型共享问题,可以在子类构造函数中向父类构造函数传递参数。
- 缺点: 方法都在构造函数中定义,无法实现函数复用(每个实例都创建自己的方法副本)。父类原型上的方法对子类实例不可见(因为没涉及原型链)。
-
组合继承: 结合原型链继承和借用构造函数继承。最常用方式。
function SuperType(name) { this.name = name; this.colors = ['red', 'blue']; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { // 1. 借用构造函数继承属性 SuperType.call(this, name); // 第二次调用 SuperType this.age = age; } // 2. 原型链继承方法 SubType.prototype = new SuperType(); // 第一次调用 SuperType SubType.prototype.constructor = SubType; // 修复 constructor 指向 SubType.prototype.sayAge = function() { console.log(this.age); }; const instance1 = new SubType('Alice', 25); instance1.colors.push('green'); const instance2 = new SubType('Bob', 30); console.log(instance2.colors); // ['red', 'blue'] (属性不共享) instance1.sayName(); // 'Alice' (方法复用)- 优点: 解决了引用类型共享问题,实现了方法复用,可以在子类构造函数中向父类构造函数传递参数。
- 缺点: 父类构造函数被调用了两次(一次创建原型
new SuperType(),一次在子类构造函数中SuperType.call(this))。导致子类原型 (SubType.prototype) 上有一份父类实例属性,子类实例 (instance) 上也有一份父类实例属性(通过call得来),造成了属性冗余。
-
原型式继承: 基于已有对象创建新对象,无需明确定义构造函数。
Object.create()是标准实现。const person = { name: 'Default', friends: ['Shelby', 'Court'] }; const person1 = Object.create(person); person1.name = 'Alice'; person1.friends.push('Van'); const person2 = Object.create(person); person2.name = 'Bob'; console.log(person2.friends); // ['Shelby', 'Court', 'Van'] (共享了 friends)- 适用于从简单对象继承的场景。
- 缺点: 和原型链继承一样,引用类型值会被共享。
-
寄生式继承: 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象(添加方法),最后返回这个对象。常与原型式继承结合。
function createAnother(original) { const clone = Object.create(original); // 原型式继承 clone.sayHi = function() { console.log('Hi!'); }; // 增强对象 return clone; }- 缺点: 方法无法复用(每个对象都有自己的方法副本)。
-
寄生组合式继承: 目前公认最理想的继承范式。解决了组合继承调用两次父类构造函数的问题。核心是使用
Object.create()来设置子类的原型,避免调用父类构造函数。function inheritPrototype(subType, superType) { const prototype = Object.create(superType.prototype); // 创建父类原型的副本 prototype.constructor = subType; // 修复 constructor subType.prototype = prototype; // 将副本赋值给子类原型 } function SuperType(name) { this.name = name; this.colors = ['red', 'blue']; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { SuperType.call(this, name); // 只调用一次父类构造函数 (继承属性) this.age = age; } inheritPrototype(SubType, SuperType); // 继承方法 (不调用父类构造函数) SubType.prototype.sayAge = function() { console.log(this.age); };- 优点: 只调用一次父类构造函数,效率高。避免了在子类原型上创建不必要的、冗余的属性。原型链保持不变。
-
-
ES6
class继承:-
ES6 引入了
class、constructor、extends、super、static等关键字,提供了一种更接近传统面向对象语言的语法来创建类和实现继承。 -
底层原理:
class语法本质上是 ES5 构造函数和原型继承的语法糖。extends和super关键字实现了类似寄生组合式继承的效果。 -
代码示例:
class SuperType { constructor(name) { this.name = name; this.colors = ['red', 'blue']; } sayName() { console.log(this.name); } } class SubType extends SuperType { constructor(name, age) { super(name); // 相当于 SuperType.call(this, name) this.age = age; } sayAge() { console.log(this.age); } } const instance = new SubType('Alice', 25); instance.sayName(); // 'Alice' instance.sayAge(); // 25 -
优点: 语法简洁清晰,更符合直觉。内置了正确的原型链设置(类似于寄生组合继承)。必须使用
new调用。类内部定义的方法默认在原型上(可复用)。支持静态方法和super关键字调用父类方法。
-
-
面试回答要点:
- 核心机制: JavaScript 使用基于原型链的继承。
- 关键概念: 原型对象、原型链 (
__proto__/[[Prototype]]->Object.getPrototypeOf())、构造函数 (prototype属性)。 - ES5 主要方式: 重点掌握组合继承(原理、优缺点)和寄生组合式继承(最优解原理)。
- ES6
class: 本质是语法糖,底层基于原型链。理解extends和super的作用(super在构造函数中调用父类构造,在方法中调用父类方法)。明确class解决了 ES5 继承中的哪些痛点(语法、冗余属性、必须用new)。 - 对比: 能对比不同继承方式的优缺点(特别是引用类型共享、构造函数调用次数、方法复用性)。
- 总结: 继承在JavaScript 是基于原型链的语言。理解原型对象、原型链、构造函数的关系是基础。ES5 有多种实现方式,组合继承常用但非最优,寄生组合式继承是最佳实践。ES6
class提供了更优雅的语法糖,其底层机制仍然是原型链,使用extends和super简化了继承实现。