JavaScript 实现继承主要依赖于原型链。虽然 ES5 和 ES6 在语法上有所不同,但底层机制都是基于原型。
ES5 中的继承
在 ES5 中,我们通常通过构造函数和原型链来模拟实现类的继承。核心思想是让子类的原型对象指向父类的实例,从而能够访问父类原型上的方法和属性。
以下是一种常见的实现方式,被称为“寄生组合式继承”,因为它结合了多种模式的优点,避免了构造函数继承和原型链继承的缺点:
JavaScript
// 父类构造函数
function Parent(name) {
this.name = name;
this.hobbies = ['reading', 'swimming']; // 引用类型属性
}
// 父类原型上的方法
Parent.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
// 子类构造函数
function Child(name, age) {
// 1. 借用构造函数继承属性
// 调用 Parent 构造函数,并改变其 this 指向 Child 的实例
Parent.call(this, name);
this.age = age;
}
// 2. 继承原型方法(关键步骤)
// 创建一个父类原型的副本作为子类的原型,避免直接赋值导致子类原型修改影响父类原型
Child.prototype = Object.create(Parent.prototype);
// 3. 修正子类构造函数的指向
// Object.create() 会创建一个新对象,它的 constructor 属性会指向 Object,
// 所以需要手动将其指回 Child 构造函数
Child.prototype.constructor = Child;
// 子类特有的方法
Child.prototype.sayAge = function() {
console.log('I am ' + this.age + ' years old.');
};
// 示例
const parentInstance = new Parent('Dad');
parentInstance.sayHello(); // Hello, my name is Dad
console.log(parentInstance.hobbies); // ["reading", "swimming"]
const childInstance1 = new Child('Alice', 10);
childInstance1.sayHello(); // Hello, my name is Alice
childInstance1.sayAge(); // I am 10 years old.
console.log(childInstance1.hobbies); // ["reading", "swimming"]
const childInstance2 = new Child('Bob', 8);
childInstance2.hobbies.push('drawing'); // 修改子实例的引用类型属性
console.log(childInstance1.hobbies); // ["reading", "swimming"]
console.log(childInstance2.hobbies); // ["reading", "swimming", "drawing"]
解析:
-
Parent.call(this, name);:这行代码实现了属性的继承。它在Child构造函数内部调用Parent构造函数,并强制Parent构造函数中的this指向Child的新实例。这样,Parent中定义的实例属性(如name和hobbies)就会被复制到Child的实例上。这解决了原型链继承中引用类型属性被所有实例共享的问题。 -
Child.prototype = Object.create(Parent.prototype);:这是实现方法继承的关键。Object.create()方法会创建一个新对象,并以指定对象(这里是Parent.prototype)作为其原型。这意味着Child.prototype现在是一个新对象,它的原型是Parent.prototype。当查找Child实例上的方法时,如果实例本身没有,就会沿着原型链向上查找到Child.prototype,如果还没有,再继续查找到Parent.prototype,从而实现了方法的继承。 -
Child.prototype.constructor = Child;:由于Object.create()创建的新对象的constructor属性会指向Object,我们需要手动将其重置为Child,以保持Child实例的constructor属性正确指向Child构造函数。
ES6 中的继承
ES6 引入了 class 语法,这使得 JavaScript 的面向对象编程更接近于传统面向对象语言(如 Java、C++)的风格。然而,ES6 的 class 只是语法糖,其底层仍然是基于 ES5 中的原型继承机制。
ES6 通过 extends 关键字和 super 关键字实现了更简洁、更直观的继承。
JavaScript
// 父类
class Parent {
constructor(name) {
this.name = name;
this.hobbies = ['reading', 'swimming']; // 引用类型属性
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
// 在子类构造函数中,如果使用了 this,则必须先调用 super()
// super() 调用父类的构造函数,并传入相应的参数
super(name); // 相当于 Parent.call(this, name)
this.age = age;
}
sayAge() {
console.log(`I am ${this.age} years old.`);
}
// 也可以重写父类方法
sayHello() {
super.sayHello(); // 调用父类的 sayHello 方法
console.log(`... and I am a child.`);
}
}
// 示例
const parentInstance = new Parent('Dad');
parentInstance.sayHello(); // Hello, my name is Dad
console.log(parentInstance.hobbies); // ["reading", "swimming"]
const childInstance1 = new Child('Alice', 10);
childInstance1.sayHello(); // Hello, my name is Alice\n... and I am a child.
childInstance1.sayAge(); // I am 10 years old.
console.log(childInstance1.hobbies); // ["reading", "swimming"]
const childInstance2 = new Child('Bob', 8);
childInstance2.hobbies.push('drawing'); // 修改子实例的引用类型属性
console.log(childInstance1.hobbies); // ["reading", "swimming"]
console.log(childInstance2.hobbies); // ["reading", "swimming", "drawing"]
解析:
-
class Child extends Parent:这是声明继承的语法。它明确表示Child类继承自Parent类。这在底层做了很多工作,包括设置Child.prototype的原型为Parent.prototype,以及处理constructor的正确指向。 -
super(name);:在子类的constructor中,必须在this关键字被使用之前调用super()。super()扮演着两个关键角色:-
作为函数调用时(
super(...)),它调用父类的构造函数,并绑定this到子类实例。这确保了父类实例的属性被正确地初始化到子类实例上。 -
作为对象使用时(
super.method()),它允许你访问父类原型上的方法。这在重写父类方法并希望在重写方法中调用父类版本时非常有用。
-
对比与思考
从实现方式上看:
-
ES5:通过手动操作原型对象和借用构造函数来模拟实现继承,代码相对复杂和冗长,但能帮助我们深入理解 JavaScript 的原型机制。
-
ES6:提供了更简洁、更符合直觉的
class语法和extends/super关键字,大大简化了继承的实现。它隐藏了原型链操作的复杂性,让开发者能更专注于业务逻辑。
从底层原理上看:
无论 ES5 还是 ES6,JavaScript 的继承都是基于原型链。一个对象的原型([[Prototype]],通常通过 __proto__ 访问,或者更推荐的 Object.getPrototypeOf())指向另一个对象,当访问一个对象的属性或方法时,如果该对象本身没有,就会沿着原型链向上查找,直到找到该属性或方法,或者原型链的终点(null)。
我的见解:
虽然 ES5 的实现方式能让我们更清晰地看到 JavaScript 继承的本质(原型链),但它确实较为繁琐且容易出错。ES6 的 class 语法糖极大地提升了代码的可读性和可维护性,使得 JavaScript 的面向对象编程体验更佳,也更符合其他主流面向对象语言的习惯。因此,在现代 JavaScript 开发中,我们理所当然地会优先选择使用 ES6 的 class 语法来实现继承。它在不改变底层原型机制的前提下,为开发者提供了更高级别的抽象,使得代码结构更加清晰,意图表达更加明确。
如果想深入了解更多关于原型链的细节,或者对其他 JavaScript 编程概念感兴趣,我们可以继续探讨。