XDM,JS如何函數式編程?看這就夠了!(四)

不知不覺,咱們已經來到了《JS如何函數式編程》系列的【第四篇】。ajax

前三篇傳送門:編程

通過前幾篇的歷練,本瓜相信你的心中必定對函數編程有了基本的藍圖。api

本篇會將這個藍圖再具象一下,談談函數編程中一個很重要的細節 —— 「反作用」數組

  • 點贊富三代👍👍👍評論美一輩子🎉🎉🎉

維基上關於反作用的解釋:安全

函數內部有隱式(Implicit)的數據流,這種狀況叫作反作用(Side Effect)。markdown

我們前文也提到過:開發人員喜歡顯式輸入輸出而不是隱式輸入輸出。app

因此咱們將細緻的看看反作用中【隱式】和【顯式】的區別!異步

何爲反作用?

先來個小例子做開胃菜:ide

// 片斷 1
function foo(x) {
    return x * 2;
}

var y = foo( 3 );

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

var y;

foo( 3 );
複製代碼

片斷 1 和片斷 2 實現的最終效果是一致的,即 y = 3 * 2 ,可是片斷 1 是顯示的,片斷 2 是隱式的。函數式編程

緣由是:片斷 2 在函數內引用了外部變量 y。

片斷 2 ,當咱們調用 foo( 3 ) 時,並不知道其內部是否會修改外部變量 y。它的修改是隱式的,即產生了反作用!

有反作用的函數可讀性更低,咱們須要更多的閱讀來理解程序。

再舉一例:

var x = 1;

foo();

console.log( x );

bar();

console.log( x );

baz();

console.log( x );
複製代碼

若是每一個函數內都引用了 x ,有可能對其賦值修改,那麼咱們很難知道每一步 x 的值是怎樣的,要每一步去追蹤!

選擇在一個或多個函數調用中編寫帶有(潛在)反作用的代碼,那麼這意味着你代碼的讀者必須將你的程序完整地執行到某一行,逐步理解。

若是 foo()bar()、和 baz() 這三個函數沒有(潛在)反作用,x 的值一眼可見!

必定是修改外部變量纔是產生反作用了嗎?

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

var y = 3;

foo( 1 ); 
複製代碼

這段代碼中,咱們沒有修改外部變量 y ,可是引用了它,也是會產生反作用的。

y = 5;

// ..

foo( 1 );   
複製代碼

兩次 foo( 1 ) 的結果卻不同,又增大了閱讀的負擔。相信我,這是個最簡單抽象的例子,實際的影響將遠大於此。

避免反作用?

  1. const

以上面的例子來講:這樣寫,foo( 1 ) 的結果固然是肯定的,由於用到了 const 來固定外部變量。

const y = 5;

// ..

foo( 1 );
複製代碼
  1. I/O

一個沒有 I/O 的程序是徹底沒有意義的,由於它的工做不能以任何方式被觀察到。一個有用的程序必須最少有一個輸出,而且也須要輸入。輸入會產生輸出。

還記得 foo(..) 函數片斷 2 嗎?沒有輸出 return,這是不太可取的。

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

var y;

foo( 3 );
複製代碼
  1. 明確依賴

咱們常常會因爲函數的異步問題致使數據出錯;一個函數引用了另一個函數的回調結果,當咱們做這種引用時要特別注意。

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++) {
                // 對每一個用戶的最新訂單保持引用
            users[userId].latestOrder = orders[i];
            userOrders[orders[i].orderId] = orders[i];
        }
    } );
}
複製代碼

fetchUserData(..) 應該在 fetchOrders(..) 以前執行,由於後者設置 latestOrder 須要前者的回調;

寫出有反作用/效果的代碼是很正常的, 但咱們須要謹慎和刻意地避免產生有反作用的代碼。

  1. 運用冪等

這是一個很新但重要的概念!

從數學的角度來看,冪等指的是在第一次調用後,若是你將該輸出一次又一次地輸入到操做中,其輸出永遠不會改變的操做。

一個典型的數學例子是 Math.abs(..)(取絕對值)。Math.abs(-2) 的結果是 2,和 Math.abs(Math.abs(Math.abs(Math.abs(-2)))) 的結果相同。

冪等在 js 中的表現:

// 例 1
var x = 42, y = "hello";

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

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

// 例 2
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

// 例 3
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
複製代碼

實際上,咱們在 js 函數式編程中冪等有更加寬泛的概念,即只用要求:f(x) === f(f(x))

// 冪等的:
obj.count = 2; // 這裏的冪等性的概念是每個冪等運算(好比 obj.count = 2)能夠重複屢次
person.name = upper( person.name );

// 非冪等的:
obj.count++;
person.lastUpdated = Date.now();

// 冪等的:
var hist = document.getElementById( "orderHistory" );
hist.innerHTML = order.historyText;

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

咱們不會一直用冪等的方式去定義數據,但若是能作到,這確定會減小意外狀況下產生的反作用。這須要時間去體會,咱們就先記住它。

純函數

你應該據說過純函數的大名,咱們把沒有反作用的函數稱爲純函數。

例 1:

function add(x,y) {
    return x + y;
}
複製代碼

輸入(x 和 y)和輸出(return ..)都是直接的,沒有引用自由變量。調用 add(3,4) 屢次和調用一次是沒有區別的。add(..) 是純粹的編程風格的冪等。

例 2:

const PI = 3.141592;

function circleArea(radius) {
    return PI * radius * radius;
}
複製代碼

circleArea 也是純函數。雖然它調用了外部變量 PI ,可是 PI 是 const 定義的常量,引用常量不會產生反作用;

例 3:

function unary(fn) {
    return function onlyOneArg(arg){
        return fn( arg );
    };
}
複製代碼

unary 也是純函數。

表達一個函數的純度的另外一種經常使用方法是:給定相同的輸入(一個或多個),它老是產生相同的輸出。

不純的函數是不受歡迎的!由於咱們須要更多的精力去判斷它的輸出結果!

寫純函數須要更多耐心,好比咱們操做數組的 push(..) 方法,或 reverse(..) 方法等,看起來安全,但實際上會修改數組自己。咱們須要複製一個變量來解耦(深拷貝)。

函數的純度是和自信是有關的。函數越純潔越好。製做純函數時越努力,當您閱讀使用它的代碼時,你的自信就會越高,這將使代碼更加可讀。

其實,關於函數純度還有更多有意思的點:

思考一個問題,若是咱們把函數和外部變量再封裝爲一個函數,外界沒法直接訪問其內部,這樣,內部的函數算不算是一個純函數?

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

階段小結

  1. 咱們反覆強調:開發人員喜歡顯式輸入輸出而不是隱式輸入輸出。

  2. 若是有隱式的輸入輸出,那麼就有可能產生反作用。

  3. 有反作用的代碼讓咱們的代碼理解起來更加費勁!

  4. 解決反作用的方法有:定義常量、明確 I/O、明確依賴、運用冪等......

  5. 在 js 運用冪等是一個新事物,咱們須要逐漸熟悉它。

  6. 沒有反作用的函數就是純函數,純函數是咱們追求編寫的!

  7. 將一個不純的函數重構爲純函數是首選。可是,若是沒法重構,嘗試封裝反作用。(假如一棵樹在森林裏倒下而沒有人在附近聽見,它有沒有發出聲音?—— 有沒有其實已經不重要了,反正聽不到)

以上,即是本次關於 JS 函數式編程 反作用 這個細節的講解。

這個細節,真的很重要!

我是掘金安東尼,公衆號【掘金安東尼】,輸出暴露輸入,技術洞見生活!

相關文章
相關標籤/搜索