很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。因为JavaScript函数没有签名,所以实现继承时JavaScript唯一支持的继承方式,而这主要是通过原型链实现的。
1. 原型链继承
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针。
原型链继承的核心:将父类的实例作为子类的原型。
// 父类
function Animal() {
this.superValue = 'animal';
this.colors = ['red', 'green', 'black']
}
Animal.prototype.getSuperValue = function() {
return this.superValue;
}
// 子类
function Cat() {
this.subValue = 'cat'
}
// 关键: 创建Animal实例,并把该实例赋值给Cat.prototype
// 相当于Cat.prototype__proto__ = Animal.prototype
Cat.prototype = new Animal();
Cat.prototype.getSubValue = function() {
return this.subValue;
}
const instance1 = new Cat();
console.log(instance1.getSuperValue()); // animal
console.log(instance1.colors); // [ 'red', 'green', 'black' ]
instance1.colors.push('pink');
console.log(instance1.colors); // [ 'red', 'green', 'black', 'pink' ]
const insatnce2 = new Cat();
console.log(instance1.colors); // [ 'red', 'green', 'black', 'pink' ]
// 子类原型上的constructor属性被重写了,不再指向Cat
console.log(Cat.prototype.constructor === Cat); // false
console.log(Cat.prototype.constructor === Animal); // true
原型继承方案的特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/属性,子类都能访问到
- 简单,易于实现
原型链继承方案的缺点:
- 来自原型对象的引用属性会被所有实例共享,如果原型对象的属性中有引用类型,在一个实例中修改引用类型的值,在其他实例中也会被修改。
- 子类型的原型上的construtor属性被重写了
- 给子类原型添加属性和方法必须要早替换原型之后
- 创建子类实例时无法向父类构造函数传参
2. 借用构造函数继承
借用构造函数继承的基本思路:在子类构造函数的内部调用父类构造函数。
本质上是使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)。
function Animal() {
this.colors = ['red', 'green', 'blue']
}
function Cat() {
// 继承Animal
Animal.call(this); // this指向Cat对象(Cat构造函数),执行之后会在Cat对象初始化colors属性
}
const instance1 = new Cat();
instance1.colors.push('black');
console.log(instance1.colors); // [ 'red', 'green', 'blue', 'black' ]
const instance2 = new Cat();
console.log(instance2.colors); // [ 'red', 'green', 'blue' ]
核心代码是Animal.call(this)
,创建子类实例时调用Animal构造函数,于是Cat的每个实例都会将Animal中的属性复制一份。
借用构造函数继承的特点:
- 解决了原型链继承中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
借用构造函数继承的缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3. 组合继承
组合上述两种方法就是组合继承。
核心:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
// 组合继承
function Animal(name) {
this.name = name;
this.colors = ['red', 'green', 'blue']
}
Animal.prototype.sayName = function() {
return this.name;
}
function Cat(name, age) {
// 继承Animal属性,第二次调用Animal
Animal.call(this, name);
this.age = age;
}
// 继承Animal原型上的方法,第一次调用Animal
Cat.prototype = new Animal();
// 重写Cat.prototype的constructor属性,指向自己的构造函数Cat
Cat.prototype.constructor = Cat;
Cat.prototype.sayAge = function(){
return this.age;
};
const instance1 = new Cat('doudou', 3)
instance1.colors.push('black');
console.log(instance1.colors); // [ 'red', 'green', 'blue', 'black' ]
console.log(instance1.sayName()); // doudou
console.log(instance1.sayAge()); // 3
const instance2 = new Cat('maomao', 1);
console.log(instance2.colors); // [ 'red', 'green', 'blue' ]
console.log(instance2.sayName()); // maomao
console.log(instance2.sayAge()); // 3
组合继承的缺点:
- 调用两次父类构造函数,一是在创建子类原型的时候,另一次是在子类型构造函数的内部。
4. 原型式继承
原型式继承的基本思路:利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型,返回控对象构造函数的实例。
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
// object()对传入其中的对象执行了一次浅复制
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
const anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
const yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
原型式继承的缺点:
- 来自原型对象的引用属性会被所有实例共享,如果原型对象的属性中有引用类型,在一个实例中修改引用类型的值,在其他实例中也会被修改。
- 无法传递参数
ES5中存在Object.create()的方法,能够代替上面的object方法。
5. 寄生式继承
核心:在原型式继承的基础上,增强对象
function createAnother(original){
var clone = object(original); // 通过调用 object() 函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强对象
console.log("hi");
};
return clone; // 返回这个对象
}
onst person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
const anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
寄生式继承的缺点同原型式继承
6. 寄生组合式继承(最佳)
所谓寄生组合式基础,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
基本思路:使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name, age){
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类,继承父类原型方法
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
// 新增子类原型属性
SubType.prototype.sayAge = function(){
console.log(this.age);
}
const instance1 = new SubType("doudou", 3);
const instance2 = new SubType("maomao", 1);
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "blue", "green", "black"]
console.log(instance2.color) // ["red", "blue", "green"]
这个例子的高效率体现在它只调用了一次SuperType
构造函数,并且因此避免了在SubType.prototype
上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof
和isPrototypeOf()
。
这是最成熟的方法,也是现在库实现的方法。
7. ES6 extends继承
前提知识
/* 类的特点
* 1. 类的所有方法都定义在类的prototype属性上;类的新方法可以添加在prototype对象上面。
* 2. 类内部所有定义的方法,都是不可枚举的,这一点与ES5的行为不一致。
* 3. 一个类必须有constructor()方法,如果没有定义,将会默认添加一个空的constructor()方法。
* 4. constructor()方法默认返回实例对象(即this),可以指定返回另一个对象。
* 5. 类必须使用new调用, 否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
* 6. 类不存在变量提升,这一点与ES5完全不同,这种规定的原因与继承有关,必须保证子类在父类之后定义。
* 7. 在类中,在一个方法前,加上static关键字,表示该方法不会被实例继承,而是直接通过类来调用,这就成为“静态方法”。
* 如果静态方法包含this关键字,这个this指的是类,而不是实例;父类的静态方法可以被子类继承; 静态方法也是可以从super对象上调用的。
* 8. 实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。
* 9. 目前有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示
* 10. 子类继承父类时,new.target会返回子类。
*/
- ES5继承,实质是先创造子类的实例对象
this
,然后再将父类的方法添加到this
上面(Parent.call(this)
) - ES6的继承机制完全不同,实质上是先将父类实例对象的属性和方法,加到
this上
(所以必须调用super()), 然后再用子类的构造函数修改this
。
类主要通过extends关键字来实现继承,使用示例如下:
class SuperType {
constructor(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
sayName() {
return this.name
}
}
class SubType extends SuperType {
constructor(name, age) {
// 继承父类的属性
super(name)
this.age = age
}
sayAge() {
return this.age
}
}
const instance1 = new SubType("doudou", 3)
const instance2 = new SubType("maomao", 1)
console.log(instance1.sayName()) // doudou
instance1.colors.push("2")
console.log(instance1.colors) // ["red", "blue", "green", "2"]
console.log(instance2.colors) // ["red", "blue", "green"]
extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样
function _inherits(subType, superType) {
// 创建对象,创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf ? Object.setPrototypeOf(subType, superType) : subType.__proto__ = superType;
}
}
补充
/* 类继承
* 1. 子类必须在constructor方法中调用super方法,否则新建实例时会报错。
* 原因:子类的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性何方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
* 2. 子类没有定义constructor方法,这个方法会被默认添加。也就是,不管有没有显示定义,任何子类都有一个constructor方法。
* 3. 在子类构造函数中,只有调用了super之后,才可以使用this关键字,否则会报错
* 4. super这个关键字,既可以当作函数使用,也可以当作对象使用。
* 4.1 super作为函数调用时,代表父类的构造函数,只能用在子类的构造函数之中,用在其他地方就会报错。
* 4.2 super作为对象时, 在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
* 5. Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
* 5.1 子类的__proto__属性,表示构造函数的继承,总是指向父类(这一点ES5没有)
* 5.2 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性
*/