妈耶,这次终于了解继承了!

922 阅读7分钟

导读


相信许多小伙伴们对继承这个概念并不陌生,也是前端技术中较为基础和重点的地方,但是往往还有许多你并不真正了解的地方,先回答我下面这几个问题:

  • 实现继承有哪几种方式?
  • 了解寄生组合继承吗?它到底解决了什么问题?
  • 组合继承的优缺点是什么,和寄生组合继承差别在那?
  • 可以手写一个原型式继承吗?

    好,如果你可以很好的回答出上面这几个问题,那么你可以跳过这篇文章或者可以帮笔者去检查一下有没有出错或者有纰漏的地方,而那些没有回答上来的下伙伴,不要怀疑自己,快来和我一起在学一下吧。


构造函数继承

构造函数继承的思想特别简单,就是在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此可以通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。


function SuperType(){
    this.colors = ['red', 'block', 'white'];
}

function SubType(){
    // 继承了SuperType
    SuperType.call(this);
}

let child1 = new SubType();

child1.colors.push('yellow');

console.log(child1.colors); // ['red', 'block', 'white','yellow']

let child2 = new SubType();

console.log(child2.colors); // ['red', 'block', 'white']

对于原型链来说,构造函数有一个比较大的优势,就是可以在子类型的构造函数中向超类型构造函数传递参数。


function SuperType(name){
    this.name = name;
}

function SubType(){
    SuperType.call(this, '周元');
    this.age = 24;
}

let child = new SubType();

console.log(child.name); // 周元

console.log(child.age); // 24

缺点

方法都是在构造函数中定义的,无法函数复用,而且子类型不能继承超类型原型中定义的方法。实践中也会很少使用构造函数继承。



原型链继承

原型链继承的基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。

这里先回顾一下构造函数、原型和实例的关系。
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。


function SuperType(){
    this.colors = ['red', 'block', 'white'];
}

function SubType(){}

//继承了SuperType
SubType.prototype = new SuperType();

let child1 = new SubType();

child1.colors.push('yellow');

console.log(child1.colors); // ['red', 'block', 'white','yellow']

let child2 = new SubType();

console.log(child2.colors); // ['red', 'block', 'white', 'yellow']

这里SuperType构造函数定义了一个colors属性,该属性包含一个数组类型,SuperType的每个实例都会有各自包含自己数组的colors属性。当SubType通过原型继承了SuperType之后,SubType.prototype就变成了Supertype的一个实例,因此它也拥有一个自己的colors属性。
原型链继承 解决了构造函数继承无法函数复用的问题,但是同时也出现了一些其他的问题。

比如:不可以向超类型的构造函数传递参数以及实例共享的问题。实践中也会很少使用原型链继承。



组合继承

组合继承其实也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一起,发挥二者之长的一种继承手段。

具体思路就是使用原型链实现对原型属性和原型方法的继承,而通过构造函数来实现对实例属性的继承。


function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'black', 'white']
}

SuperType.prototype.sayName = function () {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

SubType.prototype = new SuperType();

SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function () {
    console.log(this.age);
}

let child1 = new SubType('周元', 24);

child1.colors.push('yellow');

console.log(child1.colors); // [ 'red', 'black', 'white', 'yellow' ]

child1.sayName(); //周元

child1.sayAge(); // 24

let child2 = new SubType('夭夭', 23);

child2.colors.push('green');

console.log(child2.colors); // [ 'red', 'black', 'white', 'green' ]

child2.sayName(); //夭夭

child2.sayAge(); // 23

组合继承的方式避免了原型链继承和构造函数继承的缺陷,融合了他们的优点,成为JS最常用的继承方式。
但是这种继承方式也是存在着一点小缺点,就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。


原型式继承

原型式继承的基本思想就是可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。


function object(o) {
    function F(){}
    F.prototype = o;
    return new F();
}


在object函数内部,先创建了一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时函数的一个实例。

在ECMAScript5中新增了object.create()方法规范了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和(可选)一个为新对象定义额外的属性的对象。在传入一个参数的情况下和object()方法相同。

let SuperType = {
    name: '吞吞' ,
    friends: ['周元', '夭夭', '苍渊']
}

let SubType1 = Object.create(SuperType);

SubType1.name = '赵牧神';

SubType1.friends.push('九宫');

let SubType2 = Object.create(SuperType);

SubType2.name = '郗菁';

SubType2.friends.push('赵仙隼');

console.log(SuperType); // { name: '吞吞', friends: [ '周元', '夭夭', '苍渊', '九宫', '赵仙隼' ] }


下面我们再来看一下添加第二个参数的效果


var person = {
    name: '苏幼微',
    friends: ['武瑶', '武煌']
}

var child = Object.create(person, {
    name: {
        value: '叶冰凌'
    }
});

console.log(child); // {name: '叶冰凌'}

关于原型式继承的缺点和原型链继承相同,就是会共享实例。


寄生式继承

寄生式继承的基本思路与寄生构造函数和工厂模式类似,就是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后在返回这个对象。


function createAnother(original){
    var clone = Object.create(original);  // 通过调用函数创建一个对象
    clone.sayHi = function(){ // 以某种方式来增强这个对象
        console.log('h1');
    }
    return clone; // 返回这个对象
}

var person = {
    name: '伊秋水'
}

var newPerson = createAnother(person);

newPerson.sayHi(); // hi

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。但是使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点和构造函数继承类似。


寄生组合式继承

寄生组合继承的基本思想就是借用构造函数来继承属性,通过原型链的混合形式来继承方法,而且不必为了指定子类型的原型而调用超类型的构造函数。我们所需要的就是超类型原型的一个副本而已。本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。


function inheritprototype( subType, superType ) {
    var prototype = Object.create(superType.prototype); // 创建对象
    prototype.constructor = subType; // 指定原型
    subType.prototype = prototype; // 指定对象
}

inheritprototype函数实现了寄生组合式继承的最简单形式,主要是分为三步:

1.创建超类型原型的一个副本。
2.为创建的副本添加constructor属性。
3.将新创建的对象赋值给子类型的原型。

然后我们就可以这样使用:


function SuperType(name) {
    this.name = name;
    this.colors = ['周擎天', '秦玉'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

inheritprototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var subType1 = new SubType('绿萝', 22);

subType1.sayName(); // 绿萝

subType1.sayAge(); // 22

寄生组合式继承只调用一次超类型构造函数,避免在SubType prototype上面创建不必要的、多余的属性。开发者普遍认为寄生组合式继承是引用类型最理想的继承方式。


ES6继承

ES6继承的核心就是通过extends来实现继承。


class SuperType {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}

class SubType extends SuperType {
    constructor(name, grade) {
        super(name); // 记得用super调用父类的构造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

var subType1 = new SubType('周元', 1);

subType1.hello(); // Hello, 周元!

subType1.myGrade(); // I am at grade 1

使用class继承的时候,我们需要注意一下几点

  • 子类必须在constructor方法中调用super方法。
  • 只有在调用super()之后,才可以使用this关键字

这里在稍微提一下class的特点

  • class 声明会提升,但不会初始化赋值。
  • class 声明内部会启用严格模式。
  • class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
  • class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用.
  • 必须使用 new 调用 class。
  • class 内部无法重写类名。

综上 就是我对于前端实现继承的几种方式的一点小见解,文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞👍和关注,😀。

推荐阅读

参考


  • JavaScript高级程序设计