引言
在JavaScript中,实现继承的方式主要有以下几种:原型链继承、构造函数继承、组合继承(即使用原型链和构造函数)、原型式继承以及寄生式继承。
ES5的继承方式
1. 原型链继承
通过让子类的原型对象指向父类的一个实例对象来实现继承。
function Parent() {
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayHi = function() {
console.log('Hi');
};
function Child() {}
// 继承
Child.prototype = new Parent();
const child = new Child();
child.colors.push('yellow');
child.sayHi() // Hi
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']
const child2 = new Child();
console.log(child2.colors); // ['red', 'blue', 'green', 'yellow']
console.log(child.colors === child2.colors); // true
这种方式虽然简单易懂,但是有两个明显的缺点。一是父类的引用属性会被所有的实例共享,二是创建子类实例时不能向父类构造函数传参。
2. 构造函数继承
通过在子类构造函数内部调用父类构造函数来实现继承。
function Parent() {
this.colors = ['red', 'blue', 'green'];
this.sayHi = function() {
console.log('Hi');
};
}
function Child() {
Parent.call(this);
}
const child = new Child();
child.colors.push('yellow');
child.sayHi() // Hi
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']
const child2 = new Child();
child2.sayHi() // Hi
console.log(child2.colors); // ['red', 'blue', 'green']
console.log(child.sayHi === child2.sayHi); // false
这种继承方式解决了“原型链继承”主要存在的两个问题。但也有个明显的缺点,方法都在构造函数中定义,每个实例都有自己的方法副本,导致函数复用性差。
3. 组合继承
结合原型链继承和构造函数继承的优点,同时避免它们的缺点。
function Parent() {
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayHi = function() {
console.log('Hi');
};
function Child() {
// 第一次调用 Parent 构造函数
Parent.call(this);
}
// 第二次间接调用 Parent 构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修正 prototype.constructor 的指向
const child = new Child();
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']
解决了原型链继承和构造函数继承的缺点。实例可以访问父类的方法,而且不会重复创建这些方法。但是这也有个缺点,调用了两次父类构造函数,一次在子类构造函数中,一次在设置原型时。
两次调用父类构造函数分析
在上述代码中,Parent.call(this); 直接调用了父类构造函数 Parent,用于初始化子类 Child 的实例属性。
而 Child.prototype = Object.create(Parent.prototype); 这行代码则是通过 Object.create() 方法间接调用了父类构造函数。这是因为 Object.create() 方法创建了一个新对象,该对象的原型是传入的对象,即 Parent.prototype。这意味着创建了一个新的 Parent 类型的实例,并将其设置为 Child 类的原型。
所以,在 Child.prototype = Object.create(Parent.prototype); 这行代码中,虽然没有直接调用 Parent 构造函数,但实际上创建了一个新的 Parent 类型的实例。这就是为什么我们说这里有两次调用的原因。
4. 原型式继承
使用一个已有的实例作为新对象的原型,从而实现继承。
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
const parent = {
colors: ['red', 'blue', 'green'],
sayHi: function() {
console.log('Hi');
}
};
const child = object(parent);
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']
这种继承方式没有调用构造函数的过程,所以没有语义化的构造函数与原型链关系。
5. 寄生式继承
先通过原型式继承创建一个新对象,然后增强这个对象。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function createObj(o) {
const clone = object(o);
clone.sayHello = function() {
console.log('Hello');
};
return clone;
}
const parent = {
colors: ['red', 'blue', 'green'],
sayHi: function() {
console.log('Hi');
}
};
const child = createObj(parent);
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']
这种继承方式和原型式继承基本一致,所以缺点也与原型式继承一样,但多了一层处理,可以添加额外的功能。
ES6的继承方式
ES6 引入了 class 和 extends 关键字来简化面向对象编程,使得继承更加直观和易于理解。
class Parent {
constructor() {
this.colors = ['red', 'blue', 'green'];
}
sayHi() {
console.log('Hi');
}
}
class Child extends Parent {
constructor() {
super(); // 调用父类的构造函数
this.newColor = 'yellow';
}
sayHello() {
console.log('Hello');
}
}
const child = new Child();
child.colors.push('yellow');
console.log(child.colors); // ['red', 'blue', 'green', 'yellow']
child.sayHi(); // Hi
child.sayHello(); // Hello
在现代浏览器和Node.js环境中,由于 ES6 的 extends 提供了更好的语法支持和更高的可读性,通常推荐优先使用 ES6 的继承方式。它不仅简化了继承机制,还提供了更多面向对象编程的特性,如静态方法、私有字段等,这些都是传统继承方式所不具备的。