感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取
在第五章中,我們談?wù)摿藴p少側(cè)因/副作用的重要性:它們使你的應(yīng)用程序狀態(tài)會(huì)出乎意料地改變并造成意外的結(jié)果(bug)。這樣有地雷的地方越少,我們就能對(duì)自己的代碼更有信心,而且它的可讀性也更高。我們?cè)诒菊轮械脑掝}緊跟著為了相同的目的而做出的努力。
如果編程上的冪等性是關(guān)于定義一個(gè)只影響狀態(tài)一次的改變值的操作,那么我們現(xiàn)在將注意力轉(zhuǎn)向另一個(gè)目標(biāo):將改變發(fā)生的數(shù)量從一降為零。
現(xiàn)在我們來探索一下值的不可變性,這個(gè)概念是說我們?cè)诔绦蛑袃H使用不能被改變的值。
基本類型的不可變性
基本類型(number、string、boolean、null、以及 undefined)的值已經(jīng)是不可變的了;你無法做任何事情來改變它們。
// 不合法,也沒有意義
2 = 2.5;
然而,JS 確實(shí)有一種特殊的行為,使它看起來允許修改這樣的基本類型值:“封箱”。當(dāng)你訪問特定基本類型值上的一個(gè)屬性時(shí) —— 具體說是 number、string、和 boolean —— JS 在底層自動(dòng)地將這個(gè)值包裝(也就是“封箱”)在它對(duì)應(yīng)的對(duì)象中(分別是 Number、String、以及 Boolean)。
考慮如下代碼:
var x = 2;
x.length = 4;
x; // 2
x.length; // undefined
數(shù)字一般沒有 length 屬性可用,所以設(shè)置 x.length = 4 是在試圖添加一個(gè)新屬性,而且它無聲地失敗了(或者說被忽略/丟棄了,這要看你的視角);x 繼續(xù)持有簡單基本類型數(shù)字 2。
但是如果除了潛在地使讀者糊涂以外沒有其他原因,JS 允許語句 x.length = 4 運(yùn)行這件事看起來根本就是個(gè)麻煩。好消息是,如果你使用 strict 模式("use strict";),這樣的語句將拋出一個(gè)錯(cuò)誤。
要是你試著改變一個(gè)被明確封箱為對(duì)象表現(xiàn)形式的這樣一個(gè)值呢?
var x = new Number( 2 );
// 可以工作
x.length = 4;
這段代碼中的 x 持有一個(gè)指向?qū)ο蟮囊?,所以添加和改變自定義屬性沒有問題。
像 number 這樣的簡單基本類型值的不可變性看起來相當(dāng)顯而易見。那 string 值呢?JS 開發(fā)者們有一個(gè)很常見的誤解,就是字符串和數(shù)組很像而且因此可以被改變。JS 語法甚至使用 [] 訪問操作符暗示它們?yōu)?“類數(shù)組”。然而,字符串也是不可變的。
var s = "hello";
s[1]; // "e"
s[1] = "E";
s.length = 10;
s; // "hello"
除了能夠像在一個(gè)數(shù)組中那樣訪問 s[1],JS 字符串不是真正的數(shù)組。設(shè)置 s[1] = "E" 和 s.length = 10 都會(huì)無聲地失敗,就像上面的 x.length = 4 一樣。在 strict 模式中,這些語句會(huì)失敗,因?yàn)閷傩?1 和屬性 length 在擊基本類型的 string 值上都是只讀的。
有趣的是,即使是封箱后的 String 對(duì)象值也會(huì)表現(xiàn)為(幾乎)不可變,因?yàn)槿绻阍?strict 模式下改變它的既存屬性的話,它將拋出錯(cuò)誤:
"use strict";
var s = new String( "hello" );
s[1] = "E"; // error
s.length = 10; // error
s[42] = "?"; // OK
s; // "hello"
從值到值
我們將在本章中更徹底地展開這個(gè)概念,但為了在開始的時(shí)候讓我們的大腦中形成一個(gè)清晰的認(rèn)識(shí):值的不可變性不意味著我們不能擁有在程序運(yùn)行的整個(gè)進(jìn)程中一直改變的值。一個(gè)沒有改變的值的程序可不是非常有趣!它也不意味著我們的變量不能持有不同的值。這些都是對(duì)值的不可變性的誤解。
值的不可變性意味著,當(dāng)我們需要在程序中改變狀態(tài)時(shí),我們必須創(chuàng)建并追蹤一個(gè)新的值而不是改變一個(gè)既存的值。
例如:
function addValue(arr) {
var newArr = [ ...arr, 4 ];
return newArr;
}
addValue( [1,2,3] ); // [1,2,3,4]
注意我們沒有改變 arr 引用的數(shù)組,而是創(chuàng)建了一個(gè)新數(shù)組 —— 它包含既存的值外加新的值 4。
基于我們?cè)诘谖逭轮嘘P(guān)于側(cè)因/副作用的討論來分析一下 addValue(..)。它是純粹的嗎?它具有引用透明性嗎?給它相同的數(shù)組,它會(huì)總是產(chǎn)生相同的輸出嗎?它既沒有側(cè)因也沒有副作用嗎?是的。
想象一下,數(shù)組 [1,2,3] 表示一系列來自于某些先前操作的數(shù)據(jù),而且我們把它存儲(chǔ)在某些變量中。這就是我們的當(dāng)前狀態(tài)。如果我們想計(jì)算應(yīng)用程序的下一個(gè)狀態(tài)是什么,我們調(diào)用了 addValue(..)。但我們想要下一個(gè)狀態(tài)的計(jì)算進(jìn)行得直接且明確。所以 addValue(..) 操作接收一個(gè)直接的輸入,返回一個(gè)直接的輸出,并且避免通過改變 arr 引用的原始數(shù)組來制造副作用。
這意味著我們可以計(jì)算出新的狀態(tài) [1,2,3,4] 而且完全掌控這種狀態(tài)的轉(zhuǎn)換。我們程序中沒有其他部分可以意外地將我們提早地轉(zhuǎn)換到這個(gè)狀態(tài),或者完全轉(zhuǎn)換為另一個(gè)狀態(tài),比如 [1,2,3,5]。通過管控我們的值并將它們視為不可變的,我們極大地縮小了意外的表面積,使我們的程序更易于閱讀,易于推理,而最終更可信任。
arr 引用的數(shù)組實(shí)際上是可變的。我們只是選擇不去改變它,如此我們踐行了值的不可變性的精神。
我們也可以對(duì)對(duì)象使用這種拷貝而非改變的策略??紤]如下代碼:
function updateLastLogin(user) {
var newUserRecord = Object.assign( {}, user );
newUserRecord.lastLogin = Date.now();
return newUserRecord;
}
var user = {
// ..
};
user = updateLastLogin( user );
非本地
如果你做這樣的事情,就可以看出不可變值的重要性:
var arr = [1,2,3];
foo( arr );
console.log( arr[0] );
表面上,你希望 arr[0] 依然是值 1。但它是嗎?你不知道,因?yàn)?foo(..) 可能 會(huì)使用你傳遞給它的引用改變這個(gè)數(shù)組。
在前一章中我們已經(jīng)看到了一種作弊的方法可以避免這樣的意外:
var arr = [1,2,3];
foo( arr.slice() ); // 哈!一個(gè)拷貝!
console.log( arr[0] ); // 1
當(dāng)然,僅在 foo 沒有跳過它的形式參數(shù)而通過自由變量的詞法引用來引用我們同一個(gè) arr 時(shí),這種斷言才能夠成立!
再過一會(huì),我們將看到另一種保護(hù)我們自己的策略,使值不會(huì)出乎我們意料地被改變。
重新賦值
你如何描述一個(gè)“常量”是什么?在讀下一個(gè)段落之前思考片刻。
...
你們中的一些人可能會(huì)浮現(xiàn)出這樣的描述,“一個(gè)不能改變的值”,“一個(gè)不能被改變的變量”,等等。這些大約都是正確答案的鄰居,但不是十分正確的門牌號(hào)碼。一個(gè)常量的準(zhǔn)確定義應(yīng)當(dāng)是:一個(gè)不能被重新賦值的變量。
這種吹毛求疵真的很重要,因?yàn)樗鞔_了一個(gè)常量實(shí)際上與值沒有關(guān)系,除了無論一個(gè)常量持有什么值,這個(gè)變量都不能再被賦值為任何其他值。但它沒說關(guān)于值本身性質(zhì)的任何事情。
考慮如下代碼:
var x = 2;
就像我們?cè)缦扔懻摰?,?2 是一個(gè)不可改變的基本類型。如果我們把代碼改為:
const x = 2;
關(guān)鍵字 const —— 為人熟知的“常量聲明” —— 的出現(xiàn)實(shí)際上根本不會(huì)改變 2 的性質(zhì);它已經(jīng)是不可改變的了,而且將永遠(yuǎn)是不可改變的。
這稍后的一行將會(huì)出錯(cuò)并失敗確實(shí)是真的:
// 試著改變 `x`,雙手合十!
x = 3; // 錯(cuò)誤!
但同樣地,我們沒有在改變值的任何東西。我們?cè)噲D給變量 x 重新賦值。卷入其中的值幾乎是巧合。
為了證明 const 與值的性質(zhì)沒有關(guān)系,考慮如下代碼:
const x = [ 2 ];
這個(gè)數(shù)組是常量嗎?不。 x 是一個(gè)常量,因?yàn)樗荒鼙恢匦沦x值了。但是這一行則完全沒問題:
x[0] = 3;
為什么?因?yàn)閿?shù)組依然使完全可變的,即使 x 是一個(gè)常量。
const 與“常量”僅與賦值有關(guān)而與值的語義無關(guān),這種困惑是一個(gè)又臭又長的故事??雌饋韼缀趺糠N擁有 const 的語言的開發(fā)者中,都有很大一部分人被同樣的困惑絆到。Java 事實(shí)上廢棄了 const 而引入了一個(gè)新的關(guān)鍵字 final,至少在某種程度上將它自己與“常量”語義的困惑分離開了。
先把困惑的詆毀放在一邊,如果 const 與創(chuàng)建一個(gè)不可變的值毫無關(guān)系,那么它對(duì) FP 程序員的重要性在于何處?
意圖
const 的使用告訴你代碼的讀者 這個(gè) 變量將不會(huì)再次被賦值了。作為一種意圖的信號(hào),const 成為了 JavaScript 的一種受歡迎的增益,作為一種對(duì)代碼可讀性的全面改善而經(jīng)常受到高度的贊揚(yáng)。
在我看來,這幾乎就是炒作;這些主張中沒有太多實(shí)質(zhì)上的東西。以這種方式標(biāo)示你的意圖,我只能看到一點(diǎn)兒極其微弱的好處。而且當(dāng)你把這與近十幾年它造成的困惑 —— 暗示值的不可變性 —— 比起來的話,我認(rèn)為 const 甚至配不上它的地位。
為了證明我的判斷,讓我們檢視一下實(shí)際情況。const 會(huì)創(chuàng)建一個(gè)塊作用域變量,意味著這個(gè)變量僅會(huì)存在于這個(gè)局部化的塊中:
// 許多代碼
{
const x = 2;
// 幾行代碼
}
// 許多代碼
塊通常被認(rèn)為最好是設(shè)計(jì)成僅有幾行代碼的長度。如果你有一些塊多于,比如說 10 行,大多數(shù)開發(fā)者都會(huì)建議你重構(gòu)。所以 const x = 2 做多僅對(duì)那剩下的 9 行代碼有效。
程序中沒有其他部分可能影響 x 的賦值。就是這樣。
我的主張是:這段程序的可讀性量級(jí)基本上和下面這一段是相同的:
// 許多代碼
{
let x = 2;
// 幾行代碼
}
// 許多代碼
如果你看看 let x = 2 后面的幾行代碼;你很容易就能知道 x 實(shí)際上沒有被重新賦值。與使用什么 const 聲明來表示“不會(huì)給它重新賦值”相比,這對(duì)我來說是一種 強(qiáng)烈得多的信號(hào) —— 實(shí)際上沒有給它重新賦值。
另外,讓我們考慮一下這段代碼在第一眼看上去將會(huì)如何與讀者交流:
const magicNums = [1,2,3,4];
// ..
有沒有那么一點(diǎn)可能(很可能?),你代碼的讀者將會(huì)(錯(cuò)誤地)假設(shè)你的意圖是絕不會(huì)改變這個(gè)數(shù)組?這在我看來是一個(gè)合理的推斷。想象一下他們的困惑,如果你實(shí)際上允許 magicNums 引用的數(shù)組被改變。這將令人相當(dāng)?shù)捏@訝,不是嗎???
更可怕的是,要是你故意改變 magicNums 的方式對(duì)于讀者來說不那么顯而易見呢?在稍后的代碼中,他們看到 magicNums 的使用并(同樣錯(cuò)誤地)假設(shè)它依然是 [1,2,3,4],因?yàn)樗鼈儗⒛愕囊鈭D理解為“將不會(huì)改變它”。
我認(rèn)為你應(yīng)當(dāng)使用 var 或 let 來聲明持有你試圖改變的值的變量。與使用 const 相比,我想那才是一個(gè) 明確得多的信號(hào)。
但是 const 造成的麻煩不止于此。還記得我們?cè)诒菊麻_頭斷言的,將值視為不可變意味著當(dāng)我們需要改變狀態(tài)時(shí),我們必須創(chuàng)建一個(gè)新的值而非改變它嗎?一旦你創(chuàng)建了一個(gè)新數(shù)組,你將如何處理它?要是你使用 const 來聲明持有它的引用,你就不能再給它賦值了。那么……接下來怎么辦?
照此看來,我覺得 const 實(shí)際上使我們堅(jiān)持 FP 的努力變得更加困難,而不是容易。我的結(jié)論:const 根本就沒那么有用。它以一種麻煩的方式給我們?cè)斐闪瞬槐匾睦Щ笈c限制。我僅會(huì)對(duì)這樣簡單的常量使用 const:
const PI = 3.141592;
值 3.141592 已經(jīng)是不可變的了,而且我明確地表示,“這個(gè) PI 將總是被用于這個(gè)字面值的替代品或占位符?!?對(duì)我來說,這才是 const 有益的地方。而且坦白地講,在我的代表性的編碼中,我不會(huì)使用太多這種類型的聲明。
我寫過也看過許多 JavaScript,我想我們?cè)S多的 bug 來源于意外的重新賦值只不過是一個(gè)想象中的問題。
我認(rèn)為我們需要擔(dān)心的不是我們的變量是否被重新賦值了,而是 我們的變量是否被改變了。為什么?因?yàn)橹凳强梢粤鲃?dòng)的;而詞法賦值不會(huì)。你可以將一個(gè)數(shù)組傳遞給一個(gè)函數(shù),而它可以在不被你知曉的情況下改變。但你不可能有一個(gè)由某些程序其他部分引起的意外的重新賦值。
凍結(jié)于此
有一種既便宜又簡單的方法可以將一個(gè)可變的對(duì)象/數(shù)組/函數(shù)變?yōu)橐粋€(gè)(某種意義上的)“不變值”:
var x = Object.freeze( [2] );
Object.freeze(..) 工具遍歷一個(gè)對(duì)象/數(shù)組上的所有屬性/下標(biāo)并將它們標(biāo)識(shí)為只讀,于是它們就不能被重新賦值了。這實(shí)際上有些像使用 const 來聲明屬性!Object.freeze(..) 還會(huì)將屬性標(biāo)識(shí)為不可配置的,并且將對(duì)象/數(shù)組本身標(biāo)識(shí)為不可擴(kuò)展的(不能添加新的屬性)。實(shí)質(zhì)上,它使對(duì)象的頂層成為不可變的。
但只有頂層。要小心!
var x = Object.freeze( [ 2, 3, [4, 5] ] );
// 不允許:
x[0] = 42;
// 噢,依然允許:
x[2][0] = 42;
Object.freeze(..) 提供了一種淺層的,幼稚的不可變性。如果你想要一個(gè)深層的不可變值,你就必須手動(dòng)地遍歷整個(gè)對(duì)象/數(shù)組結(jié)構(gòu)并在每一個(gè)子對(duì)象/數(shù)組上使用 Object.freeze(..)。
但是與使你稀里糊涂地認(rèn)為你得到了一個(gè)不可變值 —— 其實(shí)不是 —— 的 const 相對(duì)比起來,Object.freeze(..) 確實(shí) 給了你一個(gè)不可變值。
回想一下之前防護(hù)的例子:
var arr = Object.freeze( [1,2,3] );
foo( arr );
console.log( arr[0] ); // 1
現(xiàn)在 arr[0] 相當(dāng)可靠地是 1。
這相當(dāng)重要,因?yàn)楫?dāng)我們的值被傳遞給不可見或不可控的某些地方時(shí),如果我們知道自己可以確信這個(gè)值不會(huì)改變,這會(huì)使推理我們的代碼容易得多。
性能
無論何時(shí)我們開始創(chuàng)建一個(gè)新的值(數(shù)組,對(duì)象,等等)而非改變既存的東西,下一個(gè)顯而易見的問題就是:這對(duì)性能意味著什么?
如果每次我們向一個(gè)數(shù)組添加元素時(shí)都不得不重新分配一個(gè)新數(shù)組,那么這不僅消耗 CPU 時(shí)間和額外的內(nèi)存,而且舊的值(如果不再被引用的話)還要被垃圾回收;那會(huì)造成更多的 CPU 負(fù)擔(dān)。
這是一筆可以接受的交易嗎?要看情況。關(guān)于代碼性能的討論或優(yōu)化不應(yīng)該在 沒有上下文環(huán)境 的前提下進(jìn)行。
如果你有一個(gè)狀態(tài)的改變?cè)谡麄€(gè)程序的生命周期中只發(fā)生一次(甚至幾次),為了新的數(shù)組/對(duì)象而把舊的扔掉就幾乎沒什么可在乎的。我們所談?wù)摰南氖侨绱酥?—— 可能最多僅僅幾微秒 —— 以至于不會(huì)對(duì)你應(yīng)用程序的性能有實(shí)質(zhì)上的影響。與你將節(jié)省下來的幾分鐘或幾小時(shí) —— 花在不得不追蹤并修復(fù)因值的意外改變而引起的 bug —— 相比,這甚至沒什么可爭論的。
那么同樣,如果這樣的操作將頻繁發(fā)生,或者特別地發(fā)生在你應(yīng)用程序的 關(guān)鍵路徑 上,那么性能 —— 性能與內(nèi)容兩者! —— 就完全是一個(gè)需要關(guān)心的問題了。
考慮一種特殊的數(shù)據(jù)結(jié)構(gòu),它像一個(gè)數(shù)組一樣,但是你想在改變它時(shí),使每一次改變都隱含地表現(xiàn)為好像得到了一個(gè)新的數(shù)組。在不實(shí)際每次創(chuàng)建一個(gè)新數(shù)組的情況下,你如何達(dá)成這個(gè)目標(biāo)?這種特殊的數(shù)組數(shù)據(jù)結(jié)構(gòu)能夠存儲(chǔ)原始值,然后跟蹤每一次改并將之作為前一個(gè)版本的增量。
從內(nèi)部講,它可能很像一個(gè)對(duì)象引用的鏈表樹,樹上的每一個(gè)節(jié)點(diǎn)都代表一個(gè)原始值的改變。實(shí)際上,這在概念上與 git 版本控制工作的方式很相似。

