国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

翻譯連載 | JavaScript 輕量級函數(shù)式編程-第5章:減少副作用 |《你不知道的JS》姊妹篇

yeyan1996 / 730人閱讀

摘要:函數(shù)式編程者并沒有消除所有的副作用。我的結(jié)論是這里的并不違反減少或避免副作用的精神。一些語言允許你指定生成隨機(jī)數(shù)的種子。因此,我們必須將內(nèi)建的隨機(jī)數(shù)生成視為不純的一方。其他的錯誤在程序運(yùn)行期間副作用可能導(dǎo)致的錯誤是多種多樣的。

原文地址:Functional-Light-JS

原文作者:Kyle Simpson-《You-Dont-Know-JS》作者

關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;分享,是 CSS 里最閃耀的一瞥;總結(jié),是 JavaScript 中最嚴(yán)謹(jǐn)?shù)倪壿嫛=?jīng)過捶打磨練,成就了本書的中文版。本書包含了函數(shù)式編程之精髓,希望可以幫助大家在學(xué)習(xí)函數(shù)式編程的道路上走的更順暢。比心。

譯者團(tuán)隊(duì)(排名不分先后):阿希、blueken、brucecham、cfanlife、dail、kyoko-df、l3ve、lilins、LittlePineapple、MatildaJin、冬青、pobusama、Cherry、蘿卜、vavd317、vivaxy、萌萌、zhouyao

第 5 章:減少副作用

在第 2 章,我們討論了一個(gè)函數(shù)除了它的返回值之外還有什么輸出。現(xiàn)在你應(yīng)該很熟悉用函數(shù)式編程的方法定義一個(gè)函數(shù)了,所以對于函數(shù)式編程的副作用你應(yīng)該有所了解。

我們將檢查各種各樣不同的副作用并且要看看他們?yōu)槭裁磿ξ覀兊拇a質(zhì)量和可讀性造成損害。

這一章的要點(diǎn)是:編寫出沒有副作用的程序是不可能的。當(dāng)然,也不是不可能,你當(dāng)然可以編寫出沒有副作用的程序。但是這樣的話程序就不會做任何有用和明顯的事情。如果你編寫出來一個(gè)零副作用的程序,你就無法區(qū)分它和一個(gè)被刪除的或者空程序的區(qū)別。

函數(shù)式編程者并沒有消除所有的副作用。實(shí)際上,我們的目標(biāo)是盡可能地限制他們。要做到這一點(diǎn),我們首先需要完全理解函數(shù)式編程的副作用。

什么是副作用

因果關(guān)系:舉一個(gè)我們?nèi)祟悓χ車澜缬绊懙淖罨尽⒆钪庇^的例子,推一下放在桌子邊沿上的一本書,書會掉落。不需要你擁有一個(gè)物理學(xué)的學(xué)位你也會知道,這是因?yàn)槟銊倓偼屏藭⑶視袈涫且驗(yàn)榈匦囊Γ@是一個(gè)明確并直接的關(guān)系。

在編程中,我們也完全會處理因果關(guān)系。如果你調(diào)用了一個(gè)函數(shù)(起因),就會在屏幕上輸出一條消息(結(jié)果)。

當(dāng)我們在閱讀程序的時(shí)候,能夠清晰明確的識別每一個(gè)起因和每一個(gè)結(jié)果是非常重要的。在某種程度上,通讀程序但不能看到因果的直接關(guān)系,程序的可讀性就會降低。

思考一下:

function foo(x) {
    return x * 2;
}

var y = foo( 3 );

在這段代碼中,有很直接的因果關(guān)系,調(diào)用值為 3 的 foo 將具有返回值 6 的效果,調(diào)用函數(shù) foo() 是起因,然后將其賦值給 y 是結(jié)果。這里沒有歧義,傳入?yún)?shù)為 3 將會返回 6,將函數(shù)結(jié)果賦值給變量 y 是結(jié)果。

但是現(xiàn)在:

function foo(x) {
    y = x * 2;
}

var y;

foo( 3 );

這段代碼有相同的輸出,但是卻有很大的差異,這里的因果是沒有聯(lián)系的。這個(gè)影響是間接的。這種方式設(shè)置 y 就是我們所說的副作用。

注意: 當(dāng)函數(shù)引用外部變量時(shí),這個(gè)變量就稱為自由變量。并不是所有的自由變量引用都是不好的,但是我們要對它們非常小心。

假使給你一個(gè)引用來調(diào)用函數(shù) bar(..),你看不到代碼,但是我告訴你這段代碼并沒有間接的副作用,只有一個(gè)顯式的 return 值會怎么樣?

bar( 4 );            // 42

因?yàn)槟阒?bar(..) 的內(nèi)部結(jié)構(gòu)不會有副作用,你可以像這樣直接地調(diào)用 bar(..)。但是如果你不知道 bar(..) 沒有副作用,為了理解調(diào)用這個(gè)函數(shù)的結(jié)果,你必須去閱讀和分析它的邏輯。這對讀者來說是額外的負(fù)擔(dān)。

有副作用的函數(shù)可讀性更低,因?yàn)樗枰嗟拈喿x來理解程序。

但是程序往往比這個(gè)要復(fù)雜,思考一下:

var x = 1;

foo();

console.log( x );

bar();

console.log( x );

baz();

console.log( x );

你能確定每次 console.log(x) 的值都是你想要的嗎?

答案是否定的。如果你不確定函數(shù) foo()bar()baz() 是否有副作用,你就不能保證每一步的 x 將會是什么,除非你檢查每個(gè)步驟的實(shí)現(xiàn),然后從第一行開始跟蹤程序,跟蹤所有狀態(tài)的改變。

換句話說,console.log(x) 最后的結(jié)果是不能分析和預(yù)測的,除非你已經(jīng)在心里將整個(gè)程序執(zhí)行到這里了。

猜猜誰擅長運(yùn)行你的程序?JS 引擎。猜猜誰不擅長運(yùn)行你的程序?你代碼的讀者。然而,如果你選擇在一個(gè)或多個(gè)函數(shù)調(diào)用中編寫帶有(潛在)副作用的代碼,那么這意味著你已經(jīng)使你的讀者必須將你的程序完整地執(zhí)行到某一行,以便他們理解這一行。

如果 foo()bar()、和?baz() 都沒有副作用的話,它們就不會影響到 x,這就意味著我們不需要在心里默默地執(zhí)行它們并且跟蹤 x 的變化。這在精力上負(fù)擔(dān)更小,并且使得代碼更加地可讀。

潛在的原因

輸出和狀態(tài)的變化,是最常被引用的副作用的表現(xiàn)。但是另一個(gè)有損可讀性的實(shí)踐是一些被認(rèn)為的側(cè)因,思考一下:

function foo(x) {
    return x + y;
}

var y = 3;

foo( 1 );            // 4

y 不會隨著 foo(..) 改變,所以這和我們之前看到的副作用有所不同。但是現(xiàn)在,對函數(shù) foo(..) 的調(diào)用實(shí)際上取決于 y 當(dāng)前的狀態(tài)。之后我們?nèi)绻@樣做:

y = 5;

// ..

foo( 1 );            // 6

我們可能會感到驚訝兩次調(diào)用 foo(1) 返回的結(jié)果不一樣。

