成熟丰满熟妇高潮XXXXX,人妻无码AV中文系列久久兔费 ,国产精品一国产精品,国精品午夜福利视频不卡麻豆

您好,歡迎來到九壹網(wǎng)。
搜索
您的當前位置:首頁翻譯連載 | JavaScript輕量級函數(shù)式編程-第6章:值的不可變性 |《你不知道的JS》姊妹篇...

翻譯連載 | JavaScript輕量級函數(shù)式編程-第6章:值的不可變性 |《你不知道的JS》姊妹篇...

來源:九壹網(wǎng)
  • 原文地址:
  • 原文作者:

第 6 章:值的不可變性

在第 5 章中,我們探討了減少副作用的重要性:副作用是引起程序意外狀態(tài)改變的原因,同時也可能會帶來意想不到的驚喜(bugs)。這樣的暗雷在程序中出現(xiàn)的越少,開發(fā)者對程序的信心無疑就會越強,同時代碼的可讀性也會越高。本章的主題,將繼續(xù)朝減少程序副作用的方向努力。

如果編程風格冪等性是指定義一個數(shù)據(jù)變更操作以便只影響一次程序狀態(tài),那么現(xiàn)在我們將注意力轉(zhuǎn)向?qū)⑦@個影響次數(shù)從 1 降為 0。

現(xiàn)在我們開始探索值的不可變性,即只在我們的程序中使用不可被改變的數(shù)據(jù)。

原始值的不可變性

原始數(shù)據(jù)類型(numberstring、booleannullundefined)本身就是不可變的;無論如何你都沒辦法改變它們。

// 無效,且毫無意義
2 = 2.5;復制代碼

然而 JS 確實有一個特性,使得看起來允許我們改變原始數(shù)據(jù)類型的值, 即“boxing”特性。當你訪問原始類型數(shù)據(jù)時 —— 特別是 number、stringboolean —— 在這種情況下,JS 會自動的把它們包裹(或者說“包裝”)成這個值對應(yīng)的對象(分別是 Number、String 以及 Boolean)。

思考下面的代碼:

var x = 2;

x.length = 4;

x;                // 2
x.length;        // undefined復制代碼

數(shù)值本身并沒有可用的 length 屬性,因此 x.length = 4 這個賦值操作正試圖添加一個新的屬性,不過它靜默地失敗了(也可以說是這個操作被忽略了或被拋棄了,這取決于你怎么看);變量 x 繼續(xù)承載那個簡單的原始類型數(shù)據(jù) —— 數(shù)值 2

但是 JS 允許 x.length = 4 這條語句正常執(zhí)行的事實著實令人困惑。如果這種現(xiàn)象真的無緣無故出現(xiàn),那么代碼的閱讀者無疑會摸不著頭腦。好消息是,如果你使用了嚴格模式("use strict";),那么這條語句就會拋出異常了。

那么如果嘗試改變那些明確被包裝成對象的值呢?

var x = new Number( 2 );

// 沒問題
x.length = 4;復制代碼

這段代碼中的 x 保存了一個對象的引用,因此可以正常地添加或修改自定義屬性。

number 這樣的原始數(shù)型,值的不可變性看起來相當明顯,但字符串呢?JS 開發(fā)者有個共同的誤解 —— 字符串和數(shù)組很像,所以應(yīng)該是可變的。JS 使用 [] 訪問字符串成員的語法甚至還暗示字符串真的就像數(shù)組。不過,字符串的確是不可變的:

var s = "hello";

s[1];                // "e"

s[1] = "E";
s.length = 10;

s;                    // "hello"復制代碼

盡管可以使用 s[1] 來像訪問數(shù)組元素一樣訪問字符串成員,JS 字符串也并不是真的數(shù)組。s[1] = "E"s.length = 10 這兩個賦值操作都是失敗的,就像剛剛的 x.length = 4 一樣。在嚴格模式下,這些賦值都會拋出異常,因為 1length 這兩個屬性在原始數(shù)據(jù)類型字符串中都是只讀的。

