JavaScript 中的求值策略

小蘿蔔丁發表於2018-01-15

最近在研究 lambda演算 中的 η-變換 在 JavaScript 中的應用,偶然在 stackoverflow 上看到一個比較有意思的問題。關於 JavaScript 的求值策略,問JS中函式的引數傳遞是按值傳遞還是按引用傳遞?回答很經典。

一慄以蔽之

function changeStuff(a, b, c) {
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);         // 10
console.log(obj1.item);   // changed
console.log(obj2.item);   // unchanged
複製程式碼
  • 如果說JS中函式的引數傳遞是按值傳遞,那麼在函式changeStuff內部改變b.item的值將不會影響外部的obj1物件的值。
  • 如果說JS中函式的引數傳遞是按引入傳遞,那函式changeStuff內部所做的改變將會影響到函式外部所有的變數定義,num將會變成100、obj2.item將會變成changed。很顯然實際不是這樣子的。

所以不能說JS中函式的引數傳遞嚴格按值傳遞按引入傳遞。總的來說函式的引數都是按值傳遞的。JS中還採用一種引數傳遞策略,叫按共享傳遞。這要取決於引數的型別。

  • 如果引數是基本型別,那麼是按值傳遞的;
  • 如果引數是引用型別,那麼是按共享傳遞的。

引數傳遞

ECMAScript 中所有函式的引數都是按值傳遞的。也就是說,把函式外部的值複製給函式內部的引數,就和把值從一個變數複製到另一個變數一樣。基本型別值的傳遞如同基本型別變數的複製一樣,而引用型別值的傳遞,則如同引用型別變數的複製一樣。-- 《JavaScript高階程式設計》

紅寶書上講所有函式的引數都是按值傳遞的,到底是不是呢?讓我們分析下上面的栗子:

按值傳遞

JavaScript中基本型別作為引數的策略為 按值傳遞(call by value):

function foo(a) {
  a = a * 10;
}

var num = 10;

foo(num);

console.log(num); // 10 沒有變化
複製程式碼

這裡看到函式內部引數的改變並沒有影響到外部變數。按值傳遞沒錯。

按共享傳遞

JavaScript中物件作為引數傳遞的策略為 按共享傳遞(call by sharing):

  • 修改引數的屬性將會影響到外部物件
  • 重新賦值將不會影響到外部物件

按上面栗子函式內部修改了引數b的屬性item,會影響到函式外部物件,因而obj1的屬性item也變了。

function bar(b) {
  b.item = "changed";
  console.log(b === obj1) // true
}

var obj1 = {item: "unchanged"};

bar(obj1);

console.log(obj1.item);   // changed 修改引數的屬性將會影響到外部物件
複製程式碼

b === obj1列印結果為true可以看出,函式內部修改了引數的屬性並沒有影響到引數的引用。bobj1共享一個物件地址,所以修改引數的屬性將會影響到外部物件。

而將引數c重新賦值一個新物件,將不會影響到外部物件。

function baz(c) {
  c = {item: "changed"};
  console.log(c === obj2) // false
}

var obj2 = {item: "unchanged"};

baz(obj2);

console.log(obj2.item);   // unchanged 重新賦值將不會影響到外部物件
複製程式碼

將引數c重新賦值一個新物件,那麼c就繫結到了一個新的物件地址,c === obj2列印結果為false,判斷他們不再共享同一個物件地址。它們各自有獨立的物件地址。所以重新賦值將不會影響到外部物件。

總結

可以說 按共享傳遞按值傳遞 的特例,傳遞的是引用地址的拷貝。所以紅寶書上說的也沒錯。

可以把 ECMAScript 函式的引數想象成區域性變數。-- 《JavaScript高階程式設計》

延伸 - 惰性求值

前面瞭解到了所有函式的引數都是按值傳遞的。JavaScript 中引數是必須先求值再作為實參傳入函式的。但是在ES6中有一個特例。

引數預設值不是傳值的,而是每次都重新計算預設值表示式的值。也就是說,引數預設值是惰性求值的。 -- 《ECMAScript 6 入門》

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101
複製程式碼

上面程式碼中,引數p的預設值是x + 1。這時,每次呼叫函式foo,都會重新計算x + 1,而不是預設p等於 100。

參考

求值策略

Is JavaScript a pass-by-reference or pass-by-value language?

ES6 中函式引數的預設值

相關文章