不知不覺,咱們已經來到了《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 ) 的結果卻不同,又增大了閱讀的負擔。相信我,這是個最簡單抽象的例子,實際的影響將遠大於此。
以上面的例子來講:這樣寫,foo( 1 ) 的結果固然是肯定的,由於用到了 const 來固定外部變量。
const y = 5;
// ..
foo( 1 );
複製代碼
一個沒有 I/O 的程序是徹底沒有意義的,由於它的工做不能以任何方式被觀察到。一個有用的程序必須最少有一個輸出,而且也須要輸入。輸入會產生輸出。
還記得 foo(..) 函數片斷 2 嗎?沒有輸出 return,這是不太可取的。
// 片斷 2
function foo(x) {
y = x * 2;
}
var y;
foo( 3 );
複製代碼
咱們常常會因爲函數的異步問題致使數據出錯;一個函數引用了另一個函數的回調結果,當咱們做這種引用時要特別注意。
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
須要前者的回調;
寫出有反作用/效果的代碼是很正常的, 但咱們須要謹慎和刻意地避免產生有反作用的代碼。
這是一個很新但重要的概念!
從數學的角度來看,冪等指的是在第一次調用後,若是你將該輸出一次又一次地輸入到操做中,其輸出永遠不會改變的操做。
一個典型的數學例子是 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(..) 方法等,看起來安全,但實際上會修改數組自己。咱們須要複製一個變量來解耦(深拷貝)。
函數的純度是和自信是有關的。函數越純潔越好。製做純函數時越努力,當您閱讀使用它的代碼時,你的自信就會越高,這將使代碼更加可讀。
其實,關於函數純度還有更多有意思的點:
思考一個問題,若是咱們把函數和外部變量再封裝爲一個函數,外界沒法直接訪問其內部,這樣,內部的函數算不算是一個純函數?
假如一棵樹在森林裏倒下而沒有人在附近聽見,它有沒有發出聲音?
咱們反覆強調:開發人員喜歡顯式輸入輸出而不是隱式輸入輸出。
若是有隱式的輸入輸出,那麼就有可能產生反作用。
有反作用的代碼讓咱們的代碼理解起來更加費勁!
解決反作用的方法有:定義常量、明確 I/O、明確依賴、運用冪等......
在 js 運用冪等是一個新事物,咱們須要逐漸熟悉它。
沒有反作用的函數就是純函數,純函數是咱們追求編寫的!
將一個不純的函數重構爲純函數是首選。可是,若是沒法重構,嘗試封裝反作用。(假如一棵樹在森林裏倒下而沒有人在附近聽見,它有沒有發出聲音?—— 有沒有其實已經不重要了,反正聽不到)
以上,即是本次關於 JS 函數式編程 反作用 這個細節的講解。
這個細節,真的很重要!
我是掘金安東尼,公衆號【掘金安東尼】,輸出暴露輸入,技術洞見生活!