深入理解JavaScript系列(19):求值策略(Evaluation strategy)

來源:互聯網
上載者:User
介紹

本章,我們將講解在ECMAScript向函數function傳遞參數的策略。

電腦科學裡對這種策略一般稱為“evaluation strategy”(大叔註:有的人說翻譯成求值策略,有的人翻譯成賦值策略,通看下面的內容,我覺得稱為賦值策略更為恰當,anyway,標題還是寫成大家容易理解的求值策略吧),例如在程式設計語言為求值或者計算運算式設定規則。向函數傳遞參數的策略是一個特殊的case。

http://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/

寫這篇文章的原因是因為論壇上有人要求準確解釋一些傳參的策略,我們這裡給出了相應的定義,希望對大家有所協助。

很多程式員都確信在JavaScript中(甚至其它一些語言),對象是按引用傳參,而原始實值型別按值傳參,此外,很多文章都說到這個“事實”,但有多人真正理解這個術語,而且又有多少是正確的?我們本篇講逐一講解。

一般理論

需要注意到,在賦值理論裡一般有2中賦值策略:嚴格——意思是說參數在進入程式之前是經過計算過的;非嚴格——意思是參數的計算是根據計算要求才去計算(也就是相當於延遲計算)。

然後,這裡我們考慮基本的函數傳參策略,從ECMAScript出發點來說是非常重要的。首先需要注意的是,在ECMAScript中(甚至其他的語如,C,JAVA,Python和Ruby中)都使用了嚴格的參數傳遞策略。

另外傳遞參數的計算順序也是很重要的——在ECMAScript是左到右,而且其它語言實現的反省順序(從右向做)也是可以用的。

嚴格的傳參策略也分為幾種子策略,其中最重要的一些策略我們在本章詳細討論。

下面討論的策略不是全部都用在ECMAScript中,所以在討論這些策略的具體行為的時候,我們使用了虛擬碼來展示。

按值傳遞

按值傳遞,很多開發人員都很瞭解了,參數的值是調用者傳遞的對象值的拷貝(copy of value),函數內部改變參數的值不會影響到外面的對象(該參數在外面的值),一般來說,是重新分配了新記憶體(我們不關注分配記憶體是怎麼實現的——也是 是棧也許是動態記憶體分配),該新記憶體塊的值是外部對象的拷貝,並且它的值是用到函數內部的。

bar = 10

procedure foo(barArg):
barArg = 20;
end

foo(bar)

// foo內部改變值不會影響內部的bar的值
print(bar) // 10

但是,如果該函數的參數不是原始值而是複雜的結構對象是時候,將帶來很大的效能問題,C++就有這個問題,將結構作為值傳進函數的時候——就是完整的拷貝。

我們來給一個一般的例子,用下面的賦值策略來檢驗一下,想想一下一個函數接受2個參數,第1個參數是對象的值,第2個是個布爾型的標記,用來標記是否完全修改傳入的對象(給對象重新賦值),還是只修改該對象的一些屬性。

// 註:以下都是虛擬碼,不是JS實現
bar = {
x: 10,
y: 20
}

procedure foo(barArg, isFullChange):

if isFullChange:
barArg = {z: 1, q: 2}
exit
end

barArg.x = 100
barArg.y = 200

end

foo(bar)

// 按值傳遞,外部的對象不被改變
print(bar) // {x: 10, y: 20}

// 完全改變對象(賦新值)
foo(bar, true)

//也沒有改變
print(bar) // {x: 10, y: 20}, 而不是{z: 1, q: 2}

按引用傳遞

另外一個眾所周知的按引用傳遞接收的不是值拷貝,而是對象的隱式引用,如該對象在外部的直接引用地址。函數內部對參數的任何改變都是影響該對象在函數外部的值,因為兩者引用的是同一個對象,也就是說:這時候參數就相當於外部對象的一個別名。

虛擬碼:

procedure foo(barArg, isFullChange):

if isFullChange:
barArg = {z: 1, q: 2}
exit
end

barArg.x = 100
barArg.y = 200

end

