1. 程式人生 > >寫了12年JS也未必全瞭解的連續賦值運算

寫了12年JS也未必全瞭解的連續賦值運算

引子

var a = {n:1}; 

var b = a; // 持有a,以回查 

a.x = a = {n:2}; 

alert(a.x);// --> undefined 

alert(b.x);// --> {n:2}

請問結果為何是這樣?

 

連等賦值的賦值順序

假設有一句程式碼: A=B=C; ,賦值語句的執行順序是從右至左,所以問題在於:

是猜想1: B = C; A = C; ?

還是猜想2: B = C; A = B;  ?

 

我們都知道若兩個物件同時指向一個物件,那麼對這個物件的修改是同步的,如:

var a={n:1};

var b=a;

a.n=2;

console.log(b);//Object {n: 2}

所以可以根據這個特性來測試連續賦值的順序。

 

按照猜想1,把C換成具體的物件,可以看到對a的修改不會同步到b上,因為在執行第一行和第二行時分別建立了兩個 {n:1} 物件。如:

var b={n:1};

var a={n:1};

a.n=0;

console.log(b);//Object {n: 1}

 

再按照猜想2,把C換成具體的物件,可以看到對a的修改同步到了b,因為a和b同時引用了一個物件,如:

var b={n:1};

var a=b;

a.n=0;

console.log(b);//Object {n: 0}

 

測試真正的連等賦值:

var a,b;

a=b={n:1};

a.n=0;

console.log(b);//Object {n: 0}

可以看到是符合猜想2的,如果有人覺得這個測試不準確可以再來測試,使用ECMA5的setter和getter特性來測試。

 

首先setter和getter是應用於變數名的,而不是變數真正儲存的物件,如下:

 

複製程式碼

Object.defineProperty(window,"obj",{

  get:function(){

    console.log("getter!!!");

  }

});

var x=obj;

obj;//getter!!! undefined

x;//undefined

複製程式碼

 

可以看到只有obj輸出了“getter!!!”,而x沒有輸出,用此特性來測試。

 

連等賦值測試2:

Object.defineProperty(window,"obj",{

  get:function(){

    console.log("getter!!!");

  }

});

a=b=obj;//getter!!!  undefined

image

 

通過getter再次證實,在A=B=C中,C只被讀取了一次。

所以,連等賦值真正的運算規則是  B = C; A = B;  即連續賦值是從右至左永遠只取等號右邊的表示式結果賦值到等號左側。

 

賦值表示式的右結合性

 

但是,你真正懂得右結合性是怎樣起作用的嗎?

看下面的連續賦值表示式:

exp1 = exp2 = exp3 = ... = expN;

其中的exp是一個表示式,並且除最後一個expN外,其他表示式都必須可以作為左值。

你能告訴我它是怎樣運算嗎?

 

是這樣的,首先根據賦值運算的右結合性,可以改寫成:

exp1 = (exp2 = (exp3 = (... = expN)...);

然後按照下面步驟進行運算:

解析exp1;

解析exp2;

解析exp3;

...

N. 解析expN;

 

以上步驟完成後,上面表示式變成了:

ref1 = (ref2 = (ref3 = (... = value)...);

其中value是表示式expN的值。接下來的步驟是:

將value賦給引用refN-1;

將value賦給引用refN-2;

...

N-1.將value賦給引用ref1;

結束。

 

特殊問題的坑

var a = {n:1}; 

var b = a; // 持有a,以回查 

a.x = a = {n:2}; 

alert(a.x);// --> undefined 

alert(b.x);// --> {n:2}

請問結果為何是這樣?

 

賦值是從右到左的,但不要被繞暈了, 其實很簡單,從運算子優先順序來考慮

 

a.x = a = {n:2};

.運算優先於=賦值運算,因此此處賦值可理解為

 

宣告a物件中的x屬性,用於賦值,此時b指向a,同時擁有未賦值的x屬性

對a物件賦值,此時變數名a改變指向到物件{n:2}

對步驟1中x屬性,也即a原指向物件的x屬性,也即b指向物件的x屬性賦值

賦值結果:

 

a => {n: 2}

b => {n: 1, x: {n: 2 } }   

 

詳細解答

var a = {n:1};

/*定義a,a賦值為`{n:1}`;

為a在記憶體堆中分配一塊記憶體用於儲存`{n:1}`,假設其地址為add_1;

此時add_1引用計數為1,即a,內容為`{n:1}`。*/

var b = a;

/*定義b,b賦值a,add_1被b引用。

此時add_1引用計數為2,即a和b,內容為`{n:1}`。*/ 

a.x = a = {n:2};

/*(`=`賦值運算子:關聯性為從右向左,優先順序為3。`.`成員訪問運算子:關聯性為從左向右,優先順序為19。19>3,所以先計算成員訪問運算子)

(1):a.x是成員訪問運算表示式,a.x中的x賦值為`a = {n:2}`的返回值`{n:2}`,add_1被改寫`{n:1,x:{n:2}}`。

此時add_1引用計數為2,即a、b,內容為`{n:1,x:{n:2}}`。

(2):a賦值為`{n:2}`;

為a在記憶體堆中分配一塊記憶體用於儲存`{n:2}`,假設其地址為add_2;

此時add_1引用計數為1,即b,內容為`{n:1,x:{n:2}}`。

此時add_2引用計數為1,即a,內容為`{n:2}`。*/

alert(a.x);

/*現在a的儲存地址add_2,內容為{n:2},上面並不存在a.x屬性,所以為undefined*/

alert(b.x);

/*現在b的儲存地址add_1,內容為{n:1,x:{n:2}},所以b.x為{n:2}*/