foo(..) 對可讀性有一個(gè)間接的破壞性。如果沒有對函數(shù) foo(..) 進(jìn)行仔細(xì)檢查,使用者可能不會知道導(dǎo)致這個(gè)輸出的原因。這看起來僅僅像是參數(shù) 1 的原因,但卻不是這樣的。

為了幫助可讀性,所有決定 foo(..) 輸出的原因應(yīng)該被設(shè)置的直接并明顯。函數(shù)的使用者將會直接看到原因和結(jié)果。

使用固定的狀態(tài)

避免副作用就意味著函數(shù) foo(..) 不能引用自由變量了嗎?

思考下這段代碼:

function foo(x) {
    return x + bar( x );
}

function bar(x) {
    return x * 2;
}

foo( 3 );            // 9

很明顯,對于函數(shù) foo(..) 和函數(shù) bar(..),唯一和直接的原因就是參數(shù) x。但是 bar(x) 被稱為什么呢?bar 僅僅只是一個(gè)標(biāo)識符,在 JS 中,默認(rèn)情況下,它甚至不是一個(gè)常量(不可重新分配的變量)。foo(..) 函數(shù)依賴于 bar 的值,bar 作為一個(gè)自由變量被第二個(gè)函數(shù)引用。

所以說這個(gè)函數(shù)還依賴于其他的原因嗎?

我認(rèn)為不。雖然可以用其他的函數(shù)來重寫 bar 這個(gè)變量,但是在代碼中我沒有這樣做,這也不是我的慣例或先例。無論出于什么意圖和目的,我的函數(shù)都是常量(從不重新分配)。

思考一下:

const PI = 3.141592;

function foo(x) {
    return x * PI;
}

foo( 3 );            // 9.424776000000001

注意: JavaScript 有內(nèi)置的 Math.PI 屬性,所以我們在本文中僅僅是用 PI 做一個(gè)方便的說明。在實(shí)踐中,總是使用 Math.PI 而不是你自己定義的。

上面的代碼怎么樣呢?PI 是函數(shù) foo(..) 的一個(gè)副作用嗎?

兩個(gè)觀察結(jié)果將會合理地幫助我們回答這個(gè)問題:

想一下是否每次調(diào)用 foo(3),都將會返回 9.424..答案是肯定的。 如果每一次都給一個(gè)相同的輸入(x),那么都將會返回相同的輸出。

你能用 PI 的當(dāng)前值來代替每一個(gè) PI 嗎,并且程序能夠和之前一樣正確地的運(yùn)行嗎?是的。 程序沒有任何一部分依賴于 PI 值的改變,因?yàn)?PI 的類型是 const,它是不能再分配的,所以變量 PI 在這里只是為了便于閱讀和維護(hù)。它的值可以在不改變程序行為的情況下內(nèi)聯(lián)。

我的結(jié)論是:這里的 PI 并不違反減少或避免副作用的精神。在之前的代碼也沒有調(diào)用 bar(x)

在這兩種情況下,PIbar 都不是程序狀態(tài)的一部分。它們是固定的,不可重新分配的(“常量”)的引用。如果他們在整個(gè)程序中都不改變,那么我們就不需要擔(dān)心將他們作為變化的狀態(tài)追蹤他們。同樣的,他們不會損害程序的可讀性。而且它們也不會因?yàn)樽兞恳圆豢深A(yù)測的方式變化,而成為錯誤的源頭。

注意: 在我看來,使用 const 并不能說明 PI 不是副作用;使用 var PI 也會是同樣的結(jié)果。PI 沒有被重新分配是問題的關(guān)鍵,而不是使用 const。我們將在后面的章節(jié)討論 const

隨機(jī)性

你以前可能從來沒有考慮過,但是隨機(jī)性是不純的。一個(gè)使用 Math.random() 的函數(shù)永遠(yuǎn)都不是純的,因?yàn)槟悴荒芨鶕?jù)它的輸入來保證和預(yù)測它的輸出。所以任何生成唯一隨機(jī)的 ID 等都需要依靠程序的其他原因。

在計(jì)算中,我們使用的是偽隨機(jī)算法。事實(shí)證明,真正的隨機(jī)是非常難的,所以我們只是用復(fù)雜的算法來模擬它,產(chǎn)生的值看起來是隨機(jī)的。這些算法計(jì)算很長的一串?dāng)?shù)字,但秘密是,如果你知道起始點(diǎn),實(shí)際上這個(gè)序列是可以預(yù)測的。這個(gè)起點(diǎn)被稱之為種子。

一些語言允許你指定生成隨機(jī)數(shù)的種子。如果你總是指定了相同的種子,那么你將始終從后續(xù)的“隨機(jī)數(shù)”中得到相同的輸出序列。這對于測試是非常有用的,但是在真正的應(yīng)用中使用也是非常危險(xiǎn)的。

在 JS 中,Math.random() 的隨機(jī)性計(jì)算是基于間接輸入,因?yàn)槟悴荒苊鞔_種子。因此,我們必須將內(nèi)建的隨機(jī)數(shù)生成視為不純的一方。

I/O 效果

這可能不太明顯,但是最常見(并且本質(zhì)上不可避免)的副作用就是 I/O(輸入/輸出)。一個(gè)沒有 I/O 的程序是完全沒有意義的,因?yàn)樗墓ぷ鞑荒芤匀魏畏绞奖挥^察到。一個(gè)有用的程序必須最少有一個(gè)輸出,并且也需要輸入。輸入會產(chǎn)生輸出。

用戶事件(鼠標(biāo)、鍵盤)是 JS 編程者在瀏覽器中使用的典型的輸入,而輸出的則是 DOM。如果你使用 Node.js 比較多,你更有可能接收到和輸出到文件系統(tǒng)、網(wǎng)絡(luò)系統(tǒng)和/或者 stdin / stdout(標(biāo)準(zhǔn)輸入流/標(biāo)準(zhǔn)輸出流)的輸入和輸出。

事實(shí)上,這些來源既可以是輸入也可以是輸出,是因也是果。以 DOM 為例,我們更新(產(chǎn)生副作用的結(jié)果)一個(gè) DOM 元素為了給用戶展示文字或圖片信息,但是 DOM 的當(dāng)前狀態(tài)是對這些操作的隱式輸入(產(chǎn)生副作用的原因)。

其他的錯誤

在程序運(yùn)行期間副作用可能導(dǎo)致的錯誤是多種多樣的。讓我們來看一個(gè)場景來說明這些危害,希望它們能幫助我們辨認(rèn)出在我們自己的程序中類似的錯誤。

思考一下:

var users = {};
var userOrders = {};

function fetchUserData(userId) {
    ajax( "http://some.api/user/" + userId, function onUserData(userData){
        users[userId] = userData;
    } );
}

function fetchOrders(userId) {
    ajax( "http://some.api/orders/" + userId, function onOrders(orders){
        for (let i = 0; i < orders.length; i++) {
                // 對每個(gè)用戶的最新訂單保持引用
            users[userId].latestOrder = orders[i];
            userOrders[orders[i].orderId] = orders[i];
        }
    } );
}

