深入理解JavaScript的原型链——关于prototype,__proto__,constructor那些你可能没有彻底搞懂的关系

说起原型链,戳进来看这篇博客的同学必然不会陌生,这是JavaScript中最核心的特性之一。那么,原型链到底是一个什么东西,它的工作原理是什么?这是本篇文章探讨的内容

对于原型链,我们可以这么理解,每一个JavaScript对象中,都存在一个内部属性,我们称之为[[prototype]](注意,此处的[[prototype]]属性并不是prototype属性,对于prototype属性,我们之后会提到),这个内部属性会指向另一个对象,而另一个对象也存在这样的一个属性,最终,在Object.propotype这个对象上面终结,至此,形成了我们所谓的原型链

原型链有这样一个特性:当访问当前对象的某个属性或者方法时,如果在当前对象访问不到,会沿着原型链一直往上走,直到找到对应的属性,找不到,则返回undefined。当我们对一个对象的属性进行写操作时,如果这个属性在原型链的高层存在,此时不会影响到高层的属性,只会作用在当前的对象上,这个特性,我们称之为遮蔽。例如下面的代码

var parent={
    name:1
};
var son=Object.create(parent);
console.log(son.name);//输出1
son.name=2;
console.log(son.name);//输出2
console.log(parent.name);//输出1

如上所示,Object.create构建了一个son对象,这个对象的[[prototype]]属性会指向parent,此时,虽然son并没有name属性,但是我们却能够获取得到son.name的值,而当我们修改son的name属性时,则会直接作用在son上面,形成一个遮蔽属性,并不会对parent产生影响

但问题在于,大多数人对于原型链的理解,就仅停留于此,并认为对于一个属性,如果这个属性已经存在于[[prototype]]链的高层的,那么对它进行赋值,将总是造成遮蔽,但事实上,这是错误的,这里面还有很多细节需要我们细细的品味

关于遮蔽

对于对象的遮蔽,事实上它存在以下三种情况

1、如果一个名为name的数据访问属性在[[prototype]]链的高层某处被找到,并且没有被标记为只读,那么对于名为name的属性的写操作将直接作用在当前对象上,并形成一个遮蔽属性。上面第一段代码举得例子可以印证我们这个结论

2、如果一个名为name的属性在[[prototype]]链的高层某处被找到,但它被标记为只读(writeable:false),那么设置属性name或者在当前对象上创建遮蔽属性,都是不被允许的,我们可以看下面的例子

var parent={
    name:1
};
Object.defineProperty(parent,'name', {writable: false});

var son=Object.create(parent);
son.name=2;
console.log(son.name);//非严格模式下输出1
console.log(parent.name);//非严格模式下输出1

通过上面的代码我们发现,对son对象的name属性进行写操作时,被无声的忽略了,不单单是parent的name无法被写入,甚至无法被遮蔽。事实上,如果在strict模式下,执行son.name=2时,会直接报错。所以不管在什么情况下,当[[prototype]]链的高层属性被设置为不可写状态时,遮蔽效果消失

3、如果一个名为name的属性在[[prototype]]链的高层被找到,但是它是一个setter,那么这个setter总是会被调用,没有新的name属性会被添加到当前对象上,也就是说,此时遮蔽效果也不会出现,我们来看下面的例子

var parent = {
    _name:'test'
};

Object.defineProperty(parent, 'name', {
    get: function () {
        return this._name
    }, set: function (value) {
        this._name+=value//传入的值与this._name相加
    }
});

var son = Object.create(parent);
son.name = '2';
console.log(son.name);//输出'test2'
console.log(parent.name);//输出'test'
console.log(son.hasOwnProperty('name'));//false

如果我们这个时候不调用hasOwnProperty验证一下的话,可能会以为name属性已经产生了遮蔽,但事实上并没有,之所以最终name的值有差异,是因为setter总是被调用,而this的指向不同罢了,这里需要注意的是,虽然name属性没有发生遮蔽,但是_name属性是发生了遮蔽的,这是产生差异的原因

关于原型链的获取

上文说到,原型链的基础是一个存在于对象内部的属性,我们称之为[[prototype]],既然是内部属性,那么意味着我们无法直接获取到它。但是es5提供了一个标准方法用来获取[[prototype]],正确的姿势如下

Object.getPrototypeOf(son)

鉴于[[prototype]]并不是只读的,所以我们也可以通过下面的方法来改变原型链的指向,但这是强烈不建议使用的。事实上我们一般不应该改变一个既存对象的[[prototype]]

Object.setPrototypeOf(son,parent2);

到了es6,es6将之前部分浏览器支持的__proto__属性标准化,现在在es6中我们可以直接用son.__proto__来访问[[prototype]],请看下面的代码,执行之后控制台输出的是true

