都0202年了,你还不知道javascript有几种继承方式?

前言
    当面试官问你:你了解js哪些继承方式?es6的class继承是如何实现的?你心中有很清晰的答案吗?若是没有的话,能够经过阅读本文,帮助你更深入地理解js的全部继承方式。
 
    js继承总共分红5种,包括构造函数式继承、原型链式继承、组合式继承、寄生式继承和寄生组合式继承。
 
构造函数式继承
 
    首先来看第一种,构造函数式继承,顾名思义,也就是利用函数去实现继承;
 
    假设咱们如今有一个父类函数:
// 父类构造函数
function Parent(color) {
    this.color = color;
    this.print = function() {
        console.log(this.color);
    }
}

    如今要编写一个子类函数来继承这个父类,以下:ios

// 子类构造函数
function Son(color) {
    Parent.call(this, color);
}

    上面代码能够看到,子类Son是经过Parent.call的方式去调用父类构造函数,而后把this对象传进去,执行父类构造函数以后,子类Son就拥有了父类定义的color和print方法。es6

    调用一下该方法,输出以下:面试

// 测试
var son1 = new Son('red');
son1.print(); // red
var son2 = new Son('blue');
son2.print(); // blue
    能够看到son1和son2都正常继承了父类的print方法和各自传进去的color属性;
 
    以上就是构造函数式继承的实现了,这是最原始的js实现继承的方式;
 
    可是当咱们深刻想一下会发现,这种根本就不是传统意义上的继承!
 
 
    由于每个Son子类调用父类生成的对象,都是各自独立的,也就是说,若是父类但愿有一个公共的属性是全部子类实例共享的话,是没办法实现的。什么意思呢,来看下面的代码:
function Flower() {
    this.colors = ['黄色', '红色'];
    this.print = function () {
        console.log(this.colors)
    }
}
​
function Rose() {
    Flower.call(this);
}
​
var r1 = new Rose();
var r2 = new Rose();
​
console.log(r1.print()); // [ '黄色', '红色' ]
console.log(r2.print()); // [ '黄色', '红色' ]

    咱们如今有一个基类Flower,它有一个属性colors,如今咱们把某一个实例的colors值改一下:函数

r1.colors.push('紫色');
​
console.log(r1.print()); // [ '黄色', '红色', '紫色' ]
console.log(r2.print()); // [ '黄色', '红色' ]
    结果如上,显然,改变的只有r1的值,由于经过构造函数创造出来的实例对象中,全部的属性和方法都是实例内部独立的,并不会跟其余实例共享。
 
    总结一下构造函数的优缺点:
  • 优势:全部的基本属性独立,不会被其余实例所影响;
  • 缺点:全部但愿共享的方法和属性也独立了,没有办法经过修改父类某一处来达到全部子实例同时更新的效果;同时,每次建立子类都会调用父类构造函数一次,因此每一个子实例都拷贝了一份父类函数的内容,若是父类很大的话会影响性能;
原型链继承
 
    下面咱们来看第二种继承方式,原型链式继承;
 
    一样先来看下例子:
function Parent() {
    this.color = 'red';
    this.print = function() {
        console.log(this.color);
    }
}
function Son() {
}

    咱们有一个父类和一个空的子类;性能

Son.prototype = new Parent();
Son.prototype.constructor = Son;

    接着咱们子函数的原型属性赋值给了父函数的实例;测试

var son1 = new Son();
son1.print(); // red
    最后新建子类实例,调用父类的方法,成功拿到父类的color和print属性方法;
 
    咱们重点来分析一下下面两行代码:
Son.prototype = new Parent();
Son.prototype.constructor = Son;
    这段代码中,子函数的原型赋给了父函数的实例,咱们知道prototype是函数中的一个属性,js的一个特性就是:若是一个对象某个属性找不到,会沿着它的原型往上去寻找,直到原型链的最后才会中止寻找。
 
    关于原型更多基础的知识,能够参考一下其余文章,或许之后我也会出一期专门讲解原型和原型链的文章。
 
    回到代码,咱们看到最后实例son成功调用了Print方法,输出了color属性,这是由于son从函数Son的prototype属性上面去找到的,也就是从new Parent这个对象里面找到的;
 
   
    这种方式也不是真正的继承,由于全部的子实例的属性和方法,都在父类同一个实例上了,因此一旦某一个子实例修改了其中的方法,其余全部的子实例都会被影响,来看下代码: 