有趣的是,即便是包裝后的 String 對象,其值也會(在大部分情況下)表現(xiàn)的和非包裝字符串一樣 —— 在嚴格模式下如果改變已存在的屬性,就會拋出異常:

"use strict";

var s = new String( "hello" );

s[1] = "E";            // error
s.length = 10;        // error

s[42] = "?";        // OK

s;                    // "hello"復制代碼

從值到值

我們將在本節(jié)詳細展開從值到值這個概念。但在開始之前應(yīng)該心中有數(shù):值的不可變性并不是說我們不能在程序編寫時不改變某個值。如果一個程序的內(nèi)部狀態(tài)從始至終都保持不變,那么這個程序肯定相當無趣!它同樣不是指變量不能承載不同的值。這些都是對值的不可變這個概念的誤解。

值的不可變性是指當需要改變程序中的狀態(tài)時,我們不能改變已存在的數(shù)據(jù),而是必須創(chuàng)建和跟蹤一個新的數(shù)據(jù)。

例如:

function addValue(arr) {
    var newArr = [ ...arr, 4 ];
    return newArr;
}

addValue( [1,2,3] );    // [1,2,3,4]復制代碼

注意我們沒有改變數(shù)組 arr 的引用,而是創(chuàng)建了一個新的數(shù)組(newArr),這個新數(shù)組包含數(shù)組 arr 中已存在的值,并且新增了一個新值 4。

使用我們在第 5 章討論的副作用的相關(guān)概念來分析 addValue(..)。它是純的嗎?它是否具有引用透明性?給定相同的數(shù)組作為輸入,它會永遠返回相同的輸出嗎?它無副作用嗎?答案是肯定的。

設(shè)想這個數(shù)組 [1, 2, 3], 它是由先前的操作產(chǎn)生,并被我們保存在一個變量中,它代表著程序當前的狀態(tài)。我們想要計算出程序的下一個狀態(tài),因此調(diào)用了 addValue(..)。但是我們希望下一個狀態(tài)計算的行為是直接的和明確的,所以 addValue(..) 操作簡單的接收一個直接輸入,返回一個直接輸出,并通過不改變 arr 引用的原始數(shù)組來避免副作用。

這就意味著我們既可以計算出新狀態(tài) [1, 2, 3, 4],也可以掌控程序的狀態(tài)變換。程序不會出現(xiàn)過早的過渡到這個狀態(tài)或完全轉(zhuǎn)變到另一個狀態(tài)(如 [1, 2, 3, 5])這樣的意外情況。通過規(guī)范我們的值并把它視為不可變的,我們大幅減少了程序錯誤,使我們的程序更易于閱讀和推導,最終使程序更加可信賴。

arr 所引用的數(shù)組是可變的,只是我們選擇不去改變他,我們實踐了值不可變的這一精神。

同樣的,可以將“以拷貝代替改變”這樣的策略應(yīng)用于對象,思考下面的代碼:

function updateLastLogin(user) {
    var newUserRecord = Object.assign( {}, user );
    newUserRecord.lastLogin = Date.now();
    return newUserRecord;
}

var user = {
    // ..
};

user = updateLastLogin( user );復制代碼

消除本地影響

下面的代碼能夠體現(xiàn)不可變性的重要性:

var arr = [1,2,3];

foo( arr );

console.log( arr[0] );復制代碼

從表面上講,你可能認為 arr[0] 的值仍然為 1。但事實是否如此不得而知,因為 foo(..) 可能會改變你傳入其中的 arr 所引用的數(shù)組。

在之前的章節(jié)中,我們已經(jīng)見到過用下面這種帶有欺騙性質(zhì)的方法來避免意外:

var arr = [1,2,3];

foo( arr.slice() );            // 哈!一個數(shù)組副本!

console.log( arr[0] );        // 1復制代碼

當然,使得這個斷言成立的前提是 foo 函數(shù)不會忽略我們傳入的參數(shù)而直接通過相同的 arr 這個自由變量詞法引用來訪問源數(shù)組。

對于防止數(shù)據(jù)變化負面影響,稍后我們會討論另一種策略。

