JS 继承:传统方式大盘点,原理解析与优劣
2023-09-20 20:03:39
在前端开发中,继承是构建复杂应用的关键概念。相对于 Java 等语言的严格继承,JavaScript 提供了更加灵活多样的继承机制。本文将深入解析传统的 JS 继承方式,包括原型链、构造函数、寄生组合继承和组合继承,揭示它们的原理、优缺点,助你掌握前端面试中必备的继承知识。
原型链继承
原型链继承是 JavaScript 中最基本的继承方式。每个对象都拥有一个称为 原型对象 的内部属性,该对象引用了创建该对象的构造函数的原型属性。通过访问原型对象,子对象可以继承父对象的属性和方法。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I am ${this.name}`);
};
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
原理: Student 通过调用 Person 构造函数来继承其属性和方法。同时,Student 的原型对象被设置为 Person 的原型对象,实现了对 Person 实例方法的访问。
优点:
- 简单易懂,实现方便。
- 由于子对象的原型对象指向父对象的原型对象,因此可以实现动态继承,即父对象新增的方法可以被子对象继承。
缺点:
- 无法继承父对象的构造函数和私有方法。
- 由于所有子对象共享同一个原型对象,因此修改原型对象会影响所有子对象。
构造函数继承
构造函数继承是一种直接使用父对象的构造函数来创建子对象的方式。子对象通过继承父对象的原型对象,获取父对象的方法和属性。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I am ${this.name}`);
};
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
原理: Student 构造函数直接调用 Person 构造函数,并传递必要的参数。这样,Student 对象可以继承 Person 的属性和方法。
优点:
- 可以继承父对象的构造函数和私有方法。
- 避免了修改原型对象影响所有子对象的问题。
缺点:
- 实现较为复杂,需要手动调用父对象的构造函数。
- 子对象无法访问父对象的原型对象上的属性和方法,即不支持动态继承。
寄生组合继承
寄生组合继承结合了原型链继承和构造函数继承的优点。它通过创建一个临时对象来继承父对象的属性和方法,然后再将临时对象的属性和方法复制到子对象中。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I am ${this.name}`);
};
function Student(name, grade) {
// 创建一个临时对象,继承 Person 的属性和方法
const temp = new Person(name);
temp.grade = grade;
// 将 temp 的属性和方法复制到 Student 实例中
for (const key in temp) {
if (temp.hasOwnProperty(key)) {
this[key] = temp[key];
}
}
}
原理: 首先创建一个临时对象,继承 Person 的属性和方法。然后将临时对象的属性和方法逐个复制到 Student 实例中,实现对 Person 实例方法的访问。
优点:
- 继承父对象的构造函数和私有方法。
- 支持动态继承。
缺点:
- 实现较为复杂,需要手动复制属性和方法。
组合继承
组合继承结合了构造函数继承和原型链继承的优点。它通过调用父对象的构造函数来继承父对象的属性和方法,并通过修改子对象的原型对象来实现对父对象原型对象上的属性和方法的访问。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I am ${this.name}`);
};
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
// 将 Person.prototype 复制到 Student.prototype 上
Student.prototype = Object.create(Person.prototype);
// 修复 Student.prototype.constructor 指向 Student 构造函数
Student.prototype.constructor = Student;
原理: 首先调用父对象的构造函数,继承父对象的属性和方法。然后将父对象的原型对象复制到子对象的原型对象上。最后,修改子对象的原型对象的 constructor 属性,指向子对象的构造函数,避免覆盖父对象的构造函数。
优点:
- 继承父对象的构造函数和私有方法。
- 支持动态继承。
- 实现相对简单。
缺点:
- 存在性能开销,因为需要多次调用构造函数和复制原型对象。
ES6 Class
ES6 中引入的 Class 继承机制是对传统继承方式的改进。它语法更加简洁,并且解决了传统继承方式的一些问题。
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, I am ${this.name}`);
}
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
}
原理: ES6 Class 本质上是一种语法糖,它底层仍然基于原型链继承机制。Class 通过 extends 来实现继承,super 关键字用于调用父类的构造函数。
优点:
- 语法简洁,易于理解。
- 支持动态继承。
- 避免了传统继承方式中的一些问题,如修改原型对象影响所有子对象。
缺点:
- 性能开销略大于传统继承方式。
- 仍然无法继承父对象的私有方法。
总结
JavaScript 的传统继承方式可谓百花齐放,每种方式都具有其自身的特点和适用场景。通过深入理解每种方式的原理、优缺点,我们可以灵活选择合适的继承机制,构建更加 robust 和可维护的 JavaScript 应用。