function deleteOrder(orderId) {
    var user = users[ userOrders[orderId].userId ];
    var isLatestOrder = (userOrders[orderId] == user.latestOrder);
    
    // 刪除用戶的最新訂單?
    if (isLatestOrder) {
        hideLatestOrderDisplay();
    }

    ajax( "http://some.api/delete/order/" + orderId, function onDelete(success){
        if (success) {
         ? ? ? ?// 刪除用戶的最新訂單?
            if (isLatestOrder) {
                user.latestOrder = null;
            }

            userOrders[orderId] = null;
        }
        else if (isLatestOrder) {
            showLatestOrderDisplay();
        }
    } );
}

我敢打賭,一些讀者顯然會發(fā)現(xiàn)其中潛在的錯誤。如果回調(diào) onOrders(..) 在回調(diào) onUserData(..) 之前運(yùn)行,它會給一個(gè)尚未設(shè)置的值(users[userId]userData 對象)添加一個(gè) latestOrder 屬性

因此,這種依賴于因果關(guān)系的“錯誤”是在兩種不同操作(是否異步)紊亂情況下發(fā)生的,我們期望以確定的順序運(yùn)行,但在某些情況下,可能會以不同的順序運(yùn)行。有一些策略可以確保操作的順序,很明顯,在這種情況下順序是至關(guān)重要的。

這里還有另一個(gè)細(xì)小的錯誤,你發(fā)現(xiàn)了嗎?

思考下這個(gè)調(diào)用順序:

fetchUserData( 123 );
onUserData(..);
fetchOrders( 123 );
onOrders(..);

// later

fetchOrders( 123 );
deleteOrder( 456 );
onOrders(..);
onDelete(..);

你發(fā)現(xiàn)每一對 fetchOrders(..) / onOrders(..)deleteOrder(..) / onDelete(..) 都是交替出現(xiàn)了嗎?這個(gè)潛在的排序會伴隨著我們狀態(tài)管理的側(cè)因/副作用暴露出一個(gè)古怪的狀態(tài)。

在設(shè)置 isLatestOrder 標(biāo)志和使用它來決定是否應(yīng)該清空 users 中的用戶數(shù)據(jù)對象的 latestOrder 屬性時(shí),會有一個(gè)延遲(因?yàn)榛卣{(diào))。在此延遲期間,如果 onOrders(..) 銷毀,它可以潛在地改變用戶的 latestOrder 引用的順序值。當(dāng) onDelete(..) 在銷毀之后,它會假定它仍然需要重新引用 latestOrder

錯誤:數(shù)據(jù)(狀態(tài))可能不同步。當(dāng)進(jìn)入 onOrders(..) 時(shí),latestOrder 可能仍然指向一個(gè)較新的順序,這樣 latestOrder 就會被重置。

這種錯誤最糟糕的是你不能和其他錯誤一樣得到程序崩潰的異常。我們只是有一個(gè)不正確的狀態(tài),同時(shí)我們的應(yīng)用程序“默默地”崩潰。

fetchUserData(..)fetchOrders(..) 的序列依賴是相當(dāng)明顯的,并且被直截了當(dāng)?shù)靥幚怼5牵?fetchOrders(..)deleteOrder(..) 之間存在潛在的序列依賴關(guān)系,就不太清楚了。這兩個(gè)似乎更加獨(dú)立。并且確保他們的順序被保留是比較棘手的,因?yàn)槟闶孪炔恢溃ㄔ?fetchOrders(..) 產(chǎn)生結(jié)果之前)是否必須要按照這樣的順序執(zhí)行。

是的,一旦 deleteOrder(..) 銷毀,你就能重新計(jì)算 isLatestOrder 標(biāo)志。但是現(xiàn)在你有另一個(gè)問題:你的 UI 狀態(tài)可能不同步。

如果你之前已經(jīng)調(diào)用過 hideLatestOrderDisplay(),現(xiàn)在你需要調(diào)用 showLatestOrderDisplay(),但是如果一個(gè)新的 latestOrder 已經(jīng)被設(shè)置好了,你將要跟蹤至少三個(gè)狀態(tài):被刪除的狀態(tài)是否本來是“最新的”、是否是“最新”設(shè)置的,和這兩個(gè)順序有什么不同嗎?這些都是可以解決的問題,但無論如何都是不明顯的。

所有這些麻煩都是因?yàn)槲覀儧Q定在一組共享的狀態(tài)下構(gòu)造出有副作用的代碼。

函數(shù)式編程人員討厭這類因果的錯誤,因?yàn)檫@有損我們的閱讀、推理、驗(yàn)證和最終相信代碼的能力。這就是為什么他們要如此嚴(yán)肅地對待避免副作用的原因。

有很多避免/修復(fù)副作用的策略。我們將在本章后面和后面的章節(jié)中討論。我要說一個(gè)確定的事情:寫出有副作用/效果的代碼是很正常的, 所以我們需要謹(jǐn)慎和刻意地避免產(chǎn)生有副作用的代碼。

一次就好

如果你必須要使用副作用來改變狀態(tài),那么一種對限制潛在問題有用的操作是冪等。如果你的值的更新是冪次的,那么數(shù)據(jù)將會適應(yīng)你可能有不同副作用來源的多個(gè)此類更新的情況。

冪等的定義有點(diǎn)讓人困惑,同時(shí)數(shù)學(xué)家和程序員使用冪等的含義稍有不同。然而,這兩種觀點(diǎn)對于函數(shù)式編程人員都是有用的。

首先,讓我們給出一個(gè)計(jì)數(shù)器的例子,它既不是數(shù)學(xué)上的,也不是程序上的冪等:

function updateCounter(obj) {
    if (obj.count < 10) {
        obj.count++;
        return true;
    }

    return false;
}

這個(gè)函數(shù)通過引用遞增 obj.count 來該改變一個(gè)對象,所以對這個(gè)對象產(chǎn)生了副作用。當(dāng) o.count 小于 10 時(shí),如果 updateCounter(o) 被多次調(diào)用,即程序狀態(tài)每次都要更改。另外,updateCounter(..) 的輸出是一個(gè)布爾值,這不適合返回到 updateCounter(..) 的后續(xù)調(diào)用。

數(shù)學(xué)中的冪等

從數(shù)學(xué)的角度來看,冪等指的是在第一次調(diào)用后,如果你將該輸出一次又一次地輸入到操作中,其輸出永遠(yuǎn)不會改變的操作。換句話說,foo(x) 將產(chǎn)生與 foo(foo(x))foo(foo(foo(x))) 等相同的輸出。

一個(gè)典型的數(shù)學(xué)例子是 Math.abs(..)(取絕對值)。Math.abs(-2) 的結(jié)果是 2,和 Math.abs(Math.abs(Math.abs(Math.abs(-2)))) 的結(jié)果相同。像Math.min(..)Math.max(..)Math.round(..)Math.floor(..)Math.ceil(..)這些工具函數(shù)都是冪等的。

我們可以用同樣的特征來定義一些數(shù)學(xué)運(yùn)算:

function toPower0(x) {
    return Math.pow( x, 0 );
}

function snapUp3(x) {
    return x - (x % 3) + (x % 3 > 0 && 3);
}

toPower0( 3 ) == toPower0( toPower0( 3 ) );            // true

snapUp3( 3.14 ) == snapUp3( snapUp3( 3.14 ) );        // true

數(shù)學(xué)上的冪等僅限于數(shù)學(xué)運(yùn)算。我們還可以用 JavaScript 的原始類型來說明冪等的另一種形式:

var x = 42, y = "hello";

String( x ) === String( String( x ) );                // true

