JavaScript 求值策略(Evaluation strategy)

2018-07-28 17:21 更新

介紹

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

計(jì)算機(jī)科學(xué)里對(duì)這種策略一般稱(chēng)為“evaluation strategy”(大叔注:有的人說(shuō)翻譯成求值策略,有的人翻譯成賦值策略,通看下面的內(nèi)容,我覺(jué)得稱(chēng)為賦值策略更為恰當(dāng),anyway,標(biāo)題還是寫(xiě)成大家容易理解的求值策略吧),例如在編程語(yǔ)言為求值或者計(jì)算表達(dá)式設(shè)置規(guī)則。向函數(shù)傳遞參數(shù)的策略是一個(gè)特殊的case。

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

寫(xiě)這篇文章的原因是因?yàn)檎搲嫌腥艘鬁?zhǔn)確解釋一些傳參的策略,我們這里給出了相應(yīng)的定義,希望對(duì)大家有所幫助。

很多程序員都確信在JavaScript中(甚至其它一些語(yǔ)言),對(duì)象是按引用傳參,而原始值類(lèi)型按值傳參,此外,很多文章都說(shuō)到這個(gè)“事實(shí)”,但有多人真正理解這個(gè)術(shù)語(yǔ),而且又有多少是正確的?我們本篇講逐一講解。

一般理論

需要注意到,在賦值理論里一般有2中賦值策略:嚴(yán)格——意思是說(shuō)參數(shù)在進(jìn)入程序之前是經(jīng)過(guò)計(jì)算過(guò)的;非嚴(yán)格——意思是參數(shù)的計(jì)算是根據(jù)計(jì)算要求才去計(jì)算(也就是相當(dāng)于延遲計(jì)算)。

然后,這里我們考慮基本的函數(shù)傳參策略,從ECMAScript出發(fā)點(diǎn)來(lái)說(shuō)是非常重要的。首先需要注意的是,在ECMAScript中(甚至其他的語(yǔ)如,C,JAVA,Python和Ruby中)都使用了嚴(yán)格的參數(shù)傳遞策略。

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

嚴(yán)格的傳參策略也分為幾種子策略,其中最重要的一些策略我們?cè)诒菊略敿?xì)討論。

下面討論的策略不是全部都用在ECMAScript中,所以在討論這些策略的具體行為的時(shí)候,我們使用了偽代碼來(lái)展示。

按值傳遞

按值傳遞,很多開(kāi)發(fā)人員都很了解了,參數(shù)的值是調(diào)用者傳遞的對(duì)象值的拷貝(copy of value),函數(shù)內(nèi)部改變參數(shù)的值不會(huì)影響到外面的對(duì)象(該參數(shù)在外面的值),一般來(lái)說(shuō),是重新分配了新內(nèi)存(我們不關(guān)注分配內(nèi)存是怎么實(shí)現(xiàn)的——也是是棧也許是動(dòng)態(tài)內(nèi)存分配),該新內(nèi)存塊的值是外部對(duì)象的拷貝,并且它的值是用到函數(shù)內(nèi)部的。

bar = 10

procedure foo(barArg):
  barArg = 20;
end

foo(bar)

// foo內(nèi)部改變值不會(huì)影響內(nèi)部的bar的值
print(bar) // 10

但是,如果該函數(shù)的參數(shù)不是原始值而是復(fù)雜的結(jié)構(gòu)對(duì)象是時(shí)候,將帶來(lái)很大的性能問(wèn)題,C++就有這個(gè)問(wèn)題,將結(jié)構(gòu)作為值傳進(jìn)函數(shù)的時(shí)候——就是完整的拷貝。

我們來(lái)給一個(gè)一般的例子,用下面的賦值策略來(lái)檢驗(yàn)一下,想想一下一個(gè)函數(shù)接受2個(gè)參數(shù),第1個(gè)參數(shù)是對(duì)象的值,第2個(gè)是個(gè)布爾型的標(biāo)記,用來(lái)標(biāo)記是否完全修改傳入的對(duì)象(給對(duì)象重新賦值),還是只修改該對(duì)象的一些屬性。

// 注:以下都是偽代碼,不是JS實(shí)現(xiàn)
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)

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

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

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

按引用傳遞

另外一個(gè)眾所周知的按引用傳遞接收的不是值拷貝,而是對(duì)象的隱式引用,如該對(duì)象在外部的直接引用地址。函數(shù)內(nèi)部對(duì)參數(shù)的任何改變都是影響該對(duì)象在函數(shù)外部的值,因?yàn)閮烧咭玫氖峭粋€(gè)對(duì)象,也就是說(shuō):這時(shí)候參數(shù)就相當(dāng)于外部對(duì)象的一個(gè)別名。

