【深入JS】理解__proto__ 和 prototype 是什么关系

530 阅读3分钟

我们看一段代码,思考输出。

let obj = {}
console.log(obj.__proto__) // ??
首先了解proto和prototype是什么

prototype是原型对象。在js中每个对象都有原型。但是不是所有的对象都有prototype属性。一般来说只有函数拥有prototype属性,这种说法其实不太准确。JS内置的对象 ObjectFunction等也都有prototype属性。prototype的值是一个object对象,默认包含的Construct属性(构造函数)和__proto__属性,原型对象上的__proto__指向他自己的原型。

__proto__指向执行构造函数的原型,是浏览器厂商实现的一个访问器属性,为了更好的访问原型链。在ES6中被收纳进标准属性。它指向的是构造函数的原型对象。在一些 情况下(比如辣鸡IE不兼容ES6),可以使用 Object.getPrototypeOf 和 Object.setPrototypeOf 来进行对原型对象的设置或读取。

Object.getPrototypeOf(obj) === obj.__proto__; //true

原型链与继承

我们知道了一个对象的__proto__属性指向构造函数的原型对象,而这个原型对象中又包含自己的原型。这样层层指向,构成了原型链。例如

Object.prototype === Function.prototype.__proto__ // true

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

js的继承通过原型得以实现。当我们希望一个属性可以一直被继承,大可以在构造函数的原型上定义。

那么继承是在那一步完成的呢?以开头的代码为例子。 第一行使用字面量的方式实例化了a对象,其实本质上和使用new关键字+Object构造函数实例化没有区别。

  1. 生成一个对象。
  2. 将这个对象作为作为this传递进构造函数。
  3. 将这个对象的proto指向构造函数的prototype,并将使用这个对象进行赋值操作。如果构造函数返回了一个对象,使用返回的对象进行前面的步骤。 所以回到开头的问题,打印出来的应该是Object的prototype属性。

__proto__只是对构造函数原型对象的引用!

要注意的一个细节是,__proto__只对构造函数原型对象的引用。换句话说就是改变构造函数的prototype,原型链下层的对象会受到影响。看下面的例子

let Func = function () {}
let a = new Func
a.toString()//"[object Object]"
Func.prototype.toString = () => "2"
a.toString()// "2"

两侧调用a.toString()方法得到不同的结果,因为在中间对原型对象增加了属性。第一次:a本身没有toString属性,在构造函数的Func的原型中也没有,沿着原型一直往上走,最后其实是Object的原型方法。而第二次直接就在Func的原型中就找到了对应属性。其实Func作为一个对象,在构造函数Function进行初始化时,在对象本身的属性上挂载了toString方法的,但是并不在原型链上,无法被访问到。这部分涉及到关于实例化方式和this的指向问题。在另一篇文章中会进行详细地阐述。

原型链的末端是null(js内置对象除外)

原型链的末端是null。

a.__proto__.__proto__ === null; //true
Object.protoType.__proto__ === null; //true

关系图附上

img