JS的__proto__、prototype和[[prototype]]

Yukino 574 2021-12-29

序(碎碎念):本文是在整理《你不知道的JS(上)》第五章时写下的,先是把整本书过了一遍,其中有些细节并未深究,待整理时再看一遍,前面几章都顺利的写出来了,一天整理一章(其中跳过了第4章,究其原因我目前没觉得混入有什么使用场景),并没有遇到什么大问题,但在整理这一章的时候,我发现书中的信息似乎并不清楚,包括上一章中补充的new的过程(摘自MDN)也有些许模糊不清,所以这一篇花了我两天时间去查阅其他资料整理,但内容和书中可能有些许差别,故不称为读书笔记了

一、这仨玩意是啥?

首先先说说[[ prototype ]],这是JS中对象一个特有的用于与其他对象共享属性的内置属性(ECMAScript® 2021 Object prototype),在浏览器中的实现大多以__proto__命名;最后是prototype,prototypeECMAScript® 2021 Function Instance prototype是指函数中内置的“原型”对象,在这个函数作为构造函数(以new的方式调用)时,新创建的对象的[[ prototype ]]/__proto__就会指向这个函数的prototype对象。

我们知道JS中函数也是对象,那么函数的[[ prototype ]]又指向哪个对象呢,答案是Function.prototype,然后Function.prototype的[[ prototype ]]指向Object.prototype,最后Object.prototype的[[ prototype ]]指向一个空对象。

来看一段代码:

function foo() {};
foo.prototype.a = 2;
let obj = new foo();
console.log(obj.a);	// 2
console.log(obj.__proto__ === foo.prototype); // true

上面的obj.__proto__就是obj对象的[[ prototype ]]内置属性,而foo.prototype就是foo函数的“原型”对象,大致的关系可以看下面的图。
此后用__proto__代指[[ prototype ]]
20450286848b188498ec9407.png
相信你已经注意到了foo.prototype中有一个constructor属性,它指向的是foo函数,这个属性是在声明函数时产生的默认属性,若之后将函数的prototype替换掉,那么它不会自动的获取constructor属性,看下面的例子:

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新的原型对象
var a1 = new Foo();
a1.constructor === Foo; // false! 
a1.constructor === Object; // true!

二、 这玩意是干啥用的?

上篇读书笔记结尾有提到原型链,其实prototype翻译过来的意思就是原型的意思,作用上一节其实也有提到,为了共享部分属性给其他对象。

在第一个例子中,我们并没有给obj添加a的属性,但我们打印obj.a仍然得到了一个非undefined的输出,并且值与foo.prototatype.a的相等,实际上,当你试图引用对象的属性时会触发[[ Get ]]操作,第一步会检查对象本身是否拥有这个属性,如果有的话就使用它,如果没有,就会查找对象原型(链)上有没有。

而它的应用场景,很简单的一个问题,一个字符串是如何调用substring方法的?首先JS引擎会将字符串字面量包装成一个String对象,然后调用substring方法,那String对象上为什么会有substring方法呢?要知道JS中是没有类的,这当中就是原型链在起作用,String对象的__proto__指向了String内置对象,在String内置对象中有一系列字符串相关的函数,所以在调用substring时,实际上调用的是内置对象的substring,来看看下面的代码:

let str = new String("wdnmd");
console.log(str)
/**
0: "w"1: "d"2: "n"3: "m"4: "d"length: 5__proto__: String*/

__proto__里的内容想看的话就自己去浏览器控制台打印看看吧,其实就是那些内置的函数。

三、 属性设置和屏蔽

此部分截取自《你不知道的JS(上)》
考虑以下代码:

obj.p = "param";

我想看到这的你应该不会只是简单的回答“先检查obj中有没有p,有就将它的值更新为‘param’,没有就给obj添加一个属性p,并将它的值设置为‘param’”,考虑到原型链,应该考虑以下3种情况:
1. obj和obj的原型链上都没有p属性,那么会p会直接添加到obj上,并设置为“param”;
2. obj上和obj原型链上都有p属性,此时就会发生屏蔽,obj上的p会屏蔽掉原型链上所有的p,因为obj.p总会选择原型链上最底层的p属性;
3. 若obj上没有,原型链上有,这时的情况就有些复杂了,分为以下三种情况:

  1. 若原型链上的p没有被标记为只读(writable为false),那么就会在obj上直接添加一个p的屏蔽属性,并且值设定为“param”;
  2. 若原型链上的p被标记为只读,那么无法修改已有属性或者在obj上创建屏蔽属性,这条语句会被忽略,如果在严格模式下,会抛出一个错误;
  3. 若原型链有p并存在p的setter,那么就一定会调用这个setter,p不会被添加到obj中,也不会重新定义p这个setter。
    若你想在第二种和第三种情况下也能屏蔽,那你应该使用Object.defineProperty()来赋值。
    考虑以下代码:
var anotherObject = { 
	a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2 
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true 
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隐式屏蔽! 
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

出现这种情况的原因是myObject.a++相当与myObject.a = myObject.a + 1,因此会先获取到antherObject上的a,将其加一之后赋值给myObject新增的屏蔽属性a。
再来看一段代码:

let obj = {
    myObj: {
        a: 1
    }
};
let antherObj = {
	__proto__ : obj
};
console.log(antherObj.myObj);	// {a: 1}
antherObj.myObj.a = 2;
console.log(antherObj.myObj);	// {a: 2}
console.log(obj.myObj);			// {a: 2}
console.log(antherObj.hasOwnProperty("myObj"));	// false

上面的结果和发生的原因也许你已经知道,我也就直接说结论了,在JS中,绝大部分对象的原型链上都是有Object.prototype的,其中就有默认的setter和getter方法,此时就会发生前面说的第三种情况,调用对象属性的setter方法,而不会被添加到antherObj中。

这个例子可能有些“荒谬”,毕竟正常情况下直接在没有myObj属性的antherObj上添加a就是不可能的,会抛出TypeErorr,这个例子主要是想说明对于所有的非基本(原始)类型,都是拥有setter方法的。

四、 new发生了什么?

又是碎碎念时间:讲到这可能关于[[ prototype ]]的基本的知识都已经说完了,但接下来讲的东西,是书中最后一节对象关联中提到的“Object.create(…) 会创建一个新的对象(bar)并把它关联到我们指定的对象(foo),这样我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使用 new 的构造函数调用会生成 .prototype 和 .constructor引用)”,也正是这一句话,让我不禁好奇,new发生了什么?而Object.create()又干了些什么?所以我决定还是将这些放在这篇文章中。
上一篇读书笔记中其实有一段对new的过程的补充,摘自MDN里对new关键字的解释:


  1. 创建一个空的简单JavaScript对象(即**{}**);
  2. 链接该对象(设置该对象的constructor)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this
    若返回的不是对象,则还是会返回this

1、3、4都没什么问题,但第2条在现在看来,问题很大,链接该对象的constructor到另一个对象?返回的对象里根本就没有constructor这个属性啊,但MDN还是比较权威的,可能是翻译出了点问题,切换到英文模式,好家伙,省掉了这么多,第2条原文:

Links (sets the constructor of) the newly created object to another object by setting 
the other object as its parent prototype;

我的理解是,通过设置the other object为新对象的父prototype,将新对象链接到the other object,而这个the other object就是指的Foo(构造时调用的函数),至于这个sets the constructor of,应该就是字面意思,当我们把obj的__proto__设置为Foo.prototype时,obj.constructor也就是Foo.prototype.constructor了。
最后总结一下,对于let obj = new Foo();,其具体步骤是:
1. 创建一个新的空对象,假定叫_this,也就是_this = {}
2. 将_this链接到Foo上,也就是_this.__proto__ = Foo.prototype
3. 将_this设置为this上下文调用Foo,也就是Foo.apply(_this, arguments);
4. 若Foo没有return或者return的不是一个对象,则返回_this。

五、 Object.create()又是啥?

**Object.create()**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__,如:let obj = Object.create(Foo),实际上就是创建了一个新的对象,其__proto__指向Foo对象。

要理解这个函数也十分简单,如果你看懂了前面的new的原理的话,MDN给出了一个Object.create()的polyfill(去除了部分的检验):

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        function F() {}
        F.prototype = proto;

        return new F();
    };
}

其实就是创建一个新的空的F函数,将其prototype指向给定对象,然后返回new F()。
来看看下面的代码:

let foo = function() {
  
};

let obj = Object.create(foo);
let newObj = new foo();

console.log(obj);
console.log(newObj);

前面讲的已经十分详细了,下面直接给出关系图帮助理解:
20450286-b1f291f5e34d3efc

六、 总结

其实[[ prototype ]]这个属性在工作中用的多不多?关不关键?我并不清楚,我本想把我看到的一个面试题转过来的,但我看到评论区,“这是八股文嘛?”、“前端早已经变天了”之类的言论,便打消了这个想法,就放一个链接吧https://zhuanlan.zhihu.com/p/334365485,我觉得我花的这些时间是有价值的,不是有了框架就不用去关注语言本身的特性了,框架也是这些语言写出来的,哪怕你有了ts,但是编译之后它还是js…写到这其实这篇文章已经花了我五六个小时了,就单纯写的时间,中途打断了两次——吃饭和回家,当然打断的时间并没有算在里面,我也不想去纠结这些东西,沉的下去才能在跃出水面时飞的更高。
感谢你能耐心看完,若是有什么想法欢迎留言,若是有错误,欢迎指出。

参考文章:Object.create() - JavaScript | MDNnew 运算符 - JavaScript | MDN__ proto __和prototype之区别和联系_个人文章 - SegmentFault 思否JavaScript. The Core: 2nd Edition – Dmitry SoshnikovECMAScript® 2021 Language SpecificationECMAScript® 2021 Language Specification


# 前端 # JS