ES規範解讀之賦值操作符&屬性訪問器
原文:https://github.com/kuitos/kuitos.github.io/issues/24
事情起源於某天某妹子同事在看angular文件中關於Scope的說明Understanding Scopes(原文) 理解angular作用域(譯文)時,對於文章中的例子有一點不理解,那個例子抽離細節之後大致是這樣的:
// 一個標準的建構函式
function Scope(){}
Scope.prototype.array = [1,2,3];
Scope.prototype.string = `Scope`;
// 生成Scope例項
var scopeInstance = new Scope();
當我們訪問scopeInstance上的屬性時,假如scopeInstance上不存在該屬性,則js直譯器會從原型鏈上一層層往上找,直到找到有該屬性,否則返回undefined。
// get物件上某一屬性時會觸發原型鏈查詢
console.log(scopeInstance.string); // `Scope`
console.log(scopeInstance.name); // undefined
而當我們往scopeInstance上某一屬性設值時,它並不會觸發原型鏈查詢,而是直接給物件自身設值,如果物件上沒有該屬性則建立一個該屬性。
scopeInstance.string = `scopeInstance`;
scopeInstance.array = [];
console.log(scopeInstance.string); // `scopeInstance`
console.log(scopeInstance.array); // []
console.log(Scope.prototype.string); // `Scope`
console.log(Scope.prototype.array); // [1,2,3]
總結起來,關於物件的屬性的set和get操作看上去有這樣一些特性:
-
讀(get)操作會觸發原型鏈查詢,直譯器會從原型鏈一層層往上查詢,直到找不到返回undefined.
-
寫(set)操作不會觸發原型鏈查詢,寫操作會直接在物件上進行,沒有這個屬性會新建一個屬性。
沒錯,這是最基本的原型鏈機制,我以前一直是這麼理解的,然後我也是這麼跟妹子解釋的,然而文章後面的例子打了我臉。。。例子大致是這樣的:
var scope2 = new Scope();
scope2.array[1] = 1;
console.log(scope2.array); // [1,1,3]
console.log(Scope.prototype.array); // [1,1,3]
WTF!!!
按照我的理解,寫操作跟原型鏈無關,在物件自身操作。
順著這個思路,那麼 scope2.array[1]=1
這行程式碼壓根就會報錯啊,因為scope2在建立array屬性之前壓根就沒有自身的array屬性啊!可是它竟然沒報錯還把Scope.prototype給改了!
於是我又在想,是不是這種引用型別(array,object)都會觸發原型鏈查詢,所以會出現這個結果?
然而我又想起前面那段程式碼:
scopeInstance.array = [];
console.log(scopeInstance.array); // []
console.log(Scope.prototype.array); // [1,2,3]
這下徹底斯巴達了?
從表象來看,scopeInstance.array[1]
的讀寫操作都會觸發原型鏈查詢,而為啥scopeInstance.array
的寫操作就不會觸發。如果說引用型別都會觸發,那麼scopeInstace.array=[]
就等價於Scope.prototype.array = []
,但是事實並不是這樣。。。
碰到這種時候我只有祭出神器了(ecmascript),google什麼的絕對不好使相信我。
翻到ecmascript關於賦值操作符那一小節,es是這樣描述的
Simple Assignment (= )
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:
Evaluate LeftHandSideExpression.
Evaluate AssignmentExpression.
Call GetValue(Result(2)).
Call PutValue(Result(1), Result(3)).
Return Result(3).
前面三步都知道,關鍵點在第四步, PutValue(Result(1), Result(3))
我們再來看看PutValue幹了啥
PutValue(V, W)
If Type(V) is not Reference, throw a ReferenceError exception.
Call GetBase(V).
If Result(2) is null, go to step 6.
Call the [[Put]] method of Result(2), passing GetPropertyName(V) for the property name and W for the value.
…
第二步有一個GetBase(V)
操作,然後第四步依賴第二步的計算結果做最終賦值。
那麼GetBase(V)
究竟做了什麼呢(V即我們賦值操作時候的左值)
GetBase(V)
GetBase(V). Returns the base object component of the reference V.
翻譯下來就是:返回引用V的基礎物件元件。
那麼什麼是基礎物件元件
呢,舉兩個例子:
GetBase(this.array) => this
GetBase(this.info.name) => this.info
GetBase(this.array[1]) => this.array
我們再來看看屬性訪問器(Property Accessors),就是括號[]
操作符及點號.
操作符都做了什麼
屬性訪問器(Property Accessors)
MemberExpression . Identifier is identical in its behaviour to MemberExpression [ <identifier-string> ]
也就是說括號跟點號對直譯器而言是一樣的。
The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:
Evaluate MemberExpression.
Call GetValue(Result(1)).
…
跟到GetValue
GetValue(V)
If Type(V) is not Reference, return V.
Call GetBase(V).
If Result(2) is null, throw a ReferenceError exception.
Call the [[Get]] method of Result(2), passing GetPropertyName( V) for the property name.
第四步的私有方法[[Get]]是關鍵:
[[Get]]
When the [[Get]] method of O is called with property name P, the following steps are taken:
If O doesn`t have a property with name P, go to step 4.
Get the value of the property.
Return Result(2).
If the [[Prototype]] of O is null, return undefined.
Call the [[Get]] method of [[Prototype]] with property name P.
Return Result(5).
意思很明顯,[[Get]]
會觸發原型鏈查詢.
我們再回到賦值操作符的PutValue
操作,走到第四步
Call the [[Put]] method of Result(2), passing GetPropertyName(V) for the property name and W for the value.
這裡的Result(2)
就是GetBase(V)
的結果,拿上面的例子也就是GetBase(this.array[2]) == this.array
再看看[[Put]]
操作幹了什麼事情:
[[Put]]
When the [[Put]] method of O is called with property P and value V, the following steps are taken:
Call the [[CanPut]] method of O with name P.
If Result(1) is false, return.
If O doesn`t have a property with name P, go to step 6.
Set the value of the property to V. The attributes of the property are not changed.
Return.
Create a property with name P, set its value to V and give it empty attributes.
Return.
很簡單,就是給物件o的屬性P賦值時,o存在屬性P就直接覆蓋,沒有就新建屬性。此時無關原型鏈。
此時再結合我們自己的案例來看,scopeInstance.array[1]=2
跟scopeInstance.array=[]
究竟都幹了啥(忽略不相關細節):
scopeInstance.array[1]=2
-
GetBase(scopeInstance.array[1]) == scopeInstance.array
-
GetValue(scopeInstance.array) => 觸發scopeInstace.array的[[Get]]方法,此時觸發原型鏈查詢 => 找到 Scope.prototype.array
-
設值操作 Scope.prototype.array.[Put];
scopeInstance.array=[]
-
GetBase(scopeInstance.array) == scopeInstance
-
GetValue(scopeInstance) => scopeInstance object
-
設值操作 scopeInstance.[Put];
完美解釋所有現象!
如果思考的比較深入的同學可能會問,scopeInstance又從哪兒取來的呢?也是類似原型鏈這樣一層層往上查出來的麼?這涉及到另一點知識,js中的作用域,具體可以看我的另一篇文章一道js面試題引發的思考