// 使用和上例相同的對象
bar = {
x: 10,
y: 20
}

// 按引用調用的結果如下:
foo(bar)

// 對象的屬性值已經被改變了
print(bar) // {x: 100, y: 200}

// 重新賦新值也影響到了該對象
foo(bar, true)

// 此刻該對象已經是一個新對象了
print(bar) // {z: 1, q: 2}

該策略可以更有效地傳遞複雜物件,例如帶有大批量屬性的大結構對象。

按共用傳遞(Call by sharing)

上面2個策略大家都是知道的,但這裡要講的一個策略可能大家不太瞭解(其實是學術上的策略)。但是,我們很快就會看到這正是它在ECMAScript中的參數傳遞戰略中起著關鍵作用的策略。

這個策略還有一些代名詞:“按對象傳遞”或“按對象共用傳遞”。

該策略是1974年由Barbara Liskov為CLU程式設計語言提出的。

該策略的要點是:函數接收的是對象對於的拷貝(副本),該引用拷貝和形參以及其值相關聯。

這裡出現的引用,我們不能稱之為“按引用傳遞”,因為函數接收的參數不是直接的對象別名,而是該引用地址的拷貝。

最重要的區別就是:函數內部給參數重新賦新值不會影響到外部的對象(和上例按引用傳遞的case),但是因為該參數是一個地址拷貝,所以在外面訪問 和裡面訪問的都是同一個對象(例如外部的該對象不是想按值傳遞一樣完全的拷貝),改變該參數對象的屬性值將會影響到外部的對象。

procedure foo(barArg, isFullChange):

if isFullChange:
barArg = {z: 1, q: 2}
exit
end

barArg.x = 100
barArg.y = 200

end

//還是使用這個對象結構
bar = {
x: 10,
y: 20
}

// 按貢獻傳遞會影響對象
foo(bar)

// 對象的屬性被修改了
print(bar) // {x: 100, y: 200}

// 重新賦值沒有起作用
foo(bar, true)

// 依然是上面的值
print(bar) // {x: 100, y: 200}

這個處理的假設前提是大多數語言裡用到的對象,而不是原始值。

按共用傳遞是按值傳遞的特例

按共用傳遞這個策略很很多語言裡都使用了:Java, ECMAScript, Python, Ruby, Visual Basic等。此外,Python社區已經使用了這個術語,至於其他語言也可以用這個術語,因為其他的名稱往往會讓大家感覺到混亂。大多數情況下,例如在 Java,ECMAScript或Visual Basic中,這一策略也稱之為按值傳遞——意味著:特殊值——引用拷貝(副本)。

一方面,它是這樣的——傳遞給函數內部用的參數僅僅是綁定值(引用地址)的一個名稱,並不會影響外部的對象。

另一方面,如果不深入研究,這些術語真的被認為吃錯誤的,因為很多論壇都在說如何將對象傳遞給JavaScript函數)。

一般理論確實有按值傳遞的說法:但這時候這個值就是我們所說的地址拷貝(副本),因此並沒喲破壞規則。

在Ruby中,這個策略稱為按引用傳遞。再說一下:它不是按照大結構的拷貝來傳遞(例如,不是按值傳遞),而另一方面,我們沒有處理原始對象的引用,並且不能修改它;因此,這個跨術語的概念可能更會造成混亂。

理論裡沒有像按值傳遞的特殊case一樣來面試按引用傳遞的特殊case。

但依然有必要瞭解這些策略在上述提到的技術中(Java, ECMAScript, Python, Ruby, other),實際上——他們用的策略就是按共用傳遞。

按共用與指標

對於С/С+ +,這個策略在思想上和按指標值傳遞是一樣的,但有一個重要的區別——該策略可以取值 (Dereference)指標以及完全改變對象。但在一般情況下,分配一個值(地址)指標到新的記憶體塊(即之前引用的記憶體塊保持不變);通過指標改變對象屬性的話會影響阿東外部對象。

