ES規範解讀之賦值操作符&屬性訪問器

kuitos發表於2015-09-23

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操作看上去有這樣一些特性:

  1. 讀(get)操作會觸發原型鏈查詢,直譯器會從原型鏈一層層往上查詢,直到找不到返回undefined.

  2. 寫(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:

  1. Evaluate LeftHandSideExpression.

  2. Evaluate AssignmentExpression.

  3. Call GetValue(Result(2)).

  4. Call PutValue(Result(1), Result(3)).

  5. Return Result(3).

前面三步都知道,關鍵點在第四步, PutValue(Result(1), Result(3))
我們再來看看PutValue幹了啥

PutValue(V, W)

  1. If Type(V) is not Reference, throw a ReferenceError exception.

  2. Call GetBase(V).

  3. If Result(2) is null, go to step 6.

  4. 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:

  1. Evaluate MemberExpression.

  2. Call GetValue(Result(1)).

跟到GetValue

GetValue(V)

  1. If Type(V) is not Reference, return V.

  2. Call GetBase(V).

  3. If Result(2) is null, throw a ReferenceError exception.

  4. 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:

  1. If O doesn`t have a property with name P, go to step 4.

  2. Get the value of the property.

  3. Return Result(2).

  4. If the [[Prototype]] of O is null, return undefined.

  5. Call the [[Get]] method of [[Prototype]] with property name P.

  6. 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:

  1. Call the [[CanPut]] method of O with name P.

  2. If Result(1) is false, return.

  3. If O doesn`t have a property with name P, go to step 6.

  4. Set the value of the property to V. The attributes of the property are not changed.

  5. Return.

  6. Create a property with name P, set its value to V and give it empty attributes.

  7. Return.

很簡單,就是給物件o的屬性P賦值時,o存在屬性P就直接覆蓋,沒有就新建屬性。此時無關原型鏈。

此時再結合我們自己的案例來看,scopeInstance.array[1]=2scopeInstance.array=[]究竟都幹了啥(忽略不相關細節):

scopeInstance.array[1]=2

  1. GetBase(scopeInstance.array[1]) == scopeInstance.array

  2. GetValue(scopeInstance.array) => 觸發scopeInstace.array的[[Get]]方法,此時觸發原型鏈查詢 => 找到 Scope.prototype.array

  3. 設值操作 Scope.prototype.array.[Put];

scopeInstance.array=[]

  1. GetBase(scopeInstance.array) == scopeInstance

  2. GetValue(scopeInstance) => scopeInstance object

  3. 設值操作 scopeInstance.[Put];

完美解釋所有現象!

如果思考的比較深入的同學可能會問,scopeInstance又從哪兒取來的呢?也是類似原型鏈這樣一層層往上查出來的麼?這涉及到另一點知識,js中的作用域,具體可以看我的另一篇文章一道js面試題引發的思考

相關文章