摘要:在使用的過程中,通過操作符為對象添加新屬性是很常見的操作。但是,這個操作的結(jié)果實際上會受到原型鏈上的同名屬性影響。通過它,可以做到操作符做不到的事情,比如為對象設置一個新屬性,即使它的原型鏈上已經(jīng)有一個的同名屬性。
在使用JavaScript的過程中,通過"="操作符為對象添加新屬性是很常見的操作:obj.newProp = "value";。但是,這個操作的結(jié)果實際上會受到原型鏈上的同名屬性影響。接下來我們分類討論。
以下討論都假設對象自身原本不存在要賦值的屬性(故稱:“為對象添加新屬性”)。如果對象自身已經(jīng)存在這個屬性,那么這是最簡單的情況,賦值行為由這個屬性的描述符(descriptor)來決定。如果原型鏈上不存在同名屬性,則直接在obj上創(chuàng)建新屬性
通過"="操作符賦值時,js引擎會沿著obj的原型鏈尋找同名屬性,如果最后到達原型鏈的尾端null還是沒有找到同名屬性,則直接在obj上創(chuàng)建新屬性。
const obj = {}; obj.newProp = "value";
這種情況非常符合人的直覺,所有js使用者應該都已經(jīng)熟悉了這種情況。但是事情并不是總是這么簡單。
如果原型鏈上存在由data descriptor定義的writable同名屬性,則直接在obj上創(chuàng)建新屬性沿著obj的原型鏈尋找同名屬性時,如果找到由data descriptor定義的同名屬性,且它的writable為true,則直接在obj上創(chuàng)建新屬性。
const proto = { newProp: "value" }; const obj = Object.create(proto); obj.newProp = "newValue";
結(jié)果:
這個情形也很常見,但是對于很多人來說可能不符合直覺:為什么通過obj.newProp能獲取到原型鏈上的newProp屬性,但是通過obj.newProp = "newValue"卻不能修改原型鏈上的屬性而是添加新屬性呢?
有2個解釋的理由:
原型鏈的作用是為對象提供默認值,即當對象自身不存在某屬性的時候,這個屬性應該表現(xiàn)出的默認值。為這個屬性賦值的時候,不應該通過“改變默認值”(修改原型鏈上的屬性)來做到,而應該通過創(chuàng)建一個新的值來掩蓋(shaow)默認值(默認值仍然存在,只是不再表現(xiàn)出來)。這樣做的一個好處是,你以后可以delete obj.newProp,然后obj.newProp就會再次表現(xiàn)出默認值。假設不采用這個方案,而是通過“改變默認值”,那么原來的默認值就會丟失,delete obj.newProp不會起作用(delete操作符只會刪除對象自身的屬性)。
多個對象可能共享同一個原型對象,如果對其中一個對象的屬性賦值就可以改變原型對象的屬性,那么"="操作符會變得非常危險,因為這會影響到共享這個原型的所有對象。
如果原型鏈上存在由data descriptor定義的non-writable同名屬性,則賦值失敗沿著obj的原型鏈尋找同名屬性時,如果找到由data descriptor定義的同名屬性,且它的writable為false,那么賦值操作失敗。在這種情況下,既不會修改原型鏈上的同名屬性,也不會為對象自身新建屬性。在"strict mode"模式下會拋出錯誤,否則靜默失敗。
"use strict"; const proto = Object.defineProperty({}, "newProp", { value: "value", writable: false }); const obj = Object.create(proto); obj.newProp = "newValue";為什么要這樣定義?
在參考資料3和4中給出了這樣定義的原因:為了使getter-only property(只定義了getter而沒定義setter的屬性)和non-writable property具有同樣的表現(xiàn):
const a = Object.defineProperty({}, "x", { value: 1, writable: false }); const b = Object.create(a); b.x = 2; // 賦值失敗
應該等價于
const a = { get x() { return 1; } }; const b = Object.create(a); b.x = 2; // 賦值失敗,這種情況會在下面討論到
因為原型鏈上的getter-only property會阻止子代對象通過"="操作符增加同名屬性(稍后會討論這種情況),所以原型鏈上的non-writable property也應該阻止子代對象通過"="操作符增加同名屬性。
此外,參考資料1還給出了一個原因,那就是為了模仿傳統(tǒng)類繼承語言的表現(xiàn)。JavaScript的繼承,從表面上看,應該像是“將父類的所有屬性都拷貝到了子類上”一樣。因此,父對象上的屬性(writable、non-writable)理應對子對象產(chǎn)生影響(如果子對象沒有覆蓋這個屬性的話)。
如果原型鏈上存在由accessor descriptor定義的同名屬性,則賦值操作由其中的setter定義沿著obj的原型鏈尋找同名屬性時,如果找到由accessor descriptor定義的同名屬性,則由這個accessor descriptor中的setter來決定做什么。setter將會被調(diào)用,this指向被賦值的對象obj(而不是setter所在的原型對象)。
如果這個accessor descriptor中只定義了getter而沒有setter,則賦值操作失敗,在"strict mode"模式下會拋出錯誤,否則靜默失敗。
const a = { get x() { return this._x; }, set x(v) { // 這里的this將指向b對象 this._x = v + 1; } }; const b = Object.create(a); b.x = 2; console.log(b.x); // 3 console.log(b.hasOwnProperty("_x")); // true,證明了setter中的this指向被賦值對象,而不是setter所在的原型對象
在上面的圖中需要注意一點,雖然在b對象下顯示了"x"屬性,但這個屬性實際是存在于b.__proto__上的(b.hasOwnProperty("x")將返回false),chrome的控制臺為了方便debug,將原型鏈上的getter屬性與對象自身的屬性放在一起展示。
為什么要這樣定義?為了增強“繼承”和“getter/setter”的威力。假如原型對象上的setter對后代對象的賦值無效、原型對象上的getter對后代對象的取值無效(也就意味著getter/setter不會被繼承),這將大大削弱getter/setter的作用。
另一方面,假如accessor descriptor定義的屬性不會被繼承,那么data descriptor定義的屬性應不應該被繼承?如果也不被繼承,那么JavaScript還怎么做到面向?qū)ο笳Z言最基本的“繼承”?如果data descriptor定義的屬性能夠被繼承,那么accessor descriptor與data descriptor的使用場景將出現(xiàn)巨大的割裂,程序員只能通過“屬性是否能被繼承”來決定是使用accessor descriptor還是data descriptor,這將大大削弱descriptor的靈活性。
此外,與前面一種情況同理,“模仿傳統(tǒng)類繼承語言的表現(xiàn)”也是一個重要的原因。
前面已經(jīng)對【通過"="操作符為對象添加新屬性】的3種情況進行了討論和解釋。接下來我們看看ECMAScript標準是如何正式地定義"="操作符的行為的。
AssignmentExpression:LeftHandSideExpression=AssignmentExpression表達式在運行時的求值算法:
說明:
abcd步驟,對于賦值表達式的左值取引用(相當于得到變量/屬性在內(nèi)存中的地址),對于右值求值。e步驟是為了處理func = function() {}這種函數(shù)表達式賦值的情況,本文不討論。f步驟中的PutValue(lref, rval)才是真正執(zhí)行賦值操作的算法。PutValue ( V, W )的算法定義:
其中第4步的作用是,對于屬性引用V,獲取V所在的對象(比如對于屬性引用a.b.c.prop,獲取到的對象是a.b.c)。本文討論的賦值情況會進入第6步的Elseif中。6.a是為了應對true.prop = 2134這種情況(這是合法的表達式?。辉诒疚挠懻?。6.b中的[[Set]]承擔賦值過程的主要操作。[[Set]]是ECMAScript為對象定義的13個基本內(nèi)部方法之一,普通對象對這些內(nèi)部方法的實現(xiàn)算法在這里,特異對象(比如數(shù)組)在普通對象的基礎(chǔ)上覆蓋某些基本內(nèi)部方法。在這里我們只看普通對象的[[Set]]算法:
可以看出,算法在2.b.i步驟做了遞歸:如果當前對象不存在這個屬性,則遞歸到父對象上找。參數(shù)O隨著每次遞歸而變化,指向當前遞歸查找到了哪個對象。而參數(shù)Receiver則不隨著遞歸而改變,始終指向最初被賦值的那個對象。
如果在原型鏈上找到了同名屬性,就會進入OrdinarySetWithOwnDescriptor的步驟3:
步驟3.a對應了前面討論的【如果原型鏈上存在由data descriptor定義的non-writable同名屬性,則賦值失敗】情況。
步驟3.e對應了前面討論的【如果原型鏈上存在由data descriptor定義的writable同名屬性,則直接在obj上創(chuàng)建新屬性】情況。
步驟6和7對應了前面討論的【如果原型鏈上存在由accessor descriptor定義的同名屬性,則賦值操作由其中的setter定義】情況。
至于步驟3.d,則對應了在文章開頭提到的【被賦值對象自身已經(jīng)存在賦值屬性】,屬于最簡單的情況。
如果在原型鏈上找不到同名屬性,會經(jīng)過步驟2.c.i,從而最終到達步驟3.e,在目標對象上創(chuàng)建新屬性,對應于前面討論的【如果原型鏈上不存在同名屬性,則直接在obj上創(chuàng)建新屬性】情況。
了解這些有什么好處?"="操作符賦值是JavaScript中最常見的操作之一,了解它的特殊性有助于更好地利用它、更好地利用“繼承”。
除此之外,你會驚訝地發(fā)現(xiàn),Proxy允許我們攔截的13個對象方法,恰好一一對應于ES標準為對象定義的13個基本內(nèi)部方法!而Reflect對象中提供的13個方法也與之一一對應!其實Reflect對象提供的13個方法就是普通對象的基本內(nèi)部方法的簡單封裝!
現(xiàn)在你應該能夠理解為什么,在我們通過Proxy攔截set操作的時候,執(zhí)行引擎會向我們暴露出剛剛談到的receiver。因為我們不僅僅會攔截到被代理對象(target)的賦值操作,并且,如果代理對象成為其他對象的原型,那么對其他對象(receiver)的賦值也會觸發(fā)代理對象的set操作。執(zhí)行引擎會將target和receiver都暴露給我們,從而我們能擁有最大的靈活度。
另一條路:Object.defineProperty()注意,我們在前面討論的時候一直強調(diào)"="操作符,這是因為,為對象添加、修改屬性還有另一種方法:Object.defineProperty()。這是比"="操作符更加強大、基礎(chǔ)的方法,它只對指定的對象進行屬性增加、修改,而不會影響到原型鏈上的對象或被原型鏈影響。通過它,可以做到"="操作符做不到的事情,比如:為對象設置一個新屬性,即使它的原型鏈上已經(jīng)有一個non-writable的同名屬性。
參考資料You Don"t Know JS
js 屬性設置與屏蔽
Property assignment and the prototype chain - 2ality
JS對象原型鏈上的同名屬性的writable為什么會影響到 對象本身的屬性呢? - 知乎
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/98868.html
摘要:一是如何工作的在上是這樣描述的運算符用于測試構(gòu)造函數(shù)的屬性是否出現(xiàn)在對象原型鏈中的任何位置換句話說,如果,那么必須是一個對象,而必須是一個合法的函數(shù)。下面我們舉一個例子一步步來說明第一步每一個構(gòu)造函數(shù)都有一個屬性。 在 JavaScript 中,我們通常用 typeof 判斷類型,但是在判斷引用類型的值時,常常會遇到一個問題:無論引用的是什么類型的對象,都會返回 object(當然還有...
摘要:對象詳解對象深度剖析,深度理解對象這算是醞釀很久的一篇文章了。用空構(gòu)造函數(shù)設置類名每個對象都共享相同屬性每個對象共享一個方法版本,省內(nèi)存。 js對象詳解(JavaScript對象深度剖析,深度理解js對象) 這算是醞釀很久的一篇文章了。 JavaScript作為一個基于對象(沒有類的概念)的語言,從入門到精通到放棄一直會被對象這個問題圍繞。 平時發(fā)的文章基本都是開發(fā)中遇到的問題和對...
之前也有和大家講過有關(guān)JS的對象創(chuàng)建和對象繼承,本篇文章主要為大家做個匯總和梳理。 JS中其實就是原型鏈繼承和構(gòu)造函數(shù)繼承的毛病,還有就是工廠、構(gòu)造、原型設計模式與JS繼承。 JS高級程序設計4:class繼承的重點,不只是簡簡單單的語法而已?! ο髣?chuàng)建 不難發(fā)現(xiàn),每一篇都離不開工廠、構(gòu)造、原型這3種設計模式中的至少其一! 那JS為什么非要用到這種3種設計模式了呢?? 我們先從對...
摘要:有個例外他就是??醋髠?cè)對象的原型鏈上是否有第一步得到。將各內(nèi)置引用類型的指向。用實例化出,,以及的行為并掛載。實例化內(nèi)置對象以及至此,所有內(nèi)置類型構(gòu)建完成。最后的最后,你還對是現(xiàn)有還是現(xiàn)有有想法了嗎以上均為個人查閱及實踐總結(jié)的觀點。 來個摸底測試,說出以下每個表達式的結(jié)果 function F(){}; var o = {}; typeof F; typeof o; typeof F...
摘要:什么是屏蔽屬性一條賦值語句引出的思考如果對象中包含名為的普通數(shù)據(jù)訪問屬性,這條賦值語句只會修改已有的屬性值。然而,如果存在于原型鏈上層,賦值語句的行為就會有些不同而且可能很出人意料。總之,不會發(fā)生屏蔽。 1.什么是屏蔽屬性 一條賦值語句引出的思考: myObject.foo = bar; 如果myObject 對象中包含名為foo 的普通數(shù)據(jù)訪問屬性,這條賦值語句只會修改已有的屬性值...
閱讀 2984·2021-09-22 15:18
閱讀 3401·2019-08-30 15:54
閱讀 3282·2019-08-30 15:53
閱讀 602·2019-08-30 14:12
閱讀 821·2019-08-29 17:01
閱讀 2209·2019-08-29 14:04
閱讀 1401·2019-08-29 13:09
閱讀 873·2019-08-26 17:40