JS的__proto__、prototype和[[prototype]]

序(碎碎念):本文是在整理《你不知道的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 ]]

image.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);

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


image.png

六、 总结

其实[[ 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容