详解JavaScript中8 种不同的继承实现方式
作者:北辰alk
前言
在 JavaScript 中,继承是实现代码复用和构建复杂对象关系的重要机制。虽然 JavaScript 是一门基于原型的语言,不像传统面向对象语言那样有类的概念,但它提供了多种实现继承的方式。本文将详细介绍 JavaScript 中 8 种不同的继承实现方式,每种方式都会配有代码示例和详细解释,最后还会通过流程图比较各种继承方式的特点。
1. 原型链继承
原型链继承是 JavaScript 中最基本的继承方式,它利用原型让一个引用类型继承另一个引用类型的属性和方法。
function Parent() { this.name = 'Parent'; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child() { this.childName = 'Child'; } // 关键步骤:将Child的原型指向Parent的实例 Child.prototype = new Parent(); var child1 = new Child(); console.log(child1.getName()); // "Parent" console.log(child1.childName); // "Child" // 问题:引用类型的属性会被所有实例共享 child1.colors.push('black'); var child2 = new Child(); console.log(child2.colors); // ["red", "blue", "green", "black"]
特点:
- 简单易实现
- 父类新增原型方法/属性,子类都能访问到
- 无法实现多继承
- 来自原型对象的引用属性被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
2. 构造函数继承(经典继承)
通过在子类构造函数中调用父类构造函数实现继承。
function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child(name, age) { // 关键步骤:在子类构造函数中调用父类构造函数 Parent.call(this, name); this.age = age; } var child1 = new Child('Tom', 18); child1.colors.push('black'); console.log(child1.name); // "Tom" console.log(child1.age); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] var child2 = new Child('Jerry', 20); console.log(child2.colors); // ["red", "blue", "green"] // 问题:无法继承父类原型上的方法 console.log(child1.getName); // undefined
特点:
- 解决了原型链继承中引用类型共享的问题
- 可以在子类构造函数中向父类构造函数传参
- 可以实现多继承(call多个父类对象)
- 只能继承父类实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3. 组合继承(最常用)
组合继承结合了原型链继承和构造函数继承的优点。
function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child(name, age) { // 构造函数继承 - 第二次调用Parent() Parent.call(this, name); this.age = age; } // 原型链继承 - 第一次调用Parent() Child.prototype = new Parent(); // 修正constructor指向 Child.prototype.constructor = Child; Child.prototype.getAge = function() { return this.age; }; var child1 = new Child('Tom', 18); child1.colors.push('black'); console.log(child1.name); // "Tom" console.log(child1.age); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] console.log(child1.getName()); // "Tom" console.log(child1.getAge()); // 18 var child2 = new Child('Jerry', 20); console.log(child2.colors); // ["red", "blue", "green"] console.log(child2.getName()); // "Jerry" console.log(child2.getAge()); // 20
特点:
- 融合原型链继承和构造函数继承的优点
- 既是子类的实例,也是父类的实例
- 不存在引用属性共享问题
- 可传参
- 函数可复用
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
4. 原型式继承
借助原型可以基于已有对象创建新对象。
function object(o) { function F() {} F.prototype = o; return new F(); } var person = { name: 'Nicholas', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = object(person); anotherPerson.name = 'Greg'; anotherPerson.friends.push('Rob'); var yetAnotherPerson = object(person); yetAnotherPerson.name = 'Linda'; yetAnotherPerson.friends.push('Barbie'); console.log(person.friends); // ["Shelby", "Court", "Van", "Rob", "Barbie"]
ES5 规范化了原型式继承,新增了 Object.create()
方法:
var person = { name: 'Nicholas', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = Object.create(person); anotherPerson.name = 'Greg'; anotherPerson.friends.push('Rob'); var yetAnotherPerson = Object.create(person, { name: { value: 'Linda' } }); yetAnotherPerson.friends.push('Barbie'); console.log(person.friends); // ["Shelby", "Court", "Van", "Rob", "Barbie"]
特点:
- 不需要单独创建构造函数
- 本质是对给定对象执行浅复制
- 适用于不需要单独创建构造函数,但仍需要在对象间共享信息的场合
- 同原型链继承一样,包含引用类型的属性会被共享
5. 寄生式继承
创建一个仅用于封装继承过程的函数,在函数内部增强对象。
function createAnother(original) { var clone = Object.create(original); // 通过调用函数创建一个新对象 clone.sayHi = function() { // 以某种方式增强这个对象 console.log('Hi'); }; return clone; // 返回这个对象 } var person = { name: 'Nicholas', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // "Hi"
特点:
- 基于原型式继承
- 增强了对象
- 无法实现函数复用
- 同原型式继承一样,引用类型属性会被共享
6. 寄生组合式继承(最理想)
通过借用构造函数继承属性,通过原型链混成形式继承方法。
function inheritPrototype(child, parent) { var prototype = Object.create(parent.prototype); // 创建父类原型的副本 prototype.constructor = child; // 修正constructor指向 child.prototype = prototype; // 将副本赋值给子类原型 } function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child(name, age) { Parent.call(this, name); this.age = age; } // 关键步骤:避免调用Parent构造函数,直接使用父类原型 inheritPrototype(Child, Parent); Child.prototype.getAge = function() { return this.age; }; var child1 = new Child('Tom', 18); var child2 = new Child('Jerry', 20); console.log(child1.getName()); // "Tom" console.log(child1.getAge()); // 18 console.log(child2.getName()); // "Jerry" console.log(child2.getAge()); // 20
特点:
- 只调用一次父类构造函数
- 避免在子类原型上创建不必要的属性
- 原型链保持不变
- 能够正常使用 instanceof 和 isPrototypeOf
- 是引用类型最理想的继承方式
7. ES6 Class 继承
ES6 引入了 class 语法糖,使得继承更加清晰易读。
class Parent { constructor(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } getName() { return this.name; } } class Child extends Parent { constructor(name, age) { super(name); // 调用父类的constructor this.age = age; } getAge() { return this.age; } } const child1 = new Child('Tom', 18); child1.colors.push('black'); console.log(child1.getName()); // "Tom" console.log(child1.getAge()); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] const child2 = new Child('Jerry', 20); console.log(child2.colors); // ["red", "blue", "green"]
特点:
- 语法更加清晰易读
- 底层实现仍然是基于原型
- 通过 extends 实现继承
- 子类必须在 constructor 中调用 super(),否则新建实例时会报错
- ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this
8. 混入方式继承(多继承)
JavaScript 本身不支持多继承,但可以通过混入(Mixin)的方式实现类似功能。
function extend(target, ...sources) { sources.forEach(source => { for (let key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } // 支持Symbol属性 const symbols = Object.getOwnPropertySymbols(source); symbols.forEach(symbol => { target[symbol] = source[symbol]; }); }); return target; } const canEat = { eat() { console.log(`${this.name} is eating.`); } }; const canWalk = { walk() { console.log(`${this.name} is walking.`); } }; const canSwim = { swim() { console.log(`${this.name} is swimming.`); } }; function Person(name) { this.name = name; } // 将多个mixin混入Person的原型 extend(Person.prototype, canEat, canWalk); const person = new Person('John'); person.eat(); // "John is eating." person.walk(); // "John is walking." // person.swim(); // 报错,没有swim方法 function Fish(name) { this.name = name; } extend(Fish.prototype, canEat, canSwim); const fish = new Fish('Nemo'); fish.eat(); // "Nemo is eating." fish.swim(); // "Nemo is swimming." // fish.walk(); // 报错,没有walk方法
ES6 中可以使用 Object.assign 简化混入:
class Person { constructor(name) { this.name = name; } } Object.assign(Person.prototype, canEat, canWalk); class Fish { constructor(name) { this.name = name; } } Object.assign(Fish.prototype, canEat, canSwim);
特点:
- 可以实现类似多继承的功能
- 灵活性强,可以按需组合功能
- 不是真正的继承,而是属性拷贝
- 可能会导致命名冲突
- 无法使用 instanceof 检查混入的功能
继承方式比较流程图
总结
JavaScript 提供了多种实现继承的方式,每种方式都有其适用场景和优缺点:
- 原型链继承:简单但引用类型属性会被共享
- 构造函数继承:可解决引用共享问题但无法继承原型方法
- 组合继承:最常用的继承方式,但会调用两次父类构造函数
- 原型式继承:适用于基于已有对象创建新对象
- 寄生式继承:增强对象但无法函数复用
- 寄生组合继承:最理想的继承方式,高效且完整
- ES6 Class继承:语法糖,底层仍是原型继承
- 混入方式:实现类似多继承的功能
在实际开发中,ES6 的 class 语法是最推荐的方式,它语法简洁,易于理解,且底层实现高效。对于需要兼容旧浏览器的项目,可以使用寄生组合式继承作为替代方案。
理解这些继承方式的原理和区别,有助于我们在不同场景下选择最合适的实现方式,写出更优雅、高效的 JavaScript 代码。
以上就是详解JavaScript中8 种不同的继承实现方式的详细内容,更多关于JavaScript实现继承的资料请关注脚本之家其它相关文章!