偽代碼:

procedure foo(barArg, isFullChange):

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

  barArg.x = 100
  barArg.y = 200

end

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

// 按引用調(diào)用的結(jié)果如下: 
foo(bar)

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

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

// 此刻該對(duì)象已經(jīng)是一個(gè)新對(duì)象了
print(bar) // {z: 1, q: 2}

該策略可以更有效地傳遞復(fù)雜對(duì)象,例如帶有大批量屬性的大結(jié)構(gòu)對(duì)象。

按共享傳遞(Call by sharing)

上面2個(gè)策略大家都是知道的,但這里要講的一個(gè)策略可能大家不太了解(其實(shí)是學(xué)術(shù)上的策略)。但是,我們很快就會(huì)看到這正是它在ECMAScript中的參數(shù)傳遞戰(zhàn)略中起著關(guān)鍵作用的策略。

這個(gè)策略還有一些代名詞:“按對(duì)象傳遞”或“按對(duì)象共享傳遞”。

該策略是1974年由Barbara Liskov為CLU編程語(yǔ)言提出的。

該策略的要點(diǎn)是:函數(shù)接收的是對(duì)象對(duì)于的拷貝(副本),該引用拷貝和形參以及其值相關(guān)聯(lián)。

這里出現(xiàn)的引用,我們不能稱(chēng)之為“按引用傳遞”,因?yàn)楹瘮?shù)接收的參數(shù)不是直接的對(duì)象別名,而是該引用地址的拷貝。

最重要的區(qū)別就是:函數(shù)內(nèi)部給參數(shù)重新賦新值不會(huì)影響到外部的對(duì)象(和上例按引用傳遞的case),但是因?yàn)樵搮?shù)是一個(gè)地址拷貝,所以在外面訪問(wèn)和里面訪問(wèn)的都是同一個(gè)對(duì)象(例如外部的該對(duì)象不是想按值傳遞一樣完全的拷貝),改變?cè)搮?shù)對(duì)象的屬性值將會(huì)影響到外部的對(duì)象。

procedure foo(barArg, isFullChange):

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

  barArg.x = 100
  barArg.y = 200

end

//還是使用這個(gè)對(duì)象結(jié)構(gòu)
bar = {
  x: 10,
  y: 20
}

// 按貢獻(xiàn)傳遞會(huì)影響對(duì)象 
foo(bar)

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

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

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

這個(gè)處理的假設(shè)前提是大多數(shù)語(yǔ)言里用到的對(duì)象,而不是原始值。

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

按共享傳遞這個(gè)策略很很多語(yǔ)言里都使用了:Java, ECMAScript, Python, Ruby, Visual Basic等。此外,Python社區(qū)已經(jīng)使用了這個(gè)術(shù)語(yǔ),至于其他語(yǔ)言也可以用這個(gè)術(shù)語(yǔ),因?yàn)槠渌拿Q(chēng)往往會(huì)讓大家感覺(jué)到混亂。大多數(shù)情況下,例如在Java,ECMAScript或Visual Basic中,這一策略也稱(chēng)之為按值傳遞——意味著:特殊值——引用拷貝(副本)。

一方面,它是這樣的——傳遞給函數(shù)內(nèi)部用的參數(shù)僅僅是綁定值(引用地址)的一個(gè)名稱(chēng),并不會(huì)影響外部的對(duì)象。

另一方面,如果不深入研究,這些術(shù)語(yǔ)真的被認(rèn)為吃錯(cuò)誤的,因?yàn)楹芏嗾搲荚谡f(shuō)如何將對(duì)象傳遞給JavaScript函數(shù))。

一般理論確實(shí)有按值傳遞的說(shuō)法:但這時(shí)候這個(gè)值就是我們所說(shuō)的地址拷貝(副本),因此并沒(méi)喲破壞規(guī)則。

在Ruby中,這個(gè)策略稱(chēng)為按引用傳遞。再說(shuō)一下:它不是按照大結(jié)構(gòu)的拷貝來(lái)傳遞(例如,不是按值傳遞),而另一方面,我們沒(méi)有處理原始對(duì)象的引用,并且不能修改它;因此,這個(gè)跨術(shù)語(yǔ)的概念可能更會(huì)造成混亂。

理論里沒(méi)有像按值傳遞的特殊case一樣來(lái)面試按引用傳遞的特殊case。

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

按共享與指針