Boolean( y ) === Boolean( Boolean( y ) );            // true

在本文的前面,我們探究了一種常見的函數(shù)式編程工具,它可以實(shí)現(xiàn)這種形式的冪等:

identity( 3 ) === identity( identity( 3 ) );    // true

某些字符串操作自然也是冪等的,例如:

function upper(x) {
    return x.toUpperCase();
}

function lower(x) {
    return x.toLowerCase();
}

var str = "Hello World";

upper( str ) == upper( upper( str ) );                // true

lower( str ) == lower( lower( str ) );                // true

我們甚至可以以一種冪等方式設(shè)計(jì)更復(fù)雜的字符串格式操作,比如:

function currency(val) {
    var num = parseFloat(
        String( val ).replace( /[^d.-]+/g, "" )
    );
    var sign = (num < 0) ? "-" : "";
    return `${sign}$${Math.abs( num ).toFixed( 2 )}`;
}

currency( -3.1 );                                    // "-$3.10"

currency( -3.1 ) == currency( currency( -3.1 ) );    // true

currency(..) 舉例說明了一個(gè)重要的技巧:在某些情況下,開發(fā)人員可以采取額外的步驟來規(guī)范化輸入/輸出操作,以確保操作是冪等的來避免意外的發(fā)生。

在任何可能的情況下通過冪等的操作限制副作用要比不做限制的更新要好得多。

編程中的冪等

冪等的面向程序的定義也是類似的,但不太正式。編程中的冪等僅僅是 f(x); 的結(jié)果與 f(x); f(x) 相同而不是要求 f(x) === f(f(x))。換句話說,之后每一次調(diào)用 f(x) 的結(jié)果和第一次調(diào)用 f(x) 的結(jié)果沒有任何改變。

這種觀點(diǎn)更符合我們對副作用的觀察。因?yàn)檫@更像是一個(gè) f(..) 創(chuàng)建了一個(gè)冪等的副作用而不是必須要返回一個(gè)冪等的輸出值。

這種冪等性的方式經(jīng)常被用于 HTTP 操作(動詞),例如 GET 或 PUT。如果 HTTP REST API 正確地遵循了冪等的規(guī)范指導(dǎo),那么 PUT 被定義為一個(gè)更新操作,它可以完全替換資源。同樣的,客戶端可以一次或多次發(fā)送 PUT 請求(使用相同的數(shù)據(jù)),而服務(wù)器無論如何都將具有相同的結(jié)果狀態(tài)。

讓我們用更具體的編程方法來考慮這個(gè)問題,來檢查一下使用冪等和沒有使用冪等是否產(chǎn)生副作用:

// 冪等的:
obj.count = 2;
a[a.length - 1] = 42;
person.name = upper( person.name );

// 非冪等的:
obj.count++;
a[a.length] = 42;
person.lastUpdated = Date.now();

記住:這里的冪等性的概念是每一個(gè)冪等運(yùn)算(比如 obj.count = 2)可以重復(fù)多次,而不是在第一次更新后改變程序操作。非冪等操作每次都改變狀態(tài)。

那么更新 DOM 呢?

var hist = document.getElementById( "orderHistory" );

// 冪等的:
hist.innerHTML = order.historyText;

// 非冪等的:
var update = document.createTextNode( order.latestUpdate );
hist.appendChild( update );

這里的關(guān)鍵區(qū)別在于,冪等的更新替換了 DOM 元素的內(nèi)容。DOM 元素的當(dāng)前狀態(tài)是獨(dú)立的,因?yàn)樗菬o條件覆蓋的。非冪等的操作將內(nèi)容添加到元素中;隱式地,DOM 元素的當(dāng)前狀態(tài)是計(jì)算下一個(gè)狀態(tài)的一部分。

我們將不會一直用冪等的方式去定義你的數(shù)據(jù),但如果你能做到,這肯定會減少你的副作用在你最意想不到的時(shí)候突然出現(xiàn)的可能性。

純粹的快樂

沒有副作用的函數(shù)稱為純函數(shù)。在編程的意義上,純函數(shù)是一種冪等函數(shù),因?yàn)樗豢赡苡腥魏胃弊饔谩K伎家幌拢?/p>

function add(x,y) {
    return x + y;
}

所有輸入(xy)和輸出(return ..)都是直接的,沒有引用自由變量。調(diào)用 add(3,4) 多次和調(diào)用一次是沒有區(qū)別的。add(..) 是純粹的編程風(fēng)格的冪等。

然而,并不是所有的純函數(shù)都是數(shù)學(xué)概念上的冪等,因?yàn)樗鼈兎祷氐闹挡灰欢ㄟm合作為再次調(diào)用它們時(shí)的輸入。思考一下:

function calculateAverage(list) {
    var sum = 0;
    for (let i = 0; i < list.length; i++) {
        sum += list[i];
    }
    return sum / list.length;
}

calculateAverage( [1,2,4,7,11,16,22] );            // 9

輸出的 9 并不是一個(gè)數(shù)組,所以你不能在 calculateAverage(calculateAverage(..)) 中將其傳入。

正如我們前面所討論的,一個(gè)純函數(shù)可以引用自由變量,只要這些自由變量不是側(cè)因。

例如:

const PI = 3.141592;

function circleArea(radius) {
    return PI * radius * radius;
}

function cylinderVolume(radius,height) {
    return height * circleArea( radius );
}

circleArea(..) 中引用了自由變量 PI,但是這是一個(gè)常量所以不是一個(gè)側(cè)因。cylinderVolume(..) 引用了自由變量 circleArea,這也不是一個(gè)側(cè)因,因?yàn)檫@個(gè)程序把它當(dāng)作一個(gè)常量引用它的函數(shù)值。這兩個(gè)函數(shù)都是純的。

另一個(gè)例子,一個(gè)函數(shù)仍然可以是純的,但引用的自由變量是閉包:

function unary(fn) {
    return function onlyOneArg(arg){
        return fn( arg );
    };
}

unary(..) 本身顯然是純函數(shù) —— 它唯一的輸入是 fn,并且它唯一的輸出是返回的函數(shù),但是閉合了自由變量 fn 的內(nèi)部函數(shù) onlyOneArg(..) 是不是純的呢?

它仍然是純的,因?yàn)?fn 永遠(yuǎn)不變。事實(shí)上,我們對這一事實(shí)有充分的自信,因?yàn)閺脑~法上講,這幾行是唯一可能重新分配 fn 的代碼。

