深刻理解JavaScript原型链与继承

原型链

原型链一直都是一个在JS中比较让人费解的知识点,可是在面试中常常会被问到,这里我来作一个总结吧,首先引入一个关系图:面试

一.要理解原型链,首先能够从上图开始入手,图中有三个概念:数组

1.构造函数: JS中全部函数均可以做为构造函数,前提是被new操做符操做;app

function Parent(){
    this.name = 'parent';
}
//这是一个JS函数

var parent1 = new Parent()
//这里函数被new操做符操做了,因此咱们称Parent为一个构造函数;
复制代码

2.实例: parent1 接收了new Parent(),parent1能够称之为实例;函数

3.原型对象: 构造函数有一个prototype属性,这个属性会初始化一个原型对象;优化

二.弄清楚了这三个概念,下面咱们来讲说这三个概念的关系(参考上图):this

1.经过new操做符做用于JS函数,那么就获得了一个实例;spa

2.构造函数会初始化一个prototype,这个prototype会初始化一个原型对象,那么原型对象是怎么知道本身是被哪一个函数初始化的呢?原来原型对象会有一个constructor属性,这个属性指向了构造函数;prototype

3.那么关键来了实例对象是怎么和原型对象关联起来的呢?原来实例对象会有一个__proto__属性,这个属性指向了该实例对象的构造函数对应的原型对象;code

4.假如咱们从一个对象中去找一个属性name,若是在当前对象中没有找到,那么会经过__proto__属性一直往上找,直到找到Object对象尚未找到name属性,才证实这个属性name是不存在,不然只要找到了,那么这个属性就是存在的,从这里能够看出JS对象和上级的关系就像一条链条同样,这个称之为原型链;cdn

5.若是看到这里还没理解原型链,能够从下面我要说到继承来理解,由于原型继承就是基于原型链;

三.new操做符的工做原理

废话很少说,直接上代码
var newObj = function(func){
    var t = {}
    t.prototype = func.prototype
    var o = t
    var k =func.call(o);
    if(typeof k === 'object'){
        return k;
    }else{
        return o;
    }
}
var parent1 = newObj(Parent)等价于new操做

1.一个新对象被建立,它继承自func.prototype。
2.构造函数func 被执行,执行的时候,相应的参数会被传入,同时上下文(this) 会被指定为这个新实例。
3.若是构造函数返回了一个新对象,那么这个对象会取代整个new出来的结果,若是构造函数没有返回对象,
那么new出来的结果为步骤1建立的对象。
复制代码

继承

一.构造函数实现继承(构造继承)

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//undefined

//如下代码看完继承方式2,再回过头来看
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

从上面构造继承的代码能够看出,构造继承实现了继承,
打印出来父级的name属性,可是实例对象并无访问到父级原型上面到属性;
复制代码

二.原型链实现继承

function Parent(){
    this.name = 'parent'
    this.play = [1,2,3]
}
function Child(){
    this.type = 'child';
}
Child.prototype = new Parent();
Parent.prototype.id = '1';
var child1 = new Child();    
console.log(child1.name)//parent1
console.log(child1.id)//1

从这里能够看出,原型继承弥补了构造继承到缺点,继承了原型上到属性;
可是下面再作一个操做:
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[2,2,3]
这里我只是改变了实例对象child1到play数组,可是实例打印实例对象child2到paly数组,发现也跟着变化
了,因此能够得出结论,原型链继承引用类型到属性,在全部实例对象上面改变该属性,全部实例对象该属性都会
变化,这样确定就存在问题,如今咱们回到继承方式1(构造继承),会发现构造继承不会存在这个问题,因此
其实构造继承和原型链继承彻底能够互补,由此咱们引入第三种继承方式;

额外解释:这里经过一个原型链继承,咱们再来回顾一下对原型链的理解,上面代码,咱们进行了一个操做:
Child.prototype = new Parent();
这个操做把父类的实例赋值给子类的原型,而后结合上面原型链的关系图,咱们再来理一下(为了阅读方便,复
制上图到此处):
复制代码

如今咱们能够把图中到实例当作child1,首先若是要找child1实例对象中的name属性,那么我首先到Child自己去找,发现没有找到name属性,由于Child函数里面只有一个type属性,那么经过__proto__找到Child的原型对象,而刚才咱们作了一个操做:

Child.prototype = new Parent(); 这个操做把父类的实例给了Child的原型,因此经过这个咱们就能够找到父级的name,这就是原型链,一层一层的,像一个链条;

三.组合继承

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Child.prototype = new Parent();
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//1
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

从上面代码能够看出,组合继承就是把构造继承和原型链继承组合在一块儿,把他们的优点互补,从而弥补了各自的
缺点;那么组合继承就完美了吗?咱们继续思考,从代码中能够发现,咱们调用了两次Parent函数,一次是
new Parent(),一次是Parent.call(this),是否能够优化呢?咱们引入第四种继承方式;
复制代码

四.组合继承(优化1)

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Child.prototype = Parent.prototype;//这里改变了
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//1
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

咱们改为Child.prototype = Parent.prototype,这样就只调用一次Parent了,解决了继承方式3的问题,
好吧,咱们继续思考,这样就没有问题了吗,咱们作以下操做:
console.log(Child.prototype.constructor)//Parent
这里咱们打印发现Child的原型的构造器成了Parent,按照咱们的理解应该是Child,这就形成了构造器紊乱,
因此咱们引入第五种继承优化
复制代码

五.组合继承(优化2)

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child//这里改变了
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//1
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

如今咱们打印
console.log(Child.prototype.constructor)//Child
这里就解决了问题,可是咱们继续打印
console.log(Parent.prototype.constructor)//Child
发现父类的构造器也出现了紊乱,全部咱们经过一个中间值来解决这个问题,最终版本为:

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}

var obj = {};
obj.prototype = Parent.prototype;
Child.prototype = obj;
//上面三行代码也能够简化成Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child

console.log(Child.prototype.constructor)//Child
console.log(Parent.prototype.constructor)//Parent
用一个中间obj,完美解决了这个问题复制代码
相关文章
相关标签/搜索