重新賦值

在進入下一個段落之前先思考一個問題 —— 你如何描述“常量”?

你可能會脫口而出“一個不能改變的值就是常量”,“一個不能被改變的變量”等等。這些回答都只能說接近正確答案,但卻并不是正確答案。對于常量,我們可以給出一個簡潔的定義:一個無法進行重新賦值(reassignment)的變量。

我們剛剛在“常量”概念上的吹毛求疵其實是很有必要的,因為它澄清了常量與值無關(guān)的事實。無論常量承載何值,該變量都不能使用其他的值被進行重新賦值。但它與值的本質(zhì)無關(guān)。

思考下面的代碼:

var x = 2;復制代碼

我們剛剛討論過,數(shù)據(jù) 2 是一個不可變的原始值。如果將上面的代碼改為:

const x = 2;復制代碼

const 關(guān)鍵字的出現(xiàn),作為“常量聲明”被大家熟知,事實上根本沒有改變 2 的本質(zhì),因為它本身就已經(jīng)不可改變了。

下面這行代碼會拋出錯誤,這無可厚非:

// 嘗試改變 x,祝我好運!
x = 3;        // 拋出錯誤!復制代碼

但再次重申,我們并不是要改變這個數(shù)據(jù),而是要對變量 x 進行重新賦值。數(shù)據(jù)被卷進來純屬偶然。

為了證明 const 和值的本質(zhì)無關(guān),思考下面的代碼:

const x = [ 2 ];復制代碼

這個數(shù)組是一個常量嗎?并不是。 x 是一個常量,因為它無法被重新賦值。但下面的操作是完全可行的:

x[0] = 3;復制代碼

為何?因為盡管 x 是一個常量,數(shù)組卻是可變的。

關(guān)于 const 關(guān)鍵字和“常量”只涉及賦值而不涉及數(shù)據(jù)語義的特性是個又臭又長的故事。幾乎所有語言的高級開發(fā)者都踩 const 地雷。事實上,Java 最終不贊成使用 const 并引入了一個全新的關(guān)鍵詞 final 來區(qū)分“常量”這個語義。

拋開混亂之后開始思考,如果 const 并不能創(chuàng)建一個不可變的值,那么它對于函數(shù)式編程者來說又還有什么重要的呢?

意圖

const 關(guān)鍵字可以用來告知閱讀你代碼的讀者該變量不會被重新賦值。作為一個表達意圖的標識,const 被加入 JavaScript 不僅常常受到稱贊,也普遍提高了代碼可讀性。

在我看來,這是夸大其詞,這些說法并沒有太大的實際意義。我只看到了使用這種方法來表明意圖的微薄好處。如果使用這種方法來聲明值的不可變性,與已使用幾十年的傳統(tǒng)方式相比,const 簡直太弱了。

為了證明我的說法,讓我們來做一個實踐。const 創(chuàng)建了一個在塊級作用域內(nèi)的變量,這意味著該變量只能在其所在的代碼塊中被訪問:

// 大量代碼

{
    const x = 2;

    // 少數(shù)幾行代碼
}

// 大量代碼復制代碼

通常來說,代碼塊的最佳實踐是用于僅包裹少數(shù)幾行代碼的場景。如果你有一個包含了超過 10 行的代碼塊,那么大多數(shù)開發(fā)者會建議你重構(gòu)這一段代碼。因此 const x = 2 只作用于下面的9行代碼。

程序的其他部分不會影響 x 的賦值。

我要說的是:上述程序的可讀性與下面這樣基本相同:

// 大量代碼

{
    let x = 2;

    // 少數(shù)幾行代碼
}

// 大量代碼復制代碼

其實只要查看一下在 let x = 2; 之后的幾行代碼,就可以判斷出 x 這個變量是否被重新賦值過了。對我來說,“實際上不進行重新賦值”相對“使用容易迷惑人的 const 關(guān)鍵字告訴讀者‘不要重新賦值’”是一個更明確的信號。

此外,讓我們思考一下,乍看這段代碼起來可能給讀者傳達什么:

const magicNums = [1,2,3,4];

// ..復制代碼

讀者可能會(錯誤地)認為,這里使用 const 的用意是你永遠不會修改這個數(shù)組 —— 這樣的推斷對我來說合情合理。想象一下,如果你的確允許 magicNums 這個變量所引用的數(shù)組被修改,那么這個 const 關(guān)鍵詞就極具混淆性了 —— 的很確容易發(fā)生意外,不是嗎?

更糟糕的是,如果你在某處故意修改了 magicNums,但對讀者而言不夠明顯呢?讀者會在后面的代碼里(再次錯誤地)認為 magicNums 的值仍然是 [1, 2, 3, 4]。因為他們猜測你之前使用 const 的目的就是“這個變量不會改變”。

我認為你應(yīng)該使用 varlet 來聲明那些你會去改變的變量,它們確實相比 const 來說是一個更明確的信號。

const 所帶來的問題還沒講完。還記得我們在本章開頭所說的嗎?值的不可變性是指當需要改變某個數(shù)據(jù)時,我們不應(yīng)該直接改變它,而是應(yīng)該使用一個全新的數(shù)據(jù)。那么當新數(shù)組創(chuàng)建出來后,你會怎么處理它?如果你使用 const 聲明變量來保存引用嗎,這個變量的確沒法被重新賦值了,那么……然后呢?

從這方面來講,我認為 const 反而增加了函數(shù)式編程的困難度。我的結(jié)論是:const 并不是那么有用。它不僅造成了不必要的混亂,也以一種很不方便的形式了我們。我只用 const 來聲明簡單的常量,例如:

const PI = 3.141592;復制代碼

3.141592 這個值本身就已經(jīng)是不可變的,并且我也清楚地表示說“PI 標識符將始終被用于代表這個字面量的占位符”。對我來說,這才是 const 所擅長的。坦白講,我在編碼時并不會使用很多這樣的聲明。

我寫過很多,也閱讀過很多 JavaScript 代碼,我認為由于重新賦值導致大量的 bug 這只是個想象中的問題,實際并不存在。

我們應(yīng)該擔心的,并不是變量是否被重新賦值,而是值是否會發(fā)生改變。為什么?因為值是可被攜帶的,但詞法賦值并不是。你可以向函數(shù)中傳入一個數(shù)組,這個數(shù)組可能會在你沒意識到的情況下被改變。但是你的其他代碼在預期之外重新給變量賦值,這是不可能發(fā)生的。

凍結(jié)

這是一種簡單廉價的(勉強)將像對象、數(shù)組、函數(shù)這樣的可變的數(shù)據(jù)轉(zhuǎn)為“不可變數(shù)據(jù)”的方式:

var x = Object.freeze( [2] );復制代碼

Object.freeze(..) 方法遍歷對象或數(shù)組的每個屬性和索引,將它們設(shè)置為只讀以使之不會被重新賦值,事實上這和使用 const 聲明屬性相差無幾。Object.freeze(..) 也會將屬性標記為“不可配置(non-reconfigurable)”,并且使對象或數(shù)組本身不可擴展(即不會被添加新屬性)。實際上,而就可以將對象的頂層設(shè)為不可變。

注意,僅僅是頂層不可變!

var x = Object.freeze( [ 2, 3, [4, 5] ] );

// 不允許改變:
x[0] = 42;

// oops,仍然允許改變:
x[2][0] = 42;復制代碼

Object.freeze(..) 提供淺層的、初級的不可變性約束。如果你希望更深層的不可變約束,那么你就得手動遍歷整個對象或數(shù)組結(jié)構(gòu)來為所有后代成員應(yīng)用 Object.freeze(..)

const 相反,Object.freeze(..) 并不會誤導你,讓你得到一個“你以為”不可變的值,而是真真確確給了你一個不可變的值。

回顧剛剛的例子:

var arr = Object.freeze( [1,2,3] );

foo( arr );

console.log( arr[0] );            // 1復制代碼

可以非常確定 arr[0] 就是 1

