12月26, 2017

浅谈JS中的继承者们(一)

2018年啦,祝大家新年快乐

2018年第一篇《浅谈JS中的继承者们》送给你们

面向对象语言的一个重要标志是:类,通过类来实现创建多个具有相同属性和方法的对象。但是js在ES5及以前的版本中,并没有真正意义上的类的概念。 JS中实现继承的方式主要通过原型链的方式。接下来,我们来总结一下ES5和ES6继承实现方式。

ES5原型链继承

function Foo(){
    this.attr = 'Jack';
    this.color = ["red", "blue"]
    this.obj = {a: '1'}
    this.sayName = function(){
        console.log(this.name);
    }
}
Foo.prototype.friend = "Nic"


function Bar() {

}

console.log(Bar)
console.log(Bar.prototype)    //Bar {}
console.log(Bar.prototype.attr)     //undefined
console.log(Bar.prototype.friend)     //undefined

//实例化Bar

var f1 = new Bar()
console.log( f1.attr)   //undefined
console.log(f1.friend)   //undefined

上面的方法实现了一个构造函数Foo,这个构造函数中包含attr,color,obj,sayName私有属性,在原型链上包含friend属性。

同时,我们声明一个Bar构造函数,此时Foo和Bar彼此是相互独立。

我们实例化Bar构造函数,产生一个f1实例,这是输出f1中的attr,friend都没有进行定义。

现在我们只管的通过内存中的模型图来描绘一下Foo,Bar,f1的关系。

alt

通过上图可以直观的看出Foo和Bar在内存中是彼此独立的两个空间,而f1是Bar的实例,f1的__proto__属性指向Bar的prototype。由于f1私有属性和Bar的属性中都不含有attrfriend属性,所以抛出undefined。

现在我们希望Bar可以继承Foo的属性,帮助Bar找回失散多年的父亲Foo,我们先尝试使用D(原)N(型)A(链)的方式来关联。

Bar.prototype = new Foo()

console.log(Bar)

console.log(Bar.prototype)    //Foo { attr: 'Jack', sayName: [Function] }

console.log(Bar.prototype.attr)    //Jack

console.log(Bar.prototype.friend)    //Nic

//实例化Bar

var f1 = new Bar()
console.log( f1.attr)   //Jack
console.log(f1.friend)    //Nic

可以看到,通过重新定义了Bar的prototype属性,为其赋值了一个Foo的实例对象。

我们现在输出一下Bar.prototype的内容:

alt

此时Foo中的所有属性都由Bar函数的原型(prototype)继承了。Foo函数的原型的属性friend则在Bar.prototype对象的上一层proto中得到继承。

嗯,说蒙了吧?来一张图理解一下:

alt

我来解释一下:

现在Bar的prototype指针不再指向原来的默认内存地址(0x1212ff91),而是指向一个虚拟实例instance(0X1212ff77)所在的存储空间,instance作为Foo构造函数的实例对象,包含proto(0x1212ff90)属性,指向Foo函数的prototype(原型)(0x1212ff90)所在的内存空间(0x1212ff90),而f1作为Bar的实例对象,拥有__proto__属性指向Bar的prototype属性,Bar的prototype属性指向instance实例,而instance实例的__proto__属性指向Foo构造函数的prototype属性。

所以,现在我们把f1的内容输出出来:

alt

可以看到由于Bar没有私有属性,所以f1没有从Bar中继承任何Bar的私有属性,只拥有指向Bar构造函数原型的__proto__默认属性,而这个__proto__则包含着_来自西伯利亚的父亲_Foo的所有属性,也就是说Foo的私有属性也通过原型链的方式继承到Bar函数中。

f1在获取属性值的时候会首先检索自己有没有该属性,如果没有的话,则向上追溯到f1.__proto__来检查,如果还是没有,则继续向上追溯 f1.__proto__.__proto__,如果f1.__proto__.__proto__ 为null则停止检索,返回undefined。用我们上面这个例子举例:f1.friend首先会检索f1中是否包含attr属性,发现没有,就会到f1.__proto__来检查,发现还是没有,于是查找f1.__proto__.__proto__找到了friend的值,就输出出来。那如果我们输出f1.test的值(实际并没有这个属性),在检索f1,f1.__proto__,f1.__proto__.__proto__都没有后,会继续向上查找f1.__proto__.__proto__.__proto__(此时指向object)依然没有找到,当获取f1.__proto__.__proto__.__proto__.__proto__返回了null,则停止检索,抛出undefined。

alt

我们已经通过原型链(图中红色的箭头)的方式实现了构造函数的继承。但是通过这种方式实现继承有一个致命的缺点:

引用类型值的原型属性会被所有实例共享

我们就刚才的栗子继续:

f1.color.push("black")
console.log(`f1.color:`, f1.color)  //[ 'red', 'blue', 'black' ]
var f2 = new Bar()
console.log(`f2.color:`, f2.color)  //[ 'red', 'blue', 'black' ]

可以看到我改变f1的color属性,f2的color属性也同步的改变,原理嘛,看下图就可以直观的回答了:

alt

f1和f2的__proto__共享Foo.prototype的内存空间,所以f1改变__proto__的属性时,f2也会同步改变。

ES5借用构造函数继承

接下来我们通过另一种方式:滴(组)血(合)认(继)亲(承)来找回Bar失散多年的爸爸。

看下面这个例子:

function Foo(){
    this.attr = 'Jack';
    this.color = ["red", "blue"]
    this.obj = {a: '1'}
    this.sayName = function(){
        console.log(this.name);
    }
}
Foo.prototype.friend = "Nic"


function Bar() {
    Foo.call(this)
}
var f1 = new Bar()

console.log(f1)

在这个例子中,Foo依然是那个风华正茂的Foo,拥有这众多的私有属性和原型属性,Bar不在是以前的Bar了,这一次,Bar的原型链没有被重新改写,而是直接在Bar内部将Foo的属性继承为Bar自己的私有属性。 我们看一下输出f1结果:

alt

发现attr,color,obj,sayName不再存在于f1.__proto__属性中了,而是属于f1的私有属性了。这时,我们尝试改变color的值,看看会不会改变Foo的原型属性。

f1.color.push("black")
console.log(`f1.color:`, f1.color)  //[ 'red', 'blue', 'black' ]
console.log(f1.friend)   //undefined
var f2 = new Bar()
console.log(`f2.color:`, f2.color)  //[ 'red', 'blue' ]

下面我们还是从内存角度讲一下这个的含义:

alt

可以看到Foo的原型链的属性并没有关联到Bar构造函数中,调用call只是将Foo中的私有属性继承到Bar的私有属性中,同时f1和f2的__proto__属性也只是指向到Bar的prototype所在的存储空间,在实例化Bar的时候产生的f1和f2分别指向了不同的存储空间,所以改变f1的私有属性并不会影响到f2。

显然,原型链继承方式存在弊端在组合继承里面得到了完美的解决,以为问题就结束了么?Too young Too simple!!!alt

想想看,现在没有共享属性了,我想同时改变实现继承Foo原型上的friend属性到f1和f2怎么办?!

ES5组合继承

下面来介绍一种集天地之灵,取日月之精华的一个好方法:把上面两种组合到一起,称:组合继承(称:DNA滴血认亲)。

栗子已经粉墨登场:

function Foo(){
    this.attr = 'Jack';
    this.color = ["red", "blue"]
    this.obj = {a: '1'}
    var test = 1
    this.sayName = function(){
        console.log(this.name);
    }
}
Foo.prototype.friend = "Nic"

function Bar(age) {
    Foo.call(this);
}

Bar.prototype = new Foo();

var f1 = new Bar();
console.log(f1.attr)      //Jack
console.log(f1.friend)    //Nic
f1.color.push("block")
var f2 = new Bar()
console.log(f1.color)      //[ 'red', 'blue', 'block' ]
console.log(f2.color)     //[ 'red', 'blue' ]

可以看到Bar的原型属性被重写,赋值了一个Foo的实例对象,实现原型链赋值。Bar函数内部又通过call方法实现了将Foo的私有属性进行继承。

现在我们把f1输出出来查看一下:

alt

f1从Bar中继承到所有的私有属性(attr,color, obj, sayName),同时__proto__对象中也包含Foo的私有属性以及指向Foo原型的f1.__proto__.__proto__指针。

来看一下在内存中的存储情况:

alt

好了,现在你输出一下这条语句:

console.log(f1.constructor)  //Foo

有没有发现虽然我们实例化Bar产生了f1,但是f1的构造函数依然指向Foo,这个检索原理如前面所述,f1会先在私有属性中查找constructor,然鹅,并木有找到,继续向上追溯到f1.__proto__(也就是Bar.prototype,也就是instance虚拟实例),instance虚拟实例并没有constructor私有属性,于是追溯到f1.__proto__.__proto__(也就是instance.__proto__),在f1.__proto__.__proto__找到constructor,这个constructor指向Foo构造函数。

现在我们对刚才的代码再增加一行赋值:

Bar.prototype.constructor = Bar;
console.log(f1.constructor)  //Bar

由于我们刚才使用Foo的实例重写了prototype,实例instance对象中不含有私有属性constructor,现在我们再来捋一遍,f1会先在私有属性中查找constructor,然鹅,并木有找到,继续向上追溯到f1.__proto__(也就是Bar.prototype,也就是instance虚拟实例),发现有诶,并且constructor指向Bar,于是就不在向上追溯了,就输出Bar。

如下图中蓝色箭头:

alt

ES5原型式继承

function Foo(){
    this.attr = 'Jack';
    this.color = ["red", "blue"]
    this.obj = {a: '1'}
    this.sayName = function(){
        console.log(this.name);
    }
}
Foo.prototype.friend = "Nic"


function Bar() {
    Foo.call(this)
}

Bar.prototype = Object.create(Foo.prototype, {
    constructor: {
        value: Bar,
        enumerable: true,
        writable: true,
        configurable: true
    }
})

var f1 = new Bar();
console.log(f1)

如上面的例子,我们也可以使用Object.create的方式来创建对象来直接改写Bar的prototype属性。这样做的好处,首先省去对Bar.prototype.constructor单独重新定义的过程,其次避免原型链上产生Foo构造函数中的私有属性。 现在我们看一下f1中的内容:

alt

可以看到f1.__proto__属性中不再包含从Foo函数中继承来的私有属性了。这样就实现了一个完美的继承。

以上是ES5中实现继承的几种方式的总结,如果你对构造函数和实例的关系还不是特别清楚,可以先读一下这两篇文章啦~~

聊聊JS与内存(一)

聊聊JS与内存(二)

本文链接:https://www.imwineki.cn/post/jsextend.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。