對(duì)于С/С+ +,這個(gè)策略在思想上和按指針值傳遞是一樣的,但有一個(gè)重要的區(qū)別——該策略可以取消引用指針以及完全改變對(duì)象。但在一般情況下,分配一個(gè)值(地址)指針到新的內(nèi)存塊(即之前引用的內(nèi)存塊保持不變);通過(guò)指針改變對(duì)象屬性的話會(huì)影響阿東外部對(duì)象。

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

然而,С/С+ +如果在沒(méi)有明顯指針的解引用的情況下,引用對(duì)象屬性的時(shí)候,還具有特殊的語(yǔ)法糖:

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

和C++關(guān)系最為緊密的這種意識(shí)形態(tài)可以從“智能指針”的實(shí)現(xiàn)中看到,例如,在 boost :: shared_ptr里,重載了賦值操作符以及拷貝構(gòu)造函數(shù),而且還使用了對(duì)象的引用計(jì)數(shù)器,通過(guò)GC刪除對(duì)象。 這種數(shù)據(jù)類(lèi)型,甚至有類(lèi)似的名字- 共享_ptr。

ECMAScript實(shí)現(xiàn)

現(xiàn)在我們知道了ECMAScript中將對(duì)象作為參數(shù)傳遞的策略了——按共享傳遞:修改參數(shù)的屬性將會(huì)影響到外部,而重新賦值將不會(huì)影響到外部對(duì)象。但是,正如我們上面提到的,其中的ECMAScript開(kāi)發(fā)人員一般都稱(chēng)之為是:按值傳遞,只不過(guò)該值是引用地址的拷貝。

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

更確切地說(shuō),這種行為可以理解為簡(jiǎn)單的賦值,我們可以看到,內(nèi)部是完全不同的對(duì)象,只不過(guò)引用的是相同的值——也就是地址副本。

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]

即兩個(gè)標(biāo)識(shí)符(名稱(chēng)綁定)綁定到內(nèi)存中的同一個(gè)對(duì)象, 共享這個(gè)對(duì)象:

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

而重新賦值分配,綁定是新的對(duì)象標(biāo)識(shí)符(新地址),而不影響已經(jīng)先前綁定的對(duì)象 :

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

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

即現(xiàn)在foo和 bar,有不同的值和不同的地址:

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

再?gòu)?qiáng)調(diào)一下,這里所說(shuō)對(duì)象的值是地址(address),而不是對(duì)象結(jié)構(gòu)本身,將變量賦值給另外一個(gè)變量——是賦值值的引用。因此兩個(gè)變量引用的是同一個(gè)內(nèi)存地址。下一個(gè)賦值卻是新地址,是解析與舊對(duì)象的地址綁定,然后綁定到新對(duì)象的地址上,這就是和按引用傳遞的最重要區(qū)別。

此外,如果只考慮ECMA-262標(biāo)準(zhǔn)所提供的抽象層次,我們?cè)谒惴ɡ锟吹降闹挥小爸怠边@個(gè)概念,實(shí)現(xiàn)傳遞的“值”(可以是原始值,也可以是對(duì)象),但是按照我們上面的定義,也可以完全稱(chēng)之為“按值傳遞”,因?yàn)橐玫刂芬彩侵怠?/p>

然而,為了避免誤解(為什么外部對(duì)象的屬性可以在函數(shù)內(nèi)部改變),這里依然需要考慮實(shí)現(xiàn)層面的細(xì)節(jié)——我們看到的按共享傳遞,或者換句話講——按安全指針傳遞,而安全指針不可能去解除引用和改變對(duì)象的,但可以去修改該對(duì)象的屬性值。

術(shù)語(yǔ)版本

讓我們來(lái)定義ECMAScript中該策略的術(shù)語(yǔ)版本。

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

或針對(duì)這種情況下,專(zhuān)門(mén)稱(chēng)之為“按共享傳遞”,通過(guò)這個(gè)正好可以看到傳統(tǒng)的按值傳遞和按引用傳遞的區(qū)別,這種情況,可以分成2個(gè)種情況:1:原始值按值傳遞;2:對(duì)象按共享傳遞。

“通過(guò)引用類(lèi)型將對(duì)象到函數(shù)”這句話和ECMAScript無(wú)關(guān),而且它是錯(cuò)誤的。

結(jié)論

我希望這篇文章有助于宏觀上了解更多細(xì)節(jié),以及在ECMAScript中的實(shí)現(xiàn)。一如既往,如果有任何問(wèn)題,歡迎討論。

其它參考


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)