注意: fn 是一個(gè)函數(shù)對象的引用,它默認(rèn)是一個(gè)可變的值。在程序的其他地方可能為這個(gè)函數(shù)對象添加一個(gè)屬性,這在技術(shù)上“改變”這個(gè)值(改變,而不是重新分配)。然而,因?yàn)槲覀兂苏{(diào)用 fn,不依賴 fn 以外的任何事情,并且不可能影響函數(shù)值的可調(diào)用性,因此 fn 在最后的結(jié)果中仍然是有效的不變的;它不可能是一個(gè)側(cè)因。

表達(dá)一個(gè)函數(shù)的純度的另一種常用方法是:給定相同的輸入(一個(gè)或多個(gè)),它總是產(chǎn)生相同的輸出。 如果你把 3 傳給 circleArea(..) 它總是輸出相同的結(jié)果(28.274328)。

如果一個(gè)函數(shù)每次在給予相同的輸入時(shí),可能產(chǎn)生不同的輸出,那么它是不純的。即使這樣的函數(shù)總是返回相同的值,只要它產(chǎn)生間接輸出副作用,并且程序狀態(tài)每次被調(diào)用時(shí)都會被改變,那么這就是不純的。

不純的函數(shù)是不受歡迎的,因?yàn)樗鼈兪沟盟械恼{(diào)用都變得更加難以理解。純的函數(shù)的調(diào)用是完全可預(yù)測的。當(dāng)有人閱讀代碼時(shí),看到多個(gè) circleArea(3) 調(diào)用,他們不需要花費(fèi)額外的精力來計(jì)算每次的輸出結(jié)果。

相對的純粹

當(dāng)我們討論一個(gè)函數(shù)是純的時(shí),我們必須非常小心。JavaScript 的動態(tài)值特性使其很容易產(chǎn)生不明顯的副作用。

思考一下:

function rememberNumbers(nums) {
    return function caller(fn){
        return fn( nums );
    };
}

var list = [1,2,3,4,5];

var simpleList = rememberNumbers( list );

simpleList(..) 看起來是一個(gè)純函數(shù),因?yàn)樗簧婕皟?nèi)部的 caller(..) 函數(shù),它僅僅是閉合了自由變量 nums。然而,有很多方法證明 simpleList(..) 是不純的。

首先,我們對純度的斷言是基于數(shù)組的值(通過 listnums 引用)一直不改變:

function median(nums) {
    return (nums[0] + nums[nums.length - 1]) / 2;
}

simpleList( median );        // 3

// ..

list.push( 6 );

// ..

simpleList( median );        // 3.5

當(dāng)我們改變數(shù)組時(shí),simpleList(..) 的調(diào)用改變它的輸出。所以,simpleList(..) 是純的還是不純的呢?這就取決于你的視角。對于給定的一組假設(shè)來說,它是純函數(shù)。在任何沒有 list.push(6) 的情況下是純的。

我們可以通過改變 rememberNumbers(..) 的定義來修改這種不純。一種方法是復(fù)制 nums 數(shù)組:

function rememberNumbers(nums) {
        // 復(fù)制一個(gè)數(shù)組
    nums = nums.slice();

    return function caller(fn){
        return fn( nums );
    };
}

但這可能會隱含一個(gè)更棘手的副作用:

var list = [1,2,3,4,5];

// 把 list[0] 作為一個(gè)有副作用的接收者
Object.defineProperty(
    list,
    0,
    {
        get: function(){
            console.log( "[0] was accessed!" );
            return 1;
        }
    }
);

var simpleList = rememberNumbers( list );

// [0] 已經(jīng)被使用!

一個(gè)更粗魯?shù)倪x擇是更改 rememberNumbers(..) 的參數(shù)。首先,不要接收數(shù)組,而是把數(shù)字作為多帶帶的參數(shù):

function rememberNumbers(...nums) {
    return function caller(fn){
        return fn( nums );
    };
}

var simpleList = rememberNumbers( ...list );

// [0] 已經(jīng)被使用!

這兩個(gè) ... 的作用是將列表復(fù)制到 nums 中,而不是通過引用來傳遞。

注意: 控制臺消息的副作用不是來自于 rememberNumbers(..),而是 ...list 的擴(kuò)展中。因此,在這種情況下,rememberNumbers(..)simpleList(..) 是純的。

但是如果這種突變更難被發(fā)現(xiàn)呢?純函數(shù)和不純的函數(shù)的合成總是產(chǎn)生不純的函數(shù)。如果我們將一個(gè)不純的函數(shù)傳遞到另一個(gè)純函數(shù) simpleList(..) 中,那么這個(gè)函數(shù)就是不純的:

// 是的,一個(gè)愚蠢的人為的例子 :)
function firstValue(nums) {
    return nums[0];
}

function lastValue(nums) {
    return firstValue( nums.reverse() );
}

simpleList( lastValue );    // 5

list;                        // [1,2,3,4,5] -- OK!

simpleList( lastValue );    // 1

注意: 不管 reverse() 看起來多安全(就像 JS 中的其他數(shù)組方法一樣),它返回一個(gè)反向數(shù)組,實(shí)際上它對數(shù)組進(jìn)行了修改,而不是創(chuàng)建一個(gè)新的數(shù)組。

我們需要對 rememberNumbers(..) 下一個(gè)更斬釘截鐵的定義來防止 fn(..) 改變它的閉合的 nums 變量的引用。

function rememberNumbers(...nums) {
    return function caller(fn){
     ? ? ? ?// 提交一個(gè)副本!
        return fn( nums.slice() );
    };
}

所以 simpleList(..) 是可靠的純函數(shù)嗎!?不。 :(

我們只防范我們可以控制的副作用(通過引用改變)。我們傳遞的任何帶有副作用的函數(shù),都將會污染 simpleList(..) 的純度:

simpleList( function impureIO(nums){
    console.log( nums.length );
} );

事實(shí)上,沒有辦法定義 rememberNumbers(..) 去產(chǎn)生一個(gè)完美純粹的 simpleList(..) 函數(shù)。

純度是和自信是有關(guān)的。但我們不得不承認(rèn),在很多情況下,我們所感受到的自信實(shí)際上是與我們程序的上下文和我們對程序了解有關(guān)的。在實(shí)踐中(在 JavaScript 中),函數(shù)純度的問題不是純粹的純粹性,而是關(guān)于其純度的一系列信心。

越純潔越好。制作純函數(shù)時(shí)越努力,當(dāng)您閱讀使用它的代碼時(shí),你的自信就會越高,這將使代碼的一部分更加可讀。

有或者無

到目前為止,我們已經(jīng)將函數(shù)純度定義為一個(gè)沒有副作用的函數(shù),并且作為這樣一個(gè)函數(shù),給定相同的輸入,總是產(chǎn)生相同的輸出。這只是看待相同特征的兩種不同方式。

但是,第三種看待函數(shù)純性的方法,也許是廣為接受的定義,即純函數(shù)具有引用透明性。

引用透明性是指一個(gè)函數(shù)調(diào)用可以被它的輸出值所代替,并且整個(gè)程序的行為不會改變。換句話說,不可能從程序的執(zhí)行中分辨出函數(shù)調(diào)用是被執(zhí)行的,還是它的返回值是在函數(shù)調(diào)用的位置上內(nèi)聯(lián)的。

從引用透明的角度來看,這兩個(gè)程序都有完全相同的行為因?yàn)樗鼈兌际怯眉兇獾暮瘮?shù)構(gòu)建的:

function calculateAverage(list) {
    var sum = 0;
    for (let i = 0; i < list.length; i++) {
        sum += list[i];
    }
    return sum / list.length;
}

var nums = [1,2,4,7,11,16,22];

var avg = calculateAverage( nums );

console.log( "The average is:", avg );        // The average is: 9
function calculateAverage(list) {
    var sum = 0;
    for (let i = 0; i < list.length; i++) {
        sum += list[i];
    }
    return sum / list.length;
}

var nums = [1,2,4,7,11,16,22];

var avg = 9;

console.log( "The average is:", avg );        // The average is: 9

