从prototype和__proto__走进JavaScript原型链的世界

377 阅读4分钟

最近在学习JavaScript基础的时候,遇到了这样的一个问题:

var obj = {}      /*也可以用完整语法var obj = new Object()*/
obj.toString()

上述代码创建了一个名为obj的空白对象,调用obj的toString方法,得到如下图的结果。

25924c17413e03b8041813c81ece630.png

obj对象不是什么内容也没有吗?为什么调用一个不存在的toString方法却不会报错呢?因为在创建obj对象时,js引擎偷偷地帮我们给obj对象加了一个隐藏属性__proto__,__proto__指向了内存中的某个地址,这个地址存放了toString方法。为了了解__proto__是怎么运作的,我们要先了解JavaScript的代码在内存中是怎么如何存放的以及JavaScript的继承机制。

前置知识

JavaScript内存分布

总所周知代码要放到内存里运行,JavaScript也不例外。而代码是分开存放的,变量名存在一个特殊的地方,姑且称之为“变量领域”,普通的数值、地址等非对象类型存放在Stack上,对象类型存放在Heap上。例如:var a=1,a是变量名,存放在变量领域,1是普通的数值,存放在Stack;又例如:var arr=[1,2,3],arr是对象名,也是变量,存放在变量领域,arr[1,2,3]这个数组对象存放在Heap上。我们可以简单地理解为非对象存放在内存中的Stack区,对象存放在Heap区。

1a2cd0f9926847fce5a2e3848551eb3.jpg 如上图Stack区存放了一个地址为401,指向Heap区的401地址,存放了arr对象的代码。

JavaScript的继承机制

为了能减少代码量,提高代码的重用率,JavaScript设计了继承机制。但为了让JavaSript学起来更容易,更轻量化,JavaScript的设计者并没有采用跟Java类似的继承机制,没有类和实例的概念,而是用构造函数和原型对象相结合的方式实现。每个对象都有其原型对象,这些原型对象中封装了其子代的共有属性,在new一个新的对象时,根据程序员编写的构造函数来设定对象的独有属性和方法,同时JS引擎会给这个新的对象暗中增加一个隐藏属性,这个隐藏属性指向了该对象原型共有的属性和方法的代码在内存中的存放地址,当用户调用对象的方法时,首先检查对象本身是否具有这个方法,如果没有则通过检查隐藏属性,找到其对象原型的代码,调用原型中的方法。

prototype和__proto__

上面提到的隐藏属性,其实指的就是prototype和__proto__,只不过区别在于prototype只存在于函数中(也就是大写字母开头的),__proto__存在于对象中。原型通过prototype指向共有的属性和方法,对象通过设置自己的__proto__指向其原型的prototype来得到使用其原型共有属性方法的途径,实现继承效果。

什么叫原型链

顾名思义这种基于原型的继承机制的继承深度不止一层,而是链式的。比如,var a = [1,2,3] a是一个普通的对象,它是根据Array函数的构造方法构造的,a.proto===Array.prototype,原型是Array函数。前面提到了__proto__存在于对象中,然而函数也是一种对象,Array函数自然也是对象,它除了具有函数特有的prototype属性之外,其实还有__proto__属性,指向对象的原型Object的prototype,Array.proto===Object.prototype。所以JavaScript就是通过这样__proto__和prototype的相互配合,实现了链式的继承,数组对象a甚至可以调用祖先Object函数prototype指定的共有的属性和方法。

原型链中有趣的问题

上面提到因为函数也是对象,所以Array函数的原型是Object,其实函数除了有原型Object之外,所有的函数也有另一个原型Function函数

b3ff62233cacc2a8a3ff8ee9161ab1f.png

这时一个有趣的问题出现了,总所周知所有的函数都是对象,又因为对象的原型Object是一个构造函数,它的原型是Function函数,到底是先有鸡还是先有蛋?Function和Object谁才是原型链的顶端?我们去查阅一下资料:

Object

Function

根据ECMA的规范,Object.prototype是原型链的顶端,所有对象均从Object.prototype继承属性。一开始,我以为万物都要先有构造器,其他对象根据构造器生成,所以先有Object和Function函数,再生成Object.prototype和Fcuntion.prototype。事实上,在JavaScript的世界中,Object.prototype最先诞生,随后得到了Function.prototype,后来再以Object.prototype和Function.prototype为原型诞生了Function和Object(Array等其他构造函数同理),Object.prototype和Function.prototype虽然都是对象,但并不是根据构造函数生成的。

由此可得,原型链的结构其实是这样的

Object.prototype<---Function.prototype<---Function|Object|Array...<---实例对象