经过原型继承理解ES6 extends 如何实现继承

前言

第一次接触到 ES6 中的 class 和 extends 时,就听人说这两个关键字不过是语法糖而已。它们的本质仍是 ES3 的构造函数,原型链那些东西,没有什么新鲜的,只要理解了原型链等这些概念天然就明白了。这话说的没错,可是这些继承的实现是不是咱们想的那样呢,今天让咱们来用原型链解释下 ES6 extends 如何实现的继承。函数

结论

这里先上结论,若是有理解不对的地方,欢迎在留言指出;若是有不理解的地方能够看完结论后继续阅读,若是阅读完后有难以理解指出也欢迎留言讨论。this

  1. extends 的继承经过两种方式完成了三类值的继承
  2. 构造函数设置的属性经过复制完成继承
  3. 实例方法经过实例原型之间的原型链完成继承
  4. 构造函数的静态方法经过构造函数之间的原型链完成继承

属性经过复制完成继承

class 实例对象属性的继承是经过复制达到继承效果的,这里的属性指的是经过构造函数的 this 定义的属性。spa

class Person {
    constructor(name) {
        this.maxage = 100;
        this.name = name
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
}
const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaozhi')

console.log(personA.hasOwnProperty('maxage'));
console.log(programmerB.hasOwnProperty('maxage'));复制代码

以上代码的打印结果都是true,这个结果就证实了对象的 extends 继承的属性是经过复制继承的,而不是经过原型链完成的属性继承。prototype

咱们将以上代码中获得的两个实例对象打印出来,能够获得以下图结果3d

根据打印结果能够更直观的看到两个实例对象上均有 maxage 属性。原始类型的值的复制好理解,直接拷贝值就好,那么引用类型的复制是深拷贝,仍是浅拷贝,或者说仅仅是对象引用的拷贝呢?code

构造函数对象值的继承,比想象中要复杂一点,根据代码实践(暂未查看标准),得出结论,引用类型的继承主要分为两种状况:cdn

  1. 字面量定义的对象属性是深拷贝
  2. 变量赋值对象属性是引用复制

字面量定义的对象属性是深拷贝

这里的字面量定义的对象属性指的是,指的是直接在构造函数中经过 {} 的形式定义的对象直接赋值给 this 的某个属性。代码形如对象

class A {
    constructor() {
        this.obj = {
            name: 'obj',
            secondObj: {
                name: 'secondObj'
            }
        }
    }
}复制代码

示例代码中,obj 属性是直接经过 {} 定义的一个对象。blog

class Person {
    constructor(name) {
        this.maxage = 100;
        this.name = name;
        this.obj = {
            name: 'obj',
            secondObj: {
                name: 'secondObj'
            }
        };
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj)
console.log(personA.obj.secondObj === programmerB.obj.secondObj)复制代码

上述代码的运行结果以下继承

Person 的实例对象上定义了一个 obj 属性,该属性被 Programmer 的实例对象继承,经过对比这两个属性的值,咱们知道他们并不相等,这首先排除了是引用复制的可能(若是是引用复制,这里两个属性应该指向同一个对象,也就是其存储的内存地址应该是致的,可是这里获得的结果应该是等式不成立)。经过实例对象属性 obj 中的 secondObj 属性的比较,排除了这是浅拷贝,由此咱们能够得出在代码示例的场景中引用类型的继承是经过深拷贝完成的。

变量赋值对象属性是引用复制

按理来讲咱们得出上一小节的结论应该基本就能够肯定 extends 继承是如何处理引用类型的值的继承了,可是事实是到这里并无结束。

考虑以下代码,这段代码和上一小节的代码区别不大,有变化的地方是,这是在外部定义了一个变量,变量的值是对象,而后将变量赋值给了了 obj 属性。

let obj = {
    name: 'obj'
}
class Person {
    constructor(name) {
        this.maxage = 100;
        this.name = name;
        this.obj = obj;
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj);复制代码

运行结果以下

从代码运行结果中不难看出,这里出现了变化,经过变量赋值定义的对象属性,是经过引用复制完成继承的。下面咱们来看看对象变量被定义在构造函数中而后再赋值给对象的属性是否仍是这样的结果。

class Person {
    constructor(name) {
        const innerObj = {
            name: 'obj'
        } 
        this.maxage = 100;
        this.name = name;
        this.obj = innerObj;
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj);复制代码

运行结果以下

没错当你把变量定义在构造函数中,而后来赋给 this 的属性的时候,是经过深拷贝来继承的。神奇不,一样是变量,只是变量定义的做用域不同,连继承方式都变了,具体为何要这么作,我如今还不太清楚,改日查下标准,有知道的同窗还望评论区不吝赐教。

小结

这节有点长,须要个小结总结下咱们获得的结论。首先,extends 的构造函数定义的属性值的继承是经过复制继承的。第二点,副职的方式主要分为如下几重情形:

  1. 原始类型直接复制值到子类对象
  2. 引用类型的值若是值是直接在构造函数中定义的(包括字面量直接赋值给属性和在构造函数内定义的变量而后变量赋值给属性),那么其会被深拷贝到子类对象上
  3. 在构造函数外定义的变量,其值是引用类型,构造函数中将该变量赋值给对象的某个属性,该属性会被经过引用复制的方式拷贝到子类对象上

实例方法的继承

实例方法的继承比较好理解,经过原型链原型链继承的,只不过这个链的形式是一个比较直接的链。这条链的大概就像下面这个图

没错,图上那条红色的线就是 programmerB 这个实例对象继承 eat 方法的方式,是否是和想的不同。这条链仍是比较好理解的,具备继承关系的构造函数的 prototype 之间有一条原型链,而每一个实例对象的原型又是其构造函数的 prototype,这样一来就产生了图中红色线条实例方法的原型链。两个 class 的实例对象之间没有什么关系。

若是对上面的图存在疑问运行下面这段代码,运行结果会证实图是没有问题的。

class Person {
    constructor(name) {
        const innerObj = {
            name: 'obj'
        } 
        this.maxage = 100;
        this.name = name;
        this.obj = innerObj;
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.__proto__ === Person.prototype);
console.log(programmerB.__proto__ === Programmer.prototype);
console.log(Programmer.prototype.__proto__ === Person.prototype);
console.log(programmerB.__proto__.__proto__ === Person.prototype);复制代码

静态方法的继承

相比实例方法的继承,静态方法的继承要简单的多,就是一条简单的原型链,具备继承关系的两个 class 之间存在一条原型链。以下图这样

这个关系图就没什么多说了,有疑问的同窗能够随便写段验证下。

继承关系图

这里假定 class B extends A,那么关于原型 class 之间的原型继承可得出以下等式。

B.proto === Ab.proto === B.prototypea.proto === A.prototypeB.prototype.proto === A.prototypeb.proto.proto === A.prototype

用关系图来表达上面的这些等式会更容易理解

转载请注明出处!

相关文章
相关标签/搜索