因此,和指標類別,我們可以明顯看到,這是按地址值傳遞。 在這種情況下,按共用傳遞只是“文法糖”,像指標賦值行為一樣(但不能取值 (Dereference)),或者像引用一樣修改屬性(不需要取值 (Dereference)操作),有時候,它可以被命名為“安全指標”。

然而,С/С+ +如果在沒有明顯指標的解引用的情況下,引用對象屬性的時候,還具有特殊的文法糖:

obj->x instead of (*obj).x

和C++關係最為緊密的這種意識形態可以從“智能指標”的實現中看到,例如,在 boost :: shared_ptr裡,重載了賦值操作符以及拷貝建構函式,而且還使用了對象的引用計數器,通過GC刪除對象。 這種資料類型,甚至有類似的名字- 共用_ptr。

ECMAScript實現

現在我們知道了ECMAScript中將對象作為參數傳遞的策略了——按共用傳遞:修改參數的屬性將會影響到外部,而重新賦值將不會影響到外部對象。但是,正如我們上面提到的,其中的ECMAScript開發人員一般都稱之為是:按值傳遞,只不過該值是引用地址的拷貝。

JavaScript發明人布倫丹·艾希也寫到了:傳遞的是引用的拷貝(地址副本)。所以論壇裡大家曾說的按值傳遞,在這種解釋下,也是對的。

更確切地說,這種行為可以理解為簡單的賦值,我們可以看到,內部是完全不同的對象,只不過引用的是相同的值——也就是地址副本。

ECMAScript代碼:

var foo = {x: 10, y: 20};
var bar = foo;

alert(bar === foo); // true

bar.x = 100;
bar.y = 200;

alert([foo.x, foo.y]); // [100, 200]

即兩個標識符(名稱綁定)綁定到記憶體中的同一個對象, 共用這個對象:

foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF) <= bar value: addr(0xFF)

而重新賦值分配,綁定是新的物件識別碼(新地址),而不影響已經先前綁定的對象 :

bar = {z: 1, q: 2};

alert([foo.x, foo.y]); // [100, 200] – 沒改變
alert([bar.z, bar.q]); // [1, 2] – 但現在引用的是新對象

即現在foo和 bar,有不同的值和不同的地址:

foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF)
bar value: addr(0xFA) => {z: 1, q: 2} (address 0xFA)

再強調一下,這裡所說對象的值是地址(address),而不是對象結構本身,將變數賦值給另外一個變數——是賦值值的引用。因此兩個變數引用的是 同一個記憶體位址。下一個賦值卻是新地址,是解析與舊對象的地址綁定,然後綁定到新對象的地址上,這就是和按引用傳遞的最重要區別。

此外,如果只考慮ECMA-262標準所提供的抽象層次,我們在演算法裡看到的只有“值”這個概念,實現傳遞的“值”(可以是原始值,也可以是對象),但是按照我們上面的定義,也可以完全稱之為“按值傳遞”,因為引用地址也是值。

然而,為了避免誤解(為什麼外部對象的屬性可以在函數內部改變),這裡依然需要考慮實現層面的細節——我們看到的按共用傳遞,或者換句話講——按安全指標傳遞,而安全指標不可能去解除引用和改變對象的,但可以去修改該對象的屬性值。

術語版本

讓我們來定義ECMAScript中該策略的術語版本。

可以稱之為“按值傳遞”——這裡所說的值是一個特殊的case,也就是該值是地址副本(address copy)。從這個層面我們可以說:ECMAScript中除了異常之外的對象都是按值傳遞的,這實際上是ECMAScript抽象的層面。

或針對這種情況下,專門稱之為“按共用傳遞”,通過這個正好可以看到傳統的按值傳遞和按引用傳遞的區別,這種情況,可以分成2個種情況:1:原始值按值傳遞;2:對象按共用傳遞。

“通過參考型別將對象到函數”這句話和ECMAScript無關,而且它是錯誤的。

結論

我希望這篇文章有助於宏觀上瞭解更多細節,以及在ECMAScript中的實現。一如既往,如果有任何問題,歡迎討論。

其它參考
  • Evaluation strategy
  • Call by value
  • Call by reference
  • Call by sharing

轉自:湯姆大叔

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.