這是非常重要的,因為這可以使我們更容易的理解代碼,當我們將值傳遞到我們看不到或者不能控制的地方,我們依然能夠相信這個值不會改變。

性能

每當我們開始創(chuàng)建一個新值(數(shù)組、對象等)取代修改已經(jīng)存在的值時,很明顯迎面而來的問題就是:這對性能有什么影響?

如果每次想要往數(shù)組中添加內(nèi)容時,我們都必須創(chuàng)建一個全新的數(shù)組,這不僅占用 CPU 時間并且消耗額外的內(nèi)存。不再存在任何引用的舊數(shù)據(jù)將會被垃圾回收機制回收;更多的 CPU 資源消耗。

這樣的取舍能接受嗎?視情況而定。對代碼性能的優(yōu)化和討論都應(yīng)該有個上下文

如果在你的程序中,只會發(fā)生一次或幾次單一的狀態(tài)變化,那么扔掉一個舊對象或舊數(shù)組完全沒必要擔心。性能損失會非常非常小 —— 頂多只有幾微秒 —— 對你的應(yīng)用程序影響甚小。追蹤和修復由于數(shù)據(jù)改變引起的 bug 可能會花費你幾分鐘甚至幾小時的時間,這么看來那幾微秒簡直沒有可比性。

但是,如果頻繁的進行這樣的操作,或者這樣的操作出現(xiàn)在應(yīng)用程序的核心邏輯中,那么性能問題 —— 即性能和內(nèi)存 —— 就有必要仔細考慮一下了。

以數(shù)組這樣一個特定的數(shù)據(jù)結(jié)構(gòu)來說,我們想要在每次操作這個數(shù)組時使每個更改都隱式地進行,就像結(jié)果是一個新數(shù)組一樣,但除了每次都真的創(chuàng)建一個數(shù)組之外,還有什么其他辦法來完成這個任務(wù)呢?像數(shù)組這樣的數(shù)據(jù)結(jié)構(gòu),我們期望除了能夠保存其最原始的數(shù)據(jù),然后能追蹤其每次改變并根據(jù)之前的版本創(chuàng)建一個分支。

在內(nèi)部,它可能就像一個對象引用的鏈表樹,樹中的每個節(jié)點都表示原始值的改變。從概念上來說,這和 git 的版本控制原理類似。

想象一下使用這個假設(shè)的、專門處理數(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]復制代碼

specialArray(..) 這個數(shù)據(jù)結(jié)構(gòu)會在內(nèi)部追蹤每個數(shù)據(jù)更新操作(例如 set(..)),類似 diff,因此不必要為原始的那些值(1、2、34)重新分配內(nèi)存,而是簡單的將 "meaning of life" 這個值加入列表。重要的是,statenewState 分別指向兩個“不同版本”的數(shù)組,因此值的不變性這個語義得以保留。

發(fā)明你自己的性能優(yōu)化數(shù)據(jù)結(jié)構(gòu)是個有趣的挑戰(zhàn)。但從實用性來講,找一個現(xiàn)成的庫會是個更好的選擇。Immutable.js( 是一個很棒的選擇,它提供多種數(shù)據(jù)結(jié)構(gòu),包括 List(類似數(shù)組)和 Map(類似普通對象)。

思考下面的 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 這樣強大的庫一般會采用非常成熟的性能優(yōu)化。如果不使用庫而是手動去處理那些細枝末節(jié),開發(fā)的難度會相當大。

當改變值這樣的場景出現(xiàn)的較少且不用太關(guān)心性能時,我推薦使用更輕量級的解決方案,例如我們之前提到過的內(nèi)置的 Object.freeze(..)

以不可變的眼光看待數(shù)據(jù)

如果我們從函數(shù)中接收了一個數(shù)據(jù),但不確定這個數(shù)據(jù)是可變的還是不可變的,此時該怎么辦?去修改它試試看嗎?不要這樣做。 就像在本章最開始的時候所討論的,不論實際上接收到的值是否可變,我們都應(yīng)以它們是不可變的來對待,以此來避免副作用并使函數(shù)保持純度。

回顧一下之前的例子:

function updateLastLogin(user) {
    var newUserRecord = Object.assign( {}, user );
    newUserRecord.lastLogin = Date.now();
    return newUserRecord;
}復制代碼

該實現(xiàn)將 user 看做一個不應(yīng)該被改變的數(shù)據(jù)來對待;user 是否真的不可變完全不會影響這段代碼的閱讀。對比一下下面的實現(xiàn):

function updateLastLogin(user) {
    user.lastLogin = Date.now();
    return user;
}復制代碼

這個版本更容易實現(xiàn),性能也會更好一些。但這不僅讓 updateLastLogin(..) 變得不純,這種方式改變的值使閱讀該代碼,以及使用它的地方變得更加復雜。

應(yīng)當總是將 user 看做不可變的值,這樣我們就沒必要知道數(shù)據(jù)從哪里來,也沒必要擔心數(shù)據(jù)改變會引發(fā)潛在問題。

JavaScript 中內(nèi)置的數(shù)組方法就是一些很好的例子,例如 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]復制代碼