這兩個(gè)片段之間的唯一區(qū)別在于,在后者中,我們跳過了調(diào)用 calculateAverage(nums) 并內(nèi)聯(lián)。因?yàn)槌绦虻钠渌糠值男袨槭窍嗤模?b>calculateAverage(..) 是引用透明的,因此是一個(gè)純粹的函數(shù)。

思考上的透明

一個(gè)引用透明的純函數(shù)可能會被它的輸出替代,這并不意味著它應(yīng)該被替換。遠(yuǎn)非如此。

我們用在程序中使用函數(shù)而不是使用預(yù)先計(jì)算好的常量的原因不僅僅是應(yīng)對變化的數(shù)據(jù),也是和可讀性和適當(dāng)?shù)某橄蟮扔嘘P(guān)。調(diào)用函數(shù)去計(jì)算一列數(shù)字的平均值讓這部分程序比只是使用確定的值更具有可讀性。它向讀者講述了 avg 從何而來,它意味著什么,等等。

我們真正建議使用引用透明是當(dāng)你閱讀程序,一旦你已經(jīng)在內(nèi)心計(jì)算出純函數(shù)調(diào)用輸出的是什么的時(shí)候,當(dāng)你看到它的代碼的時(shí)候不需要再去思考確切的函數(shù)調(diào)用是做什么,特別是如果它出現(xiàn)很多次。

這個(gè)結(jié)果有一點(diǎn)像你在心里面定義一個(gè) const,當(dāng)你閱讀的時(shí)候,你可以直接跳過并且不需要花更多的精力去計(jì)算。

我們希望純函數(shù)的這種特性的重要性是顯而易見的。我們正在努力使我們的程序更容易讀懂。我們能做的一種方法是給讀者較少的工作,通過提供幫助來跳過不必要的東西,這樣他們就可以把注意力集中在重要的事情上。

讀者不需要重新計(jì)算一些不會改變(也不需要改變)的結(jié)果。如果用引用透明定義一個(gè)純函數(shù),讀者就不必這樣做了。

不夠透明?

那么如果一個(gè)有副作用的函數(shù),并且這個(gè)副作用在程序的其他地方?jīng)]有被觀察到或者依賴會怎么樣?這個(gè)功能還具有引用透明性嗎?

這里有一個(gè)例子:

function calculateAverage(list) {
    sum = 0;
    for (let i = 0; i < list.length; i++) {
        sum += list[i];
    }
    return sum / list.length;
}

var sum, nums = [1,2,4,7,11,16,22];

var avg = calculateAverage( nums );

你發(fā)現(xiàn)了嗎?

sum 是一個(gè) calculateAverage(..) 使用的外部自由變量。但是,每次我們使用相同的列表調(diào)用 calculateAverage(..),我們將得到 9 作為輸出。并且這個(gè)程序無法和使用參數(shù) 9 調(diào)用 calculateAverage(nums) 在行為上區(qū)分開來。程序的其他部分和 sum 變量有關(guān),所以這是一個(gè)不可觀察的副作用。
這是一個(gè)像這棵樹一樣不能觀察到的副作用嗎?

假如一棵樹在森林里倒下而沒有人在附近聽見,它有沒有發(fā)出聲音?

通過引用透明的狹義的定義,我想你一定會說 calculateAverage(..) 仍然是一個(gè)純函數(shù)。但是,因?yàn)樵谖覀兊膶W(xué)習(xí)中不僅僅是學(xué)習(xí)學(xué)術(shù),而且與實(shí)用主義相平衡,我認(rèn)為這個(gè)結(jié)論需要更多的觀點(diǎn)。讓我們探索一下。

性能影響

你經(jīng)常會發(fā)現(xiàn)這些不易觀察的副作用被用于性能優(yōu)化的操作。例如:

var cache = [];

function specialNumber(n) {
 ? ? ? ?// 如果我們已經(jīng)計(jì)算過這個(gè)特殊的數(shù),
    // 跳過這個(gè)操作,然后從緩存中返回
    if (cache[n] !== undefined) {
        return cache[n];
    }

    var x = 1, y = 1;

    for (let i = 1; i <= n; i++) {
        x += i % 2;
        y += i % 3;
    }

    cache[n] = (x * y) / (n + 1);

    return cache[n];
}

specialNumber( 6 );                // 4
specialNumber( 42 );            // 22
specialNumber( 1E6 );            // 500001
specialNumber( 987654321 );        // 493827162

這個(gè)愚蠢的 specialNumber(..) 算法是確定性的,并且,純函數(shù)從定義來說,它總是為相同的輸入提供相同的輸出。從引用透明的角度來看 —— 用 22 替換對 specialNumber(42) 的任何調(diào)用,程序的最終結(jié)果是相同的。

但是,這個(gè)函數(shù)必須做一些工作來計(jì)算一些較大的數(shù)字,特別是輸入像 987654321 這樣的數(shù)字。如果我們需要在我們的程序中多次獲得特定的特殊號碼,那么結(jié)果的緩存意味著后續(xù)的調(diào)用效率會更高。

注意: 思考一個(gè)有趣的事情:CPU 在執(zhí)行任何給定操作時(shí)產(chǎn)生的熱量,即使是最純粹的函數(shù) / 程序,也是不可避免的副作用嗎?那么 CPU 的時(shí)間延遲,因?yàn)樗〞r(shí)間在一個(gè)純操作上,然后再執(zhí)行另一個(gè)操作,是否也算作副作用?

不要這么快地做出假設(shè),你僅僅運(yùn)行 specialNumber(987654321) 計(jì)算一次,并手動將該結(jié)果粘貼到一些變量 / 常量中。程序通常是高度模塊化的并且全局可訪問的作用域并不是通常你想要在這些獨(dú)立部分之間分享狀態(tài)的方式。讓specialNumber(..) 使用自己的緩存(即使它恰好是使用一個(gè)全局變量來實(shí)現(xiàn)這一點(diǎn))是對狀態(tài)共享更好的抽象。

關(guān)鍵是,如果 specialNumber(..) 只是程序訪問和更新 cache 副作用的唯一部分,那么引用透明的觀點(diǎn)顯然可以適用,這可以被看作是可以接受的實(shí)際的“欺騙”的純函數(shù)思想。

但是真的應(yīng)該這樣嗎?

典型的,這種性能優(yōu)化方面的副作用是通過隱藏緩存結(jié)果產(chǎn)生的,因此它們不能被程序的任何其他部分所觀察到。這個(gè)過程被稱為記憶化。我一直稱這個(gè)詞是 “記憶化”,我不知道這個(gè)想法是從哪里來的,但它確實(shí)有助于我更好地理解這個(gè)概念。

思考一下:

var specialNumber = (function memoization(){
    var cache = [];

    return function specialNumber(n){
            // 如果我們已經(jīng)計(jì)算過這個(gè)特殊的數(shù),
            // 跳過這個(gè)操作,然后從緩存中返回
        if (cache[n] !== undefined) {
            return cache[n];
        }

        var x = 1, y = 1;

        for (let i = 1; i <= n; i++) {
            x += i % 2;
            y += i % 3;
        }

        cache[n] = (x * y) / (n + 1);

        return cache[n];
    };
})();

我們已經(jīng)遏制 memoization() 內(nèi)部 specialNumber(..) IIFE 范圍內(nèi)的 cache 的副作用,所以現(xiàn)在我們確定程序任何的部分都不能觀察到它們,而不僅僅是觀察它們。