console.log(Object.getPrototypeOf(son)==son.__proto__)//true

关于prototype属性

首先再次说明一下,prototype属性不等于[[prototype]],prototype属性仅存在于函数中,而[[prototype]]存在于所有的对象中,尽管二者偶尔会指向同一个地方,但却不是同一个东西。

MDN上有这么一段解释:

每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。几乎所有 JavaScript 中的对象都是位于原型链顶端的Object的实例。

我们可以这么理解这段话,所有对象的[[prototype]]都指向它的构造函数的prototype,而构造函数的prototype也是一个对象,也有原型链,因为所有的对象都是作为Object的实例由Object构造出来的,所以所有的原型链的实际上都会指向Object.prototype,最终,Object.prototype的[[prototype]]会指向null,最终结束。

所以我们得出本篇的第一个结论:函数的prototype属性可以理解为函数实例的公有属性,每一个函数(称之为Foo吧)被new之后,其实例的原型链都会指向Foo.prototype。并且,prototype仅存在于函数中,在普通对象中,并不存在prototype属性

穿插一点科普

在这里,我们顺便科普一下,所有的对象,都是Object函数的实例,这个很容易理解,从新建一个对象的方式就可以看出来

var myObj=new Object();

而所有的函数,则都是Function函数的实例,我们平时声明函数是这个样子的

function test(){
alert('test');
}

但事实上我们可以写成这个样子

var test=new Function('alert("test")');

两种写法的效果是一样的,但是,这么写只是为了解释为什么所有的函数都是Function的实例,实际开发过程中也是强烈不建议这么做,会造成维护方面的困难。

那么这就解释了为什么所有的对象最终都会指向Object.prototype。而Object本身也是一个函数,所以Object.__proto__事实上最终也指向了Function.prototype,但是Function.prototype本身又是一个对象,所以Function.prototype的原型链,也就是[[prototype]]最终也指向了Object.prototype,并在这里终结,指向了null,嗯,虽然有点乱,但是记住上文的结论,一步一步推导,也就不会乱了

关于constructor

接着,我们来看一下下面这段代码

function MyConstructor(){
    this.test=1;
}
var myObj=new MyConstructor();
console.log(myObj.constructor.name);//MyConstructor

从直觉来看,实例myObj存在存在了一个constructor属性,这个属性指向了它的构造函数,然而,打脸依旧来得如此之快,这是错的,了解一下以下两个输出

console.log(myObj.hasOwnProperty('constructor'));//false
console.log(MyConstructor.prototype.constructor==myObj.constructor);//true

我们发现,constructor事实上是存在于MyConstructor.prototype上的,并且这个值指向了MyConstructor自身,myObj这个实例本身是没有constructor这个属性的,它是从原型链上继承来的,这也是很多时候,我们替换掉构造函数的prototype时constructor会丢失的原因

这里我们得出结论:constructor指向实例的构造函数,并且存在于构造函数的prototype上

关于instanceof

instanceof 关键字左边接受一个普通对象,右边接受一个函数,它经常用来检查实例和构造函数的关系,例如

function MyConstructor(){
    this.test=1;
}
var myObj=new MyConstructor();
console.log(myObj instanceof MyConstructor);//true

但事实上,对于上述代码而言,instanceof所回答的问题是,在myobj的整个[[prototype]]链条中,是否出现过任何一个对象,指向了MyConstructor.prototype

我们看一下下面的代码

var a={};
var b=Object.create(a);
function MyConstructor(){
    this.test=1;
}
MyConstructor.prototype=a;
console.log(b instanceof MyConstructor);//true

并没有构造函数与实例的关系,但instanceof确实返回了true,所以,对于instanceof所做的事有正确的认识非常重要

总结

最后,附上一张其他博客作者写类似文章总结的图,个人觉得总结得比较好,贴出来方便大家对这几个概念的理解

如果上面的内容都理解了的话,个人觉得这张图应该不难看得懂,可能有几个点会比较难理解

1、关于Foo的Constructor,上文我们提到,Function函数时所有的函数的构造函数,所以Foo的Constructor指向Function没毛病,Foo.__proto__指向Function.prototype也就是理所当然的事

2、关于Function本身,还是由于Function是所有函数的构造函数,所以Function是它自己的构造函数,按照这么理解,Function的constructor指向它自身没毛病,根据原型链的规则,Function.__proto__也应该指向它的构造函数,也就是Function.prototype

3、关于Object,将它暂时理解为一个普通函数,则它的原型链和Constructor的指向和Foo的对比一下,其实非常清晰

参考资料:

《你不懂js:this与对象原型》

继承与原型链

https://blog.csdn.net/cc18868876837/article/details/81211729

猜你喜欢

转载自blog.csdn.net/xiaomingelv/article/details/92170314