在之前的面试过程中,经常会被面试官问到JS实现继承的方式。由于在校期间修过Java,日常开发中常用的是ES6的extends所以一直没有对JS的继承深入了解,知道看了《JavaScript设计模式》这本书后,才发现JS中的继承,其实大有学问,接下来我就借花献佛,给大家简单分享一下JS的继承方式。
在了解各种继承之前,需要大家了解创建一个类的实例var book1 = new classA()中发生了什么。
- 创建一个空的新对象:
var obj = {};
- 设置新对象的__proto__属性指向类的prototype属性
obj.__proto__ = classA.prototype;
- 使用新对象调用类的构造函数,并且将this指向新对象
classA.call(obj);
- 将初始化完毕的新对象(obj)地址保存到等号左边,完毕
var book1 = new classA();
1 类式继承
// 父类
function SuperClass() {
// 共有方法
this.superValue = true;
}
// 父类添加原型方法
SuperClass.prototype.getSuperValue = function() {
return this.superValue;
}
// 子类
function SubClass() {
this.subValue = false;
}
// 类式继承
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubValue = function() {
return this.subValue;
}
var sub1 = new SubClass();
我们看到SubClass.prototype = new SuperClass()这句话。类的原型对象就是为类的原型添加共有方法,我们实例化一个父类时(想想文章开头new一个对象的四个步骤),新对象将__proto__指向了父对象的prototype属性(第二步),并且复制了父类的构造函数内的属性和方法(第三步),创建的对象不仅可以访问父类原型上的属性和方法,也可以访问父类构造函数中的属性和方法(就是prototype中的方法和SuperClass中的方法)。
类式继承的缺点
1. 如果父类构造函数中的共有属性有引用类型,当我们修改某个实例的这个引用类型的时候,另一个实例也会受影响
2. 子类在创建的时候,无法给父类的构造函数传参(相当于每次从父类继承过来的属性都是同一个)
2 构造函数继承
为了解决类式继承的两个缺点,我们可以尝试着使用构造函数继承(但这种方法也有缺陷,要不然文章到这不就完结了嘛? )
function SuperClass(id) {
// 引用类型共有属性
this.books = [1, 2, 3];
// 值类型共有属性
this.id = id;
}
SuperClass.prototype.showBooks = function() {
console.log(this.books);
};
function SubClass(id) {
SuperClass.call(this, id); // 构造函数继承,对没错是你最熟悉的样子
}
var instance1 = new SubClass(10);
var instance2 = new SubClass(11);
instance1.books.push(4);
console.log(instance1.books, instance1.id, instance2.book, instance2.id); // [1, 2, 3] 10 [1, 2, 3, 4] 11
instance1.showBooks(); // TypeError
注意!注意!SuperClass.call(this, id) 是构造函数继承的精华,由于call可以改变this的指向,所以等于在子类的构造函数中将父类的构造函数执行了一遍,因此解决了类式继承中无法传参
(现在可以在子类的构造函数中传参给父类的构造函数)和多个实例中修改引用类型属性会同时影响
(每次创建一个实例,都会将构造函数重新执行一遍)的缺点。
But!But! 由于这种继承不涉及到原型prototype,所以父类的原型属性我们就继承不了了,可以看到代码最后报了’TypeError’的错误。这也是构造函数继承的一个缺点,那有不足的地方就得改正啊,怎么改正呢?组合继承了解一下?
3 组合继承
其实我们大胆一点,可以把类式继承和构造函数继承看做是两种基础的继承类型,而组合继承就是类式继承 + 构造函数继承
function SuperClass(name) {
this.name = name;
this.books = [1, 2, 3];
}
SuperClass.prototype.getName = function() {
console.log(this.name);
}
function SubClass(name, time) {
SuperClass.call(this, name); // 构造函数继承方法
// 从父类继承后,新增一个属性
this.time = time;
}
SubClass.prototype = new SuperClass(); // 类式继承方法
SubClass.prototype.getTime = function() {
console.log(this.time);
}
var instance1 = new SubClass('name1', 2020);
var instance2 = new SubClass('name2', 2020);
instance1.books.push(4);
console.log(instance1.books); // [1, 2, 3]
console.log(instance2.books); // [1, 2, 3, 4] 改变引用类型属性不会对其他实例造成影响
instance1.getName(); // 可以调用父类的原型属性
instance1.getTime(); // 子类的原型属性
instance2.getName();
instance2.getTime();
OK! Nice! 我们可以看到,使用了组合继承,既解决了类式继承的引用类型问题和传参问题(类式继承的缺点),又解决了构造函数继承不能使用父类的原型属性问题(构造函数继承的缺点)。
BUT! BUT 有没有发现我们父类的构造函数在构造函数继承中被调用了一次,在类式继承中又被调用了一次(如果不清楚为什么,可以参考new一个对象发生了什么)。没错,我们使用组合继承,父类构造函数被调用了两次,所以这并不是最完美的方式。
keep patient ! keep patient!
可以先提前透露下,最优秀的继承方式:·寄生组合式继承
,但是在这之前需要先了解一下原型式继承和寄生式继承。
4 原型式继承
function inheritObject(o) {
// 声明一个过渡函数对象
function F() {}
// 过渡对象的原型继承父对象 (感觉和类式继承有点像)
F.prototype = o;
return new F();
}
对于这个原型式继承,其实是基于类式继承的一个封装。类式继承需要我们先声明一个子类,然后将子类的prototype指向父元素的实例,而原型式继承则是在内部声明一个过渡对象,直接返回实例。(但是我个人感觉,原型式继承不是很灵活,如果我们子类需要额外的属性,就得每次去修改funciotn F()这个过渡对象的构造函数,或者使用其他办法)
5 寄生式继承
啪啪啪!我上面不是才说了原型式继承这种方式不太灵活吗?如果子类想要赋予额外的属性,是一件很麻烦的事情,寄生式继承就提供了一种更好的方案。
function inheritObject(o) {
function F() {}
F.prototype = o;
return new F();
}
var book = {
name: 'js book',
aLikeBook: ['css book', 'html book']
}
function createBook(obj) {
var o = new inheritObject(obj);
o.getName = function() {
console.log(name);
};
return o;
}
其实吧,寄生式继承也没啥,又是基于原型式继承的二次封装,并且在这第二次封装中对继承的对象进行了拓展,这样创建的对象不仅仅有父类中的属性和方法,还可以添加新的属性和方法。
6 终极继承者–寄生组合式继承
终于,到了最NB的一种继承方式了。顾名思义,应该就是构造函数继承 + 寄生继承(包括了类式继承和原型式继承)
之前说的组合式继承存在一些问题:
-
在组合式继承中,存在父类的构造函数被调用两次的问题
-
子类不是父类的实例,而子类的原型是父类的实例(书上这样说的,我暂时也没太想明白为什么这样不妥)
组合寄生式集成可以分成两部分:一部分式继承父类的属性和方法,另一部分继承父类的原型属性
- 继承父类的属性和方法,我们使用构造函数继承
- 通过寄生式继承重新继承父类的原型
// 原型式继承
function inheritObject(o) {
function F() {}
F.prototype = o;
return new F();
}
// 对子类原型的处理,为什么呢?我们不希望子类的原型是父类的实例,听着别扭?你可以打印一下类式继承的sub1.__proto__.constructor 发现是父类的构造函数,接下来需要进行改造
function inheritPrototype(subClass, superClass) {
// 复制一份父类的原型副本保存在变量中
var p = inheritObject(superClass.prototype);
// 修正因为重写子类原型导致子类的constructor属性被修改
p.constructor = subClass;
// 设置子类的原型
subClass.prototype = p; // p可以看作是父类的一个实例,而F只是一个过渡对象
}
// 定义父类
function SuperClass(name) {
this.name = name;
this.color = ['red', 'blue', 'green'];
}
// 定义父类的原型方法
SuperClass.prototype.getName = function() {
console.log(this.name);
}
// 定义子类
function SubClass(name, time) {
// 构造函数式继承,继承父类的共有属性和方法
SuperClass.call(this, name);
// 子类新增属性
SubClass.time = time;
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass);
// 子类新增原型方法
SubClass.prototype.getTime = function() {
console.log(this.time);
}
// 创建两个实例来测试一下
var instance1 = new SubClass('js book', 2019);
var instance2 = new SubClass('css book', 2020);
instance1.colors.push("black");
console.log(instance1.colors);
console.log(instance2.colors);
instance2.getName();
instance2.getTime();
首先看到构造函数继承,帮助我们执行了父类的构造函数,继承了父类的共有属性。重点在inheritPrototype这个函数中,由于我们希望子类是父类的实例,于是乎使用inheritObject方法创建一个父类的实例(F相当于只是一个过渡对象),由于我们使用类式继承constructor是父类的构造函数,需要修正一下p.constuctor = subClass,改成自己的构造函数。
现在我们可以看到,instance.proto.constructor已经为子类的构造函数了。(有没有发现,父类的构造函数这时候没有被调用两次,inheritPrototype只是对父类的原型进行了复制)
参考内容: 《JavaScript设计模式第二章》