最后一句話似乎是一個(gè)的微妙觀點(diǎn),但實(shí)際上我認(rèn)為這可能是整章中最重要的一點(diǎn)。 再讀一遍。

回到這個(gè)哲學(xué)理論:

假如一棵樹在森林里倒下而沒有人在附近聽見,它有沒有發(fā)出聲音?

通過這個(gè)暗喻,我所得到的是:無論是否產(chǎn)生聲音,如果我們從不創(chuàng)造一個(gè)當(dāng)樹落下時(shí)周圍沒有人的情景會更好一些。當(dāng)樹落下時(shí),我們總是會聽到聲音。

減少副作用的目的并不是他們在程序中不能被觀察到,而是設(shè)計(jì)一個(gè)程序,讓副作用盡可能的少,因?yàn)檫@使代碼更容易理解。一個(gè)沒有觀察到的發(fā)生的副作用的程序在這個(gè)目標(biāo)上并不像一個(gè)不能觀察它們的程序那么有效。

如果副作用可能發(fā)生,作者和讀者必須盡量應(yīng)對它們。使它們不發(fā)生,作者和讀者都要對任何可能或不可能發(fā)生的事情更有自信。

純化

如果你有不純的函數(shù),且你無法將其重構(gòu)為純函數(shù),此時(shí)你能做些什么?

您需要確定該函數(shù)有什么樣的副作用。副作用來自不同的地方,可能是由于詞法自由變量、引用變化,甚至是 this 的綁定。我們將研究解決這些情況的方法。

封閉的影響

如果副作用的本質(zhì)是使用詞法自由變量,并且您可以選擇修改周圍的代碼,那么您可以使用作用域來封裝它們。

回憶一下:

var users = {};

function fetchUserData(userId) {
    ajax( "http://some.api/user/" + userId, function onUserData(userData){
        users[userId] = userData;
    } );
}

純化此代碼的一個(gè)方法是在變量和不純的函數(shù)周圍創(chuàng)建一個(gè)容器。本質(zhì)上,容器必須接收所有的輸入。

function safer_fetchUserData(userId,users) {
 ? ? ? ?// 簡單的、原生的 ES6 + 淺拷貝,也可以
    // 用不同的庫或框架
    users = Object.assign( {}, users );

    fetchUserData( userId );

        // 返回拷貝過的狀態(tài) 
    return users;


    // ***********************

 ? ? ? ?// 原始的沒被改變的純函數(shù):
    function fetchUserData(userId) {
        ajax( "http://some.api/user/" + userId, function onUserData(userData){
            users[userId] = userData;
        } );
    }
}

userIdusers 都是原始的的 fetchUserData 的輸入,users 也是輸出。safer_fetchUserData(..) 取出他們的輸入,并返回 users。為了確保在 users 被改變時(shí)我們不會在外部創(chuàng)建副作用,我們制作一個(gè) users 本地副本。

這種技術(shù)的有效性有限,主要是因?yàn)槿绻悴荒軐⒑瘮?shù)本身改為純的,你也幾乎不可能修改其周圍的代碼。然而,如果可能,探索它是有幫助的,因?yàn)樗撬行迯?fù)方法中最簡單的。

無論這是否是重構(gòu)純函數(shù)的一個(gè)實(shí)際方法,最重要的是函數(shù)的純度僅僅需要深入到皮膚。也就是說,函數(shù)的純度是從外部判斷的, 不管內(nèi)部是什么。只要一個(gè)函數(shù)的使用表現(xiàn)為純的,它就是純的。在純函數(shù)的內(nèi)部,由于各種原因,包括最常見的性能方面,可以適度的使用不純的技術(shù)。正如他們所說的“世界是一只馱著一只一直馱下去的烏龜群”。

不過要小心。程序的任何部分都是不純的,即使它僅僅是用純函數(shù)包裹的,也是代碼錯誤和困惑讀者的潛在的根源。總體目標(biāo)是盡可能減少副作用,而不僅僅是隱藏它們。

覆蓋效果

很多時(shí)候,你無法在容器函數(shù)的內(nèi)部為了封裝詞法自由變量來修改代碼。例如,不純的函數(shù)可能位于一個(gè)你無法控制的第三方庫文件中,其中包括:

var nums = [];
var smallCount = 0;
var largeCount = 0;

function generateMoreRandoms(count) {
    for (let i = 0; i < count; i++) {
        let num = Math.random();

        if (num >= 0.5) {
            largeCount++;
        }
        else {
            smallCount++;
        }

        nums.push( num );
    }
}

蠻力的策略是,在我們程序的其余部分使用此通用程序時(shí)隔離副作用的方法時(shí)創(chuàng)建一個(gè)接口函數(shù),執(zhí)行以下步驟:

捕獲受影響的當(dāng)前狀態(tài)

設(shè)置初始輸入狀態(tài)

運(yùn)行不純的函數(shù)

捕獲副作用狀態(tài)

恢復(fù)原來的狀態(tài)

返回捕獲的副作用狀態(tài)

function safer_generateMoreRandoms(count,initial) {
 ? ? ? ?// (1) 保存原始狀態(tài)
    var orig = {
        nums,
        smallCount,
        largeCount
    };

 ? ? ? ?// (2) 設(shè)置初始副作用狀態(tài)
    nums = initial.nums.slice();
    smallCount = initial.smallCount;
    largeCount = initial.largeCount;

 ? ? ? ?// (3) 當(dāng)心雜質(zhì)!
    generateMoreRandoms( count );

 ? ? ? ?// (4) 捕獲副作用狀態(tài)
    var sides = {
        nums,
        smallCount,
        largeCount
    };

 ? ? ? ?// (5) 重新存儲原始狀態(tài)
    nums = orig.nums;
    smallCount = orig.smallCount;
    largeCount = orig.largeCount;

 ? ? ? ?// (6) 作為輸出直接暴露副作用狀態(tài)
    return sides;
}

并且使用 safer_generateMoreRandoms(..)

var initialStates = {
    nums: [0.3, 0.4, 0.5],
    smallCount: 2,
    largeCount: 1
};

safer_generateMoreRandoms( 5, initialStates );
// { nums: [0.3,0.4,0.5,0.8510024448959794,0.04206799238...

nums;            // []
smallCount;        // 0
largeCount;        // 0

這需要大量的手動操作來避免一些副作用,如果我們一開始就沒有它們,那就容易多了。但如果我們別無選擇,那么這種額外的努力是值得的,以避免我們的項(xiàng)目出現(xiàn)意外。

注意: 這種技術(shù)只有在處理同步代碼時(shí)才有用。異步代碼不能可靠地使用這種方法被管理,因?yàn)槿绻绦虻钠渌糠衷谄陂g也在訪問 / 修改狀態(tài)變量,它就無法防止意外。

回避影響

當(dāng)要處理的副作用的本質(zhì)是直接輸入值(對象、數(shù)組等)的突變時(shí),我們可以再次創(chuàng)建一個(gè)接口函數(shù)來替代原始的不純的函數(shù)去交互。

考慮一下:

function handleInactiveUsers(userList,dateCutoff) {
    for (let i = 0; i < userList.length; i++) {
        if (userList[i].lastLogin == null) {
         ? ? ? ?// 將 user 從 list 中刪除
            userList.splice( i, 1 );
            i--;
        }
        else if (userList[i].lastLogin < dateCutoff) {
            userList[i].inactive = true;
        }
    }
}

