ES5实现继承的多种方式详解
深入讲解ES5中实现继承的6种经典方式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承和寄生组合式继承
问题
在ES6的class语法出现之前,JavaScript通过原型链和构造函数来实现面向对象编程中的继承特性。本文将详细介绍ES5中实现继承的多种方式,分析每种方式的优缺点,帮助理解JavaScript继承的本质。
解答
1. 原型链继承
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child(age) {
this.age = age;
}
// 实现继承:将父类的实例作为子类的原型
Child.prototype = new Parent('parent');
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
优点: 简单易实现,可以继承父类原型上的方法
缺点:
- 所有子类实例共享父类引用类型属性
- 创建子类实例时无法向父类构造函数传参
2. 构造函数继承(借用构造函数)
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child(name, age) {
// 调用父类构造函数
Parent.call(this, name);
this.age = age;
}
Child.prototype.sayAge = function() {
console.log(this.age);
};
优点:
- 避免了引用类型属性被所有实例共享
- 可以在子类构造函数中向父类传参
缺点:
- 无法继承父类原型上的方法
- 每次创建实例都会创建一遍方法
3. 组合继承(原型链 + 构造函数)
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child(name, age) {
// 第二次调用Parent():继承实例属性
Parent.call(this, name);
this.age = age;
}
// 第一次调用Parent():继承原型方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
优点:
- 融合了原型链继承和构造函数继承的优点
- 既可以继承原型上的方法,又可以继承实例属性
- 可以向父类传参,且不会共享引用属性
缺点: 调用了两次父类构造函数,造成性能浪费
4. 原型式继承
// 函数
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// 父对象
var parent = {
name: 'parent',
colors: ['red', 'blue', 'green'],
sayName: function() {
console.log(this.name);
}
};
// 创建子对象
var child1 = object(parent);
child1.name = 'child1';
var child2 = object(parent);
child2.name = 'child2';
说明: ES5的Object.create()方法就是原型式继承的实现
优点: 不需要创建构造函数,直接基于已有对象创建新对象
缺点: 引用类型属性会被所有实例共享
5. 寄生式继承
function createChild(original) {
// 通过原型式继承创建新对象
var clone = Object.create(original);
// 增强对象,添加新方法
clone.sayHi = function() {
console.log('Hi');
};
return clone;
}
// 父对象
var parent = {
name: 'parent',
colors: ['red', 'blue', 'green']
};
// 创建子对象
var child = createChild(parent);
优点: 在原型式继承基础上增强对象
缺点:
- 引用类型属性共享问题
- 每次创建对象都会创建一遍方法,效率较低
6. 寄生组合式继承(最优方案)
// 函数:实现原型继承
function inheritPrototype(child, parent) {
// 创建父类原型的副本
var prototype = Object.create(parent.prototype);
// 修正constructor指向
prototype.constructor = child;
// 将副本赋值给子类原型
child.prototype = prototype;
}
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child(name, age) {
// 继承实例属性
Parent.call(this, name);
this.age = age;
}
// 继承原型方法
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() {
console.log(this.age);
};
优点:
- 只调用一次父类构造函数,避免了组合继承的性能问题
- 保持了原型链的完整性
- 能够正常使用instanceof和isPrototypeOf()
这是ES5中实现继承的最优方案,也是ES6 class继承的底层实现原理
使用示例
// 使用寄生组合式继承
function inheritPrototype(child, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 定义父类
function Animal(name) {
this.name = name;
this.energy = 100;
}
Animal.prototype.eat = function() {
console.log(this.name + ' is eating');
this.energy += 10;
};
Animal.prototype.sleep = function() {
console.log(this.name + ' is sleeping');
this.energy += 20;
};
// 定义子类
function Dog(name, breed) {
Animal.call(this, name); // 继承属性
this.breed = breed;
}
inheritPrototype(Dog, Animal); // 继承方法
Dog.prototype.bark = function() {
console.log(this.name + ' is barking: Woof!');
this.energy -= 5;
};
// 创建实例
var dog1 = new Dog('旺财', '哈士奇');
var dog2 = new Dog('小黑', '拉布拉多');
dog1.eat(); // 旺财 is eating
dog1.bark(); // 旺财 is barking: Woof!
console.log(dog1.energy); // 105
dog2.sleep(); // 小黑 is sleeping
console.log(dog2.energy); // 120
// 验证继承关系
console.log(dog1 instanceof Dog); // true
console.log(dog1 instanceof Animal); // true
console.log(Dog.prototype.isPrototypeOf(dog1)); // true
console.log(Animal.prototype.isPrototypeOf(dog1)); // true
关键点
-
原型链是JavaScript继承的机制:通过
__proto__或prototype建立对象之间的继承关系 -
构造函数继承解决属性继承问题:使用
call()或apply()在子类构造函数中调用父类构造函数 -
原型继承解决方法继承问题:通过设置
Child.prototype来继承父类原型上的方法 -
寄生组合式继承是最优方案:结合构造函数继承和原型继承的优点,避免了重复调用父类构造函数
-
必须修正constructor指向:继承后需要将
Child.prototype.constructor指向Child本身 -
Object.create()的作用:创建一个新对象,使用现有对象作为新对象的
__proto__,避免直接使用new Parent() -
理解继承的本质:JavaScript的继承是通过原型链实现的委托机制,而不是传统面向对象语言的类继承
-
ES6 class的本质:ES6的class语法只是语法糖,底层仍然是基于原型链和构造函数实现的寄生组合式继承
目录