其他一些將參數(shù)看做不可變數(shù)據(jù)且返回新數(shù)組的原型方法還有:map(..)filter(..) 等。reduce(..) / reduceRight(..) 方法也會盡量避免改變參數(shù),盡管它們并不默認返回新數(shù)組。

不幸的是,由于歷史問題,也有一部分不純的數(shù)組原型方法:splice(..)、pop(..)push(..)、shift(..)、unshift(..)、reverse(..) 以及 fill(..)。

有些人建議禁止使用這些不純的方法,但我不這么認為。因為一些性能面的原因,某些場景下你仍然可能會用到它們。不過你也應(yīng)當注意,如果一個數(shù)組沒有被本地化在當前函數(shù)的作用域內(nèi),那么不應(yīng)當使用這些方法,避免它們所產(chǎn)生的副作用影響到代碼的其他部分。

不論一個數(shù)據(jù)是否是可變的,永遠將他們看做不可變。遵守這樣的約定,你程序的可讀性和可信賴度將會大大提升。

總結(jié)

值的不可變性并不是不改變值。它是指在程序狀態(tài)改變時,不直接修改當前數(shù)據(jù),而是創(chuàng)建并追蹤一個新數(shù)據(jù)。這使得我們在讀代碼時更有信心,因為我們了狀態(tài)改變的場景,狀態(tài)不會在意料之外或不易觀察的地方發(fā)生改變。

由于其自身的信號和意圖,const 關(guān)鍵字聲明的常量通常被誤認為是強制規(guī)定數(shù)據(jù)不可被改變。事實上,const 和值的不可變性聲明無關(guān),而且使用它所帶來的困惑似乎比它解決的問題還要大。另一種思路,內(nèi)置的 Object.freeze(..) 方法提供了頂層值的不可變性設(shè)定。大多數(shù)情況下,使用它就足夠了。

對于程序中性能敏感的部分,或者變化頻繁發(fā)生的地方,處于對計算和存儲空間的考量,每次都創(chuàng)建新的數(shù)據(jù)或?qū)ο螅ㄌ貏e是在數(shù)組或?qū)ο蟀芏鄶?shù)據(jù)時)是非常不可取的。遇到這種情況,通過類似 Immutable.js 的庫使用不可變數(shù)據(jù)結(jié)構(gòu)或許是個很棒的主意。

值不變在代碼可讀性上的意義,不在于不改變數(shù)據(jù),而在于以不可變的眼光看待數(shù)據(jù)這樣的約束。

【上一章】

【下一章】

iKcamp原創(chuàng)新書《移動Web前端高效開發(fā)實戰(zhàn)》已在亞馬遜、京東、當當開售。

因篇幅問題不能全部顯示,請點此查看更多更全內(nèi)容

Copyright ? 2019- 91gzw.com 版權(quán)所有 湘ICP備2023023988號-2

違法及侵權(quán)請聯(lián)系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市萬商天勤律師事務(wù)所王興未律師提供法律服務(wù)