userList 數(shù)組本身,加上其中的對象,都發(fā)生了改變。防御這些副作用的一種策略是先做一個(gè)深拷貝(不是淺拷貝):

function safer_handleInactiveUsers(userList,dateCutoff) {
 ? ? ? ?// 拷貝列表和其中 `user` 的對象
    let copiedUserList = userList.map( function mapper(user){
     ? ? ? ?// 拷貝 user 對象
        return Object.assign( {}, user );
    } );

 ? ? ? ?// 使用拷貝過的對象調(diào)用最初的函數(shù)
    handleInactiveUsers( copiedUserList, dateCutoff );
         
    // 將突變的 list 作為直接的輸出暴露出來
    return copiedUserList;
}

這個(gè)技術(shù)的成功將取決于你所做的復(fù)制的深度。使用 userList.slice() 在這里不起作用,因?yàn)檫@只會創(chuàng)建一個(gè) userList 數(shù)組本身的淺拷貝。數(shù)組的每個(gè)元素都是一個(gè)需要復(fù)制的對象,所以我們需要格外小心。當(dāng)然,如果這些對象在它們之內(nèi)有對象(可能會這樣),則復(fù)制需要更加完善。

再看一下 this

另一個(gè)參數(shù)變化的副作用是和 this 有關(guān)的,我們應(yīng)該意識到 this 是函數(shù)隱式的輸入。查看第 2 章中的“什么是This”獲取更多的信息,為什么 this 關(guān)鍵字對函數(shù)式編程者是不確定的。

思考一下:

var ids = {
    prefix: "_",
    generate() {
        return this.prefix + Math.random();
    }
};

我們的策略類似于上一節(jié)的討論:創(chuàng)建一個(gè)接口函數(shù),強(qiáng)制 generate() 函數(shù)使用可預(yù)測的 this 上下文:

function safer_generate(context) {
    return ids.generate.call( context );
}

// *********************

safer_generate( { prefix: "foo" } );
// "foo0.8988802158307285"

這些策略絕對不是愚蠢的,對副作用的最安全的保護(hù)是不要產(chǎn)生它們。但是,如果您想提高程序的可讀性和你對程序的自信,無論在什么情況下盡可能減少副作用 / 效果是巨大的進(jìn)步。

本質(zhì)上,我們并沒有真正消除副作用,而是克制和限制它們,以便我們的代碼更加的可驗(yàn)證和可靠。如果我們后來遇到程序錯誤,我們就知道代碼仍然產(chǎn)生副作用的部分最有可能是罪魁禍?zhǔn)住?/p> 總結(jié)

副作用對代碼的可讀性和質(zhì)量都有害,因?yàn)樗鼈兪鼓拇a難以理解。副作用也是程序中最常見的錯誤原因之一,因?yàn)楹茈y應(yīng)對他們。冪等是通過本質(zhì)上創(chuàng)建僅有一次的操作來限制副作用的一種策略。

避免副作用的最優(yōu)方法是使用純函數(shù)。純函數(shù)給定相同輸入時(shí)總返回相同輸出,并且沒有副作用。引用透明更近一步的狀態(tài)是 —— 更多的是一種腦力運(yùn)動而不是文字行為 —— 純函數(shù)的調(diào)用是可以用它的輸出來代替,并且程序的行為不會被改變。

將一個(gè)不純的函數(shù)重構(gòu)為純函數(shù)是首選。但是,如果無法重構(gòu),嘗試封裝副作用,或者創(chuàng)建一個(gè)純粹的接口來解決問題。

沒有程序可以完全沒有副作用。但是在實(shí)際情況中的很多地方更喜歡純函數(shù)。盡可能地收集純函數(shù)的副作用,這樣當(dāng)錯誤發(fā)生時(shí)更加容易識別和審查出最像罪魁禍?zhǔn)椎腻e誤。

【上一章】翻譯連載 | JavaScript輕量級函數(shù)式編程-第4章:組合函數(shù) |《你不知道的JS》姊妹篇

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

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

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/85188.html

相關(guān)文章

  • 翻譯連載 |《你不知道JS妹篇 |《JavaScript 量級函數(shù)編程》- 引言&前言

    摘要:我稱之為輕量級函數(shù)式編程。序眾所周知,我是一個(gè)函數(shù)式編程迷。函數(shù)式編程有很多種定義。本書是你開啟函數(shù)式編程旅途的絕佳起點(diǎn)。事實(shí)上,已經(jīng)有很多從頭到尾正確的方式介紹函數(shù)式編程的書了。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 譯者團(tuán)隊(duì)(排名不分先后):阿希、blueken、brucecham、...

    2bdenny 評論0 收藏0
  • 翻譯連載 |《你不知道JS妹篇 |《JavaScript 量級函數(shù)編程》- 1

    摘要:所以我覺得函數(shù)式編程領(lǐng)域更像學(xué)者的領(lǐng)域。函數(shù)式編程的原則是完善的,經(jīng)過了深入的研究和審查,并且可以被驗(yàn)證。函數(shù)式編程是編寫可讀代碼的最有效工具之一可能還有其他。我知道很多函數(shù)式編程編程者會認(rèn)為形式主義本身有助于學(xué)習(xí)。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液...

    omgdog 評論0 收藏0
  • 翻譯連載 | JavaScript量級函數(shù)編程-6:值不可變性 |《你不知道JS妹篇

    摘要:但在開始之前應(yīng)該心中有數(shù)值的不可變性并不是說我們不能在程序編寫時(shí)不改變某個(gè)值。這些都是對值的不可變這個(gè)概念的誤解。程序的其他部分不會影響的賦值。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;分享,是 CSS 里最閃...

    ysl_unh 評論0 收藏0
  • 翻譯連載 | 9 :遞歸(上)-《JavaScript量級函數(shù)編程》 |《你不知道JS

    摘要:一旦我們滿足了基本條件值為,我們將不再調(diào)用遞歸函數(shù),只是有效地執(zhí)行了。遞歸深諳函數(shù)式編程之精髓,最被廣泛引證的原因是,在調(diào)用棧中,遞歸把大部分顯式狀態(tài)跟蹤換為了隱式狀態(tài)。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;...

    MasonEast 評論0 收藏0
  • 翻譯連載 | JavaScript量級函數(shù)編程-7: 閉包vs對象 |《你不知道JS姊妹

    摘要:相像閉包和對象之間的關(guān)系可能不是那么明顯。一個(gè)沒有對象的編程語言可以用閉包來模擬對象。事實(shí)上,表達(dá)一個(gè)對象為閉包形式,或閉包為對象形式是相當(dāng)簡單的。簡而言之,閉包和對象是狀態(tài)的同構(gòu)表示及其相關(guān)功能。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,...

    leanxi 評論0 收藏0
  • 翻譯連載 | 10 :異步函數(shù)(上)-《JavaScript量級函數(shù)編程》 |《你不

    摘要:這就是積極的函數(shù)式編程。上一章翻譯連載第章遞歸下輕量級函數(shù)式編程你不知道的姊妹篇原創(chuàng)新書移動前端高效開發(fā)實(shí)戰(zhàn)已在亞馬遜京東當(dāng)當(dāng)開售。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;分享,是 CSS 里最閃耀的一瞥;總...

    Lucky_Boy 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<