想象一下這樣使用這種理論上的特殊數(shù)組數(shù)據(jù)結(jié)構(gòu):
var state = specialArray( 1, 2, 3, 4 );
var newState = state.set( 42, "meaning of life" );
state === newState; // false
state.get( 2 ); // 3
state.get( 42 ); // undefined
newState.get( 2 ); // 3
newState.get( 42 ); // "meaning of life"
newState.slice( 1, 3 ); // [2,3]
數(shù)據(jù)結(jié)構(gòu) specialArray(..) 將會(huì)在內(nèi)部追蹤每一次改變操作(比如 set(..)),作為一個(gè) diff,所以它不必僅為了向列表中添加值 "meaning of life" 而為原始值(1、2、3、和 4)重新分配內(nèi)存。但重要的是,state 和 newState 指向了這個(gè)數(shù)組值的不同版本,所以 值的不可變性的語義被保持了下來。
發(fā)明你自己的性能優(yōu)化數(shù)據(jù)結(jié)構(gòu)是一種有趣的挑戰(zhàn)。但從實(shí)用的角度講,你可能應(yīng)當(dāng)使用一個(gè)在這方面已經(jīng)做得很好的庫。一個(gè)很棒的選項(xiàng)是 Immutable.js (http://facebook.github.io/immutable-js),它提供了各種數(shù)據(jù)結(jié)構(gòu),包括 List(類似數(shù)組)和 Map(類似對(duì)象)。
考慮上面的 specialArray 例子,但是用 Immutable.List:
var state = Immutable.List.of( 1, 2, 3, 4 );
var newState = state.set( 42, "meaning of life" );
state === newState; // false
state.get( 2 ); // 3
state.get( 42 ); // undefined
newState.get( 2 ); // 3
newState.get( 42 ); // "meaning of life"
newState.toArray().slice( 1, 3 ); // [2,3]
像 Immutable.js 這樣強(qiáng)大的庫采用了非常精巧的性能優(yōu)化方法。在沒有這樣的庫的幫助下處理所有的細(xì)節(jié)以及罕見狀況將十分困難。
當(dāng)一個(gè)值改變的情況很少發(fā)生或不頻繁,而且不太需要關(guān)心性能的時(shí)候,我推薦輕量級(jí)的解決方案,使用早先討論的內(nèi)建 Object.freeze(..)。
解決方法
要是我們?cè)诤瘮?shù)中收到一個(gè)值而且我們無法確定它是可變還是不可變呢?直接改變它也沒問題嗎?不。 正如我們?cè)诒菊麻_頭斷言的那樣,我們應(yīng)當(dāng)將所有收到的值看做可變的 —— 為了避免副作用而且保持純粹性 —— 不論它們是否可變。
回憶一下先前的這個(gè)例子:
function updateLastLogin(user) {
var newUserRecord = Object.assign( {}, user );
newUserRecord.lastLogin = Date.now();
return newUserRecord;
}
這種實(shí)現(xiàn)將 user 視為一個(gè)不應(yīng)當(dāng)被改變的值;它是不是可變的與閱讀這部分代碼無關(guān)。與這個(gè)實(shí)現(xiàn)相比較的話:
function updateLastLogin(user) {
user.lastLogin = Date.now();
return user;
}
這個(gè)版本更易于編寫,而且甚至性能更好。但這種方式不但使 updateLastLogin(..) 變得不純粹,而且它改變值的方式還使閱讀這段代碼,以及使用這段代碼的地方,更加復(fù)雜。
我們應(yīng)當(dāng)總是將 user 視為不可變的,因?yàn)樵陂喿x到代碼的這一點(diǎn)時(shí)我們不知道值從何而來,或者我們改變它的話會(huì)造成什么潛在的問題。
這種方式的一些很好的例子可以在 JS 數(shù)組的各種內(nèi)建方法中看到,比如 concat(..) 和 slice(..):
var arr = [1,2,3,4,5];
var arr2 = arr.concat( 6 );
arr; // [1,2,3,4,5]
arr2; // [1,2,3,4,5,6]
var arr3 = arr2.slice( 1 );
arr2; // [1,2,3,4,5,6]
arr3; // [2,3,4,5,6]
其他的一些將值視為不可變,并返回一個(gè)新數(shù)組而非修改的數(shù)組原型方法是:map(..) 和 filter(..)。reduce(..) / reduceRight(..) 工具也會(huì)避免修改值,雖然它們不會(huì)默認(rèn)地返回一個(gè)新數(shù)組。
不幸的是,由于一些歷史原因,有好幾個(gè)數(shù)組方法是不純粹的修改器方法:splice(..)、pop(..)、push(..)、shift(..)、unshift(..)、reverse(..)、sort(..)、和 fill(..)。
就像一些人主張的,這不應(yīng)當(dāng)被視為使用這些工具的 禁令。為了例如性能優(yōu)化之類的原因,有時(shí)你會(huì)想要使用它們。但你絕不應(yīng)該在一個(gè)對(duì)于你當(dāng)前函數(shù)來說還不是本地值的數(shù)組使用這樣的方法,以避免對(duì)代碼稍遠(yuǎn)處的其他部分造成副作用。
嚴(yán)格遵守并總是將 收到的值 視為可變的,不管它們是還是不是。這種努力將會(huì)改進(jìn)你代碼的可讀性與可信性。
總結(jié)
值的不可變性不是關(guān)于不改變值。它是隨著程序狀態(tài)的改變來創(chuàng)建并追蹤新的值,而不是修改既存的值。這種方式將在閱讀代碼時(shí)帶來更多的信心,因?yàn)槲覀兿拗屏四切顟B(tài) —— 以一些我們不愿看到或期望的方式 —— 可以改變的地方。
const 聲明(常量)常被誤認(rèn)為具有表示意圖并強(qiáng)制不可變性的能力。而實(shí)際情況是,const 基本上與值的不可變性無關(guān),而且它的使用造成的困惑很可能要比它解決的多。相反,Object.freeze(..) 提供了一種不錯(cuò)的內(nèi)建方式來在數(shù)組或?qū)ο笊显O(shè)置淺層的值不可變性。在許多情況下,這就夠了。
對(duì)于程序中性能敏感的部分,或者在改變頻繁發(fā)生的情況下,創(chuàng)建一個(gè)新數(shù)組或?qū)ο螅ㄌ貏e是如果它包含大量數(shù)據(jù)時(shí))對(duì)于處理與內(nèi)存上的考慮都是不合適的。在這樣的情況下,使用來自于 Immutable.js 這樣的庫的不可變數(shù)據(jù)結(jié)構(gòu)可能是最好的辦法。
值的不可變性在代碼可讀性上的重要性并不太關(guān)乎它對(duì)改變一個(gè)值的無能為力,而更多地在于將一個(gè)值視為可變的自律。