function Flower() {
    this.colors = ['黄色', '红色'];
    this.print = function () {
        console.log(this.colors)
    }
}
​
function Rose() {}
Rose.prototype = new Flower();
Rose.prototype.constructor = Rose;
​
var r1 = new Rose();
var r2 = new Rose();
​
console.log(r1.print()); // [ '黄色', '红色' ]
console.log(r1.print()); // [ '黄色', '红色' ]
​
r1.colors.push('紫色');
​
console.log(r1.print()); // [ '黄色', '红色', '紫色' ]
console.log(r2.print()); // [ '黄色', '红色', '紫色' ]
    仍是刚才的例子,此次Rose子类选择了原型链继承,因此,子实例r1修改了colors以后,r2实例的colors也被改动了,这就是原型链继承很差的地方。
 
    来总结下原型链继承的优缺点:
  • 优势:很好的实现了方法的共享;
  • 缺点:正是由于什么都共享了,因此致使一切的属性都是共享的,只要某一个实例进行修改,那么全部的属性都会变化 
组合式继承
 
    这里来介绍第三种继承方式,组合式继承;
 
    这种继承方式很好理解,既然构造函数式继承和原型链继承都有各自的优缺点,那么咱们把它们各自的优势整合起来,不就完美了吗?
 

 

 

   
 
 
 
 
 
 
 
组合式继承作的就是这个事情~来看一段代码例子:
function Parent(color) {
    this.color = color;
}
Parent.prototype.print = function() {
    console.log(this.color);
}
function Son(color) {
    Parent.call(this, color);
}
Son.prototype = new Parent();
Son.prototype.constructor = Son;
​
var son1 = new Son('red');
son1.print(); // red
var son2 = new Son('blue');
son2.print(); // blue
    上面代码中,在Son子类中,使用了Parent.call来调用父类构造函数,同时又将Son.prototype赋给了父类实例;为何要这样作呢?为何这样就能解决上面两种继承的问题呢?
 
 
    咱们接着分析一下,使用Parent.call调用了父类构造函数以后,那么,之后全部经过new Son建立出来的实例,就单独拷贝了一份父类构造函数里面定义的属性和方法这是前面构造函数继承所提到的同样的原理;
 
    而后,再把子类原型prototype赋值给父类的实例,这样,全部子类的实例对象就能够共享父类原型上定义的全部属性和方法。这也不难理解,由于子实例会沿着原型链去找到父类函数的原型。
 
    所以,只要咱们定义父类函数的时候,将私有属性和方法放在构造函数里面,将共享属性和方法放在原型上,就能让子类使用了。
 
    以上就是组合式继承,它很好的融合了构造函数继承和原型链继承,发挥二者的优点之处,所以,它算是真正意义上的继承方式。
 
寄生式继承
 
    既然上面的组合式继承都已经这么完美了,为何还须要其余的继承方式呢?
 
 
    咱们细想一下,Son.prototype = new Parent();这行代码,它有什么问题没有?
 
    显然,每次咱们实例化子类的时候,都须要调用一次父类构造函数,那么,若是父类构造函数是一个很大很长的函数,那么每次实例化子类就会执行很长时间。
 
    实际上咱们并不须要从新执行父类函数,咱们只是想要继承父类的原型。
 
    寄生式继承就是在作这个事情,它是基于原型链式继承的改良版:
 
var obj = {
    color: 'red',
    print: function() {
        console.log(this.color);
    }
};
​
var son1 = Object.create(obj);
son1.print(); // red
var son2 = Object.create(obj);
son2.print(); // red

    寄生式继承本质上仍是原型链继承,Object.create(obj);方法意思是以obj为原型构造对象,因此寄生式继承不须要构造函数,可是一样有着原型链继承的优缺点,也就是它把全部的属性和方法都共享了。this

寄生组合式继承
 
    接下来到咱们最后一个继承方式,也就是目前业界最为完美的继承解决方案:寄生组合式继承。
 
    没错,它就是es6的class语法实现原理。
 
    可是若是你理解了组合式继承,那么理解这个方式也很简单,只要记住,它出现的主要目的,是为了解决组合式继承中每次都须要new Parent致使的执行多一次父类构造函数的缺点。
 
    下面来看代码:
function Parent(color) {
    this.color = color;
}
Parent.prototype.print = function() {
    console.log(this.color);
}
function Son(color) {
    Parent.call(this, color);
}
Son.prototype = Object.create(Parent.prototype);
Son.prototype.constructor = Son;
​
var son1 = new Son('red');
son1.print(); // red
var son2 = new Son('blue');
son2.print(); // blue
    这段代码不一样之处只有一个,就是把原来的Son.prototype = new Parent();修改成了Son.prototype = Object.create(Parent.prototype);
 
    咱们前面讲过,Object.create方法是以传入的对象为原型,建立一个新对象;建立了这个新对象以后,又赋值给了Son.prototype,所以Son的原型最终指向的其实就是父类的原型对象,和new Parent是同样的效果;
 
 
    到这里,咱们5中js的继承方式也就讲完了;

 

 

 

 
 
 
 
 
 
 
 
 
    若是你对上面的内容感到疑问或者不理解的,能够留言和我交流,或者关注公众号直接联系我~
 
    最后感谢小伙伴的阅读,若是以为文章写的还能够的话,欢迎点个赞、点个关注,我会持续输出优质的技术分享文章。​
 
相关文章
相关标签/搜索