Es6让我们在进行javascript coding的时候带来了巨大的便捷性, 就继承而言, 我们只需要简单的使用extend关键字就可以实现, 但是这种继承方式其实也是一种语法糖(我们在使用babel编译的时候还是可以看到其变异后的代码还是使用了Es5的继承方式), 所以, 学习继承方式可以让我们更好的理解javascript, 让我们开始吧
前提知识
- 如果一个对象A的原型__proto__指向一个函数B的prototype, 那么A instance B为真
- 如果一个对象A的原型__proto__的__proto__指向函数B的prototype, 上述条件也成立
- 对基本数据类型使用instanceof永远为false, 即使基本数据类型A的原型__proto__指向一个函数B的prototype 验证
const A = 1
function B() {}
Object.setPrototypeOf(A, B.prototype); // 这个设置不会生效
A.__proto__ = B.prototype // 这个设置也不会生效
console.log(A.__proto__);
// Number(...)
console.log(A instanceof B);
// false
console.log(A instanceof Number);
// false
1. 借用构造函数继承
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.eat = function () {
console.log(`${this.name} is eating...`);
};
function Dog(name, age, color) {
Animal.call(this, name, age);
this.color = color;
}
const dog = new Dog("Husky", 2, "Black");
console.log(dog.name);
// Husky
console.log(dog.age);
// 2
console.log(dog.color);
// Black
dog instanceof Dog
// true
dog instanceof Animal
// false(因为dog.__proto__没有指向Animal.prototype)
dog.eat()
// Uncaught TypeError: dog.eat is not a function at <anonymous>:1:5
这种继承缺点很明显
- 只继承了Animal构造函数里面的属性, 没有继承到Animal原型上的属性
- new出来的对象只是子类的实例, 不是父类的实例(instanceof)
2. 原型链继承
为了结果借用构造函数继承导致不是父类实例的问题, 我们直接改变子类的prototype属性的指向, 让它指向一个父类的实例
function Animal(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
Animal.prototype.eat = function () {
console.log(`${this.name} is eating...`);
};
olor) {
function Dog(c
this.color = color;
}
Dog.prototype = new Animal("DuoDuo", 2, ["MeiMei", "LiLi"]);
const dog = new Dog("Black");
const dog1 = new Dog("White");
console.log(dog.name)
// DuoDuo
console.log(dog.age)
// 2
console.log(dog.friends)
// ['MeiMei', 'LiLi']
dog.friends.push("Tom")
console.log(dog.friends)
// ['MeiMei', 'LiLi', 'Tom']
console.log(dog1.friends)
// ['MeiMei', 'LiLi', 'Tom']
console.log(Dog.prototype.constructor;)
// Animal (当然, 因为我们改变了prototype这个对象, 所以位于这个对象的constructor的指向肯定也是不对的
这种继承的缺点也很明显
- 因为是直接指定子类的prototype为一个对象, 所以无法传参, 这就导致所有的狗狗都叫多多
- 从父类继承而来的引用类型的数据存在数据共享问题, 这就导致我们改了狗狗1的朋友以后, 狗狗2的朋友也随之改变了
3.组合式继承
组合式继承即上述两种继承方式的组合
function Animal(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
Animal.prototype.eat = function () {
console.log(`${this.name} is eating...`);
};
function Dog(name, age, friends, color) {
Animal.call(this, name, age, friends);
this.color = color;
}
Dog.prototype = new Animal("DuoDuo", 2, ["MeiMei", "LiLi"]);
Dog.prototype.constructor = Dog // 修复constructor指向问题
这种继承方式可以看到, 因为我们在子类中调用了this.friend的赋值, 所以不会存在属性共享的问题, 也解决了传参问题, 看上去是一种比较完美的继承方式, 但是这种继承方式也会有一些问题
- 一个问题Animal被调用了2次, 增加了开销
- 另外一个问题就是其实friends应该属于实例上的属性, 但是现在相当于把这些属性放在父类上面, 然后在子类中再去覆盖它, 怎么样, 是不是有点怪怪的?
4.寄生式继承
寄生式继承可以理解: 1.创建一个原型指向父类的空对象 2.并且改变这个空对象的constructor指向为父类 并将这个空对象作为子类的父类, 就是将这一部分代码
Dog.prototype = new Animal("DuoDuo", 2, ["MeiMei", "LiLi"]);
Dog.prototype.constructor = Dog // 修复constructor指向问题
替换为
Dog.prototype = Object.create(Animal.prototype, {
constructor: {
value: Animal
}
})
可见, 通过寄生式继承, 我们同样是创建了一个对象来作为子类的上一层, 但是解决组合继承的2个问题, 就是这个对象不会有那些冗余的属性, 也不存在父类多次调用的问题
5.寄生组合式继承
结合寄生式继承和组合式继承的优点, 我们得到了完美的继承方案
function Animal(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
Animal.prototype.eat = function () {
console.log(`${this.name} is eating...`);
};
function Dog(name, age, friends, color) {
Animal.call(this, name, age, friends);
this.color = color;
}
Dog.prototype = Object.create(Animal.prototype, {
constructor: {
value: Animal,
},
});
6.extends关键字继承
终于, ECMA看不下去了, 出了一个方便的extends关键字来继承
class Animal {
constructor(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
eat () {
console.log(`${this.name} is eating...`);
};
}
class Dog extends Animal {
constructor(name, age, friends, color) {
super(name, age, friends,)
this.color = color;
}
}
知识补充
1.为何总是要修正constructor方法的指向呢?
我的理解是, 其实并没有什么卵用, 因为我们的实例的__proto__是有constructor这个属性的, 这个属性指向我们的构造函数, but, __proto__本来就是一个不建议访问的属性了, 难道真的会有人去访问__proto__这个属性吗?但是由于js的设计者在上面__proto__携带了这个属性, 那从语义化和程序员的强迫症的角度来说, 我们当然要求修正他
2.我们老是让子类的prototype指向一个新的对象, 例如Dog.prototype = new Animal(), 为什么不直接点? 让Dog.prototype = Animal.prototype不行吗?
不行! 因为, 我们会对子类的原型对象进行拓展, 例如
Dog.prototype.run = function() {
console.log(`${this.name} is running, hush hush...`)
}