javascript函數式編程

長久以來,面向對象在 JavaScript 編程范式中占據著主導地位。不過,最近人們對函數式編程的興趣正在增長。函數式編程是一種編程風格,它強調將程序狀態變化(即副作用[side effect])的次數減到最小。因此,函數式編程鼓勵使用不可變數據(immutable data)和純函數(pure functions)(“純”意味著沒有副作用的)。它也更傾向于使用聲明式的風格,鼓勵使用命名良好的函數,這樣就能使用在我們視線之外的那些打包好的細節實現,通過描述希望發生什么以進行編碼。

概念:
命令式vs 聲明式
純函數
高階函數
函數組合
偏函數
函數柯里化

應用:
函數柯里化的使用:
函數等價范式
js中的鏈式調用
函數節流與防抖
惰性載入
尾遞歸優化

編碼風格
讓JS代碼更優雅

概念
命令式vs聲明式

  • 命令式編程:命令“機器”如何去做事情(how),這樣不管你想要的是什么(what),它都會按照你的命令實現。
  • 聲明式編程:告訴“機器”你想要的是什么(what),讓機器想出如何去做(how)。

舉例
讓一個數組里的數值翻倍。
我們用命令式編程風格實現,像下面這樣:
var numbers = [1,2,3,4,5]
var doubled = []
for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push(newNumber)
}
console.log(doubled) //=> [2,4,6,8,10]

我們直接遍歷整個數組,取出每個元素,乘以二,然后把翻倍后的值放入新數組,每次都要操作這個雙倍數組,直到計算完所有元素。

而使用聲明式編程方法,我們可以用 Array.map 函數,像下面這樣:
var numbers = [1,2,3,4,5]
var doubled = numbers.map(function(n) {
return n * 2
})
console.log(doubled) //=> [2,4,6,8,10]

一個list求和,命令式編程會這樣做:
var numbers = [1,2,3,4,5]
var total = 0
for(var i = 0; i < numbers.length; i++) {
total += numbers[i]
}
console.log(total) //=> 15

而在聲明式編程方式里,我們使用 reduce 函數:
var numbers = [1,2,3,4,5]
var total = numbers.reduce(function(sum, n) {
return sum + n
});
console.log(total) //=> 15

reduce 函數利用傳入的函數把一個 list 運算成一個值。它以這個函數為參數,數組里的每個元素都要經過它的處理。每一次調用,第一個參數(這里是sum)都是這個函數處理前一個值時返回的結果,而第二個參數(n)就是當前元素。這樣下來,每此處理的新元素都會合計到sum中,最終我們得到的是整個數組的和。

純函數
函數內的運算對函數外無副作用
不改變外部
不依賴被外部改變的
結果來自于傳入的參數
并且當輸入相同時輸出保持一致
// 純函數
const add10 = (a) => a + 10
// 依賴于外部變量的非純函數
let x = 10
const addx = (a) => a + x
// 會產生副作用的非純函數
const setx = (v) => x = v
非純函數間接地依賴于參數 x。如果你改變了 x 的值,對于相同的 a,addx 會輸出不同的結果。

寫純函數的優點:
純函數降低了程序的認知難度。寫純函數時,你僅僅需要關注函數體本身。不必去擔心一些外部因素所帶來的問題,比如在 addx 函數中的 x 被改變。

不改變外部——這意味著函數求值的結果只是其返回值,而惟一影響其返回值的就是函數的參數。如果一個函數式程序不如你期望地運行,調試是輕而易舉的。因為函數式程序的 bug 不依賴于執行前與其無關的代碼路徑,你遇到的問題就總是可以再現。

在單元測試中,你只需在意其參數,而不必考慮函數調用順序,不用謹慎地設置外部狀態。所有要做的就是傳遞代表了邊際情況的參數。

純函數 不會改變程序的狀態,也不會產生可感知的副作用。純函數的輸出,僅僅取決于輸入值。無論何時何地被調用,只要輸入值相同,返回值也就一樣。

高階函數
高階函數就是函數當參數,把傳入的函數做一個封裝,然后返回這個封裝函數。
例如我們要實現一個計算1和任意數字的和的函數:
var partAdd = function(p1){
this.add = function (p2){
return p1 + p2;
};
return add;
};
var add = partAdd(1);
add(2); // 3

執行 partAdd(1) 時返回的任然是一個函數,當再次傳入第二個參數時,就可以計算出和了。
上面的例子只是為了理解高階函數,實際運用如下例所示:
var add = function(a,b){
return a + b;
};
function math(func,array){
return func(array[0],array[1]);
}
math(add,[1,2]); // 3

函數組合
通過函數組合,可能將函數組合在一起形成新的函數。一起來看例子:
// 通過 add 和 square 函數組合生成 addThenSquare
const add = function ( x, y ) {
return x + y;
};

const square = function ( x ) {
return x * x;
};

const addThenSquare = function ( x, y ) {
return square(add( x, y ));
};
你可能會發現一直在重復這種利用更小的函數生成一個更復雜的函數的形式。通常編寫一個組合函數會更有效率:
const add = function ( x, y ) {
return x + y;
};

const square = function ( x ) {
return x * x;
};

const composeTwo = function ( f, g ) {
return function ( x, y ) {
return g( f ( x, y ) );
};
};

函數式編程中還有一個很棒的東西就是你可以在新的函數中組合它們。一用于在 lambda 運算中描述程序的很特殊的運算符就是組合。組合將兩個函數在一個新的函數中『組合』到一起。如下:
const add1 = (a) => a + 1
const times2 = (a) => a * 2
const compose = (a, b) => (c) => a(b(c))
const add1OfTimes2 = compose(add1, times2)
add1OfTimes2(5) // => 11

借助函數組合,我們可以通過將多個小函數結合在一起來構建更復雜的數據變化。這篇文章詳細地展示了函數組合是如何幫助你以更加干凈簡潔的方式來處理數據。
從實際來講,組合可以更好地替代面向對象中的繼承。下面是一個有點牽強但很實際的示例。假如你需要為你的用戶創建一個問候語。

const greeting = (name) => Hello ${name}
很棒!一個簡單的純函數。突然,你的項目經理說你現在需要為用戶展示更多的信息,在名字前添加前綴。所以你可能把代碼改成下面這樣:
const greeting = (name, male=false, female=false) =>
Hello ${male ? ‘Mr. ‘ : female ? ‘Ms. ‘ : ‘’} ${name}
代碼并不是很糟糕,不過如果我們又要添加越來越多的判斷邏輯,比如『Dr.』或『Sir』呢?如果我們要添加『MD』或者『PhD』前綴呢?又或者我們要變更下問候的方式,用『Sup』替代『Hello』呢?現在事情已然變得很棘手。像這樣為函數添加判斷邏輯并不是面向對象中的繼承,不過與繼承并且重寫對象的屬性和方法的情況有些類似。既然反對添加判斷邏輯,那我們就來試試函數組合的方式:
const formalGreeting = (name) => Hello ${name}
const casualGreeting = (name) => Sup ${name}
const male = (name) => Mr. ${name}
const female = (name) => Mrs. ${name}
const doctor = (name) => Dr. ${name}
const phd = (name) => ${name} PhD
const md = (name) => ${name} M.D.
formalGreeting(male(phd("Chet"))) // => "Hello Mr. Chet PhD"
這就是更加可維護和一讀的原因。每個函數僅完成了一個簡單的事情,我們很容易就可以將它們組合在一起。現在,我們還沒有完成整個實例,接下來使用 pipe 函數!
const identity = (x) => x
const greet = (name, options) => {
return pipe([
// greeting
options.formal ? formalGreeting :
casualGreeting,
// prefix
options.doctor ? doctor :
options.male ? male :
options.female ? female :
identity,
// suffix
options.phd ? phd :
options.md ?md :
identity
])(name)
}
另外一個使用純函數和函數組合的好處是更加容易追蹤錯誤。無論在什么時候出現一個錯誤,你都能夠通過每個函數追溯到問題的緣由。在面向對象編程中,這通常會相當的復雜,因為你一般情況下并不知道引發改問題的對象的其他狀態。

偏函數
Partial 函數應用 指定函數參數中的一個或多個,然后返回一個稍后會被完整調用的函數。
在下面的例子中,double、triple 和 quadruple 都是 multiply 函數的 partial 應用。
const multiply = function ( x, y ) {
return x * y;
};

const partApply = function ( fn, x ) {
return function ( y ) {
fn( x, y );
};
};

const double = partApply( multiply, 2 );
const triple = partApply( multiply, 3 );
const quadruple = partApply( multiply, 4 );

函數柯里化
函數柯里化就是對高階函數的降階處理。是將接收多個參數的函數轉換為一系列只接收一個參數的函數的過程。
eg:
function(arg1,arg2)變成function(arg1)(arg2)
function(arg1,arg2,arg3,arg4)變成function(arg1)(arg2)(arg3)(arg4)

柯里化把一個結果分成了多個步驟去走,把前面的步驟緩存起來,直至達到最后一步才出結果,這樣可以很好區分什么參數在哪個步驟做了哪些事,用于debug和理解代碼很有幫助。
這有別于一個封裝好的大函數,傳入需要的參數,一步到位!
代碼實例:
var aaa = function(p1){
return function (p2){
return p1 + ' ' + p2;
};
};
console.log(aaa('Hello')('World')); // Hello World
在這個函數中,當調用a('hello')時,這個函數的功能并沒有完成,直至再次調用才打出了日志。

如果把函數式編程這樣的嵌套返回,說成層,那就可以怎么理解。通常情況下,前面幾層會做參數驗證,數據準備,邊界排查等前期工作,最后一層為核心代碼。

應用
函數柯里化的應用
實現方式:
實際應用:
//這是一個Ajax頁面局部刷新的例子
//替換DOM中某個節點的html
function update(id){
return function (data){
$("div#"+id).html(data.text);
}
}
//Ajax局部刷新
function refresh(url, callback){
$.ajax({
type:"get",
url:url,
dataType:"json",
success: function (data){
callback(data);
});
}
//刷新兩個區域
refresh("friends.php", update("friendsDiv"));
refresh("newfeeds.php", update("feedsDiv"));

首先聲明一個柯里化的函數 update,這個函數會將傳入的參數作為選擇器的 id,并更新這個 div 的內容 (innerHTML)。
然后聲明一個函數 refresh,refresh 接受兩個參數,第一個參數為服務器端的 url,第二個參數為一個回調函數,當服務器端成功返回時,調用該函數。
然后我們陸續調用 refresh,每次的 url 和 id 都不同,這樣可以將內容通過異步方式更新。
其中如何與服務器通信,以及如果選取頁面內容的部分被很好的抽象成了函數。

函數等價范式
functional將函數本身看成輸入和輸出的映射,或者說是一種確定的運算。那么我們可以定義兩個函數是否相等,對于任意函數a、b,如果給定的參數x、y,返回值z,當xa === xb且ya === yb時,總有za === zb,那么我們認為a、b等價。當然,對于js的函數,除了這一點,還必須包括一個附加條件,那就是this上下文,也就是說,對于函數a、b當this上下文、參數都相同時,兩個函數總是返回相同的結果,那么我們就說方法a和方法b完全等價。

function equal(func){
return function(){
var ret = func.apply(this, arguments);
return ret;
}
}
someObj. doSomething = equal(someObj.doSomething);
不論someObj和doSomething如何實現的,上面的賦值語句都完全不會讓代碼運行結果有一絲一毫改變

有了這個等價范式,我們確保不會影響函數運算產生的結果,而不用考慮函數運算的具體過程,而且,我們可以在這個產生結果的過程中添加我們的干預,只要我們的干預并不去影響a的輸入參數和返回值,那么這層“干預”并不會對系統造成任何不可預知的影響。

function safeTransform(func){
return function(){
做一些事情,但不去改變 arguments
var ret = func.apply(this, arguments);
做一些事情,但不去改變 ret
return ret;
}
}

我們通過范式約定,用模式把對系統的影響控制在可控的范圍內,使得對系統的干預造成的風險可預知可控,而這一切,基于基礎的函數式編程原理。

有什么用?
我在項目中需要使用其中的 Swipable組件 , 但是呢,我發現一個問題,按照產品設計,我的swipable元素有一個偽滾動條,這個滾動條要根據Swipable組件的滾動條狀態同步更新,也就是說我必須拿到組件當前的滾動狀態,而這個狀態在組件設計對外的接口中并沒有暴露出來。

那么怎么辦呢?無外乎有幾種辦法:

  1. 放棄用這個組件來實現這個需求
  2. 自己修改組件,將滾動狀態暴露出來
  3. 讓團隊負責這個組件的同學升級組件
  4. 利用函數式編程思想,不修改組件的情況下對這個組件的方法做一些額外的修飾

1)不說了,
2)的問題是將來組件升級了我就不能享受新的功能,
3)的問題是這個項目時間上不允許。
因此我選擇了第四種方案——

var fn = {
watch: function(func, before, after){
return function(){
var args = [].slice.call(arguments);
before && before.apply(this, args);
var ret = func.apply(this, arguments);
after && after.apply(this, [ret].concat(args));
return ret;
}
}
}

var swipable = new Swipable({
element: '.swipable-wrap',
dir: 'horizontal'
});

swipable._scroll = fn.watch(swipable._scroll, function(pos){
var p = pos / (swipable.min - swipable.max);
$('#progress-bar .current').css('width', Math.min(p, 1.0) * 240 + 'px');
});

基本思路就是上面的代碼,首先定義一個watch的高階函數,它可以"watch"任意一個方法,在它被調用之前(before)和被調用之后(after)攔截進去。

然后我就watch了swipable組件中的_scroll方法,在它被調用前拿到了它的參數pos,進行計算,同步更新偽滾動條。

這樣的話我就在沒有對swipable組件本身改動一行代碼的情況下得到了我要的功能。

函數節流與防抖

Debounce
debounce 函數所做的事情就是,強制一個函數在某個連續時間段內只執行一次,哪怕它本來會被調用多次。我們希望在用戶停止某個操作一段時間之后才執行相應的監聽函數,而不是在用戶操作的過程當中,瀏覽器觸發多少次事件,就執行多少次監聽函數。
比如,在某個 3s 的時間段內連續地移動了鼠標,瀏覽器可能會觸發幾十(甚至幾百)個 mousemove 事件,不使用 debounce 的話,監聽函數就要執行這么多次;如果對監聽函數使用 100ms 的“去彈跳”,那么瀏覽器只會執行一次這個監聽函數,而且是在第 3.1s 的時候執行的。
有時候我們希望函數在某些操作執行完成之后被觸發。例如,實現搜索框的 Suggest 效果,如果數據是從服務器端讀取的,為了限制從服務器讀取數據的頻率,我們可以等待用戶輸入結束 100ms 之后再觸發Suggest 查詢:

現在,我們就來實現一個 debounce 函數。
實現:
function debounce(fn, delay){
var timer = null; // 定時器,用來 setTimeout
// 返回一個函數,這個函數會在一個時間區間結束后的 delay 毫秒時執行 fn 函數
return function(...args){
// 每這當返回的函數被調用,就清除定時器,以保證不執行 fn
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
}
}

其實思路很簡單,debounce 返回了一個閉包,這個閉包依然會被連續頻繁地調用,但是在閉包內部,卻限制了原始函數 fn 的執行,強制 fn 只在連續操作停止后只執行一次。

debounce 的使用方式如下:
$(document).on('mouvemove', debounce(function(e) {
// 代碼
}, 250))

用例
考慮一個場景:根據用戶的輸入實時向服務器發 ajax 請求獲取數據。我們知道,瀏覽器觸發 key* 事件也是非常快的,即便是正常人的正常打字速度,key* 事件被觸發的頻率也是很高的。以這種頻率發送請求,一是我們并沒有拿到用戶的完整輸入發送給服務器,二是這種頻繁的無用請求實在沒有必要。
更合理的處理方式是,在用戶“停止”輸入一小段時間以后,再發送請求。那么 debounce 就派上用場了:
$('input').on('keyup', debounce(function(e) {
// 發送 ajax 請求
}, 300))

Throttle
固定函數執行速率,即所謂的“節流”。
在實際項目中,我們有時候會遇到限制某函數調用頻率的需求。例如,防止一個按鈕短時間的的重復點擊,防止 resize、scroll 和 mousemove 事件過于頻繁地觸發等。

實現
接收兩個參數,一個實際要執行的函數 fn,一個執行間隔。
// throttle 的簡單實現
function throttle(fn, wait){
var timer;
return function(...args){
if(!timer){
timer = setTimeout(()=>timer=null, wait);
return fn.apply(this, args);
}
}
}

//按鈕每500ms一次點擊有效
btn.onclick = throttle(function(){
console.log("button clicked");
}, 500);

尾調用優化
尾調用指的是,某個函數的最后一步動作是調用函數。尾調用優化指的是,當語言編譯器識別到尾調用的時候,會對其復用相同的調用幀。這意味著,在編寫尾調用的遞歸函數時,調用幀的限制永遠不會被超出,因為調用幀會被反復使用。
下面是將前面的遞歸函數采用尾遞歸優化重寫之后的例子:
const factorial = function ( n, base ) {
if ( n === 0 ) {
return base;
}
base *= n;
return factorial( n - 1, base );
};

console.log(factorial( 10, 1 )); // 3628800

尾調用優化
尾調用的概念非常簡單,一句話就能說清楚,就是指某個函數的最后一步是調用另一個函數。
function f(x){
return g(x);
}
上面代碼中,函數f的最后一步是調用函數g,這就叫尾調用。
以下兩種情況,都不屬于尾調用。
// 情況一
function f(x){
let y = g(x);
return y;
}

// 情況二
function f(x){
return g(x) + 1;
}
上面代碼中,情況一是調用函數g之后,還有別的操作,所以不屬于尾調用,即使語義完全一樣。情況二也屬于調用后還有操作,即使寫在一行內。
我們知道函數調用會在內存形成一個"調用記錄",又稱"調用幀"(call frame),保存調用位置和內部變量等信息。多層次的調用記錄形成了調用棧。尾調用由于是函數的最后一步操作,所以不需要保留外層函數的調用記錄,因為調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用記錄,取代外層函數的調用記錄就可以了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();

// 等同于
function f() {
return g(3);
}
f();

// 等同于
g(3);
函數調用自身,稱為遞歸。如果尾調用自身,就稱為尾遞歸。基于尾調用優化的原理,我們可以對尾遞歸進行優化。遞歸需要保存大量的調用記錄,很容易發生棧溢出錯誤,如果使用尾遞歸優化,將遞歸變為循環,那么只需要保存一個調用記錄,這樣就不會發生棧溢出錯誤了。
例如計算階乘的函數:
// 不是尾遞歸,無法優化
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}

// 尾遞歸,可以優化
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
目前的ES5中并沒有規定尾調用優化,但是ES6中明確規定了必須實現尾調用優化,也就是ES6中只要使用尾遞歸,就不會發生棧溢出。所以 對于遞歸函數盡量改寫為尾遞歸形式 。

函數式代碼風格 讓JS代碼更優雅
在函數式的語言中,連續運算是被推崇的方法
連續賦值
var a = b = c = d = 100;

“短路”條件
||和&&(||用來提供變量的默認值,&&可避免從undefined中取值拋出異常),也是連續運算的體現。

三元表達式
var objType = getFromInput();
var cls = ((objType == 'String') ? String :
(objType == 'Array') ? Array :
(objType == 'Number') ? Number :
(objType == 'Boolean') ? Boolean :
(objType == 'RegExp') ? RegExp :
Object
);
var obj = new cls();
如果你要用if/else,switch,甚至于“多態”的手段去重寫上面的方法,可以想象一下是多么長的一坨……

鏈式調用
例如jQuery的DOM操作:
$('#item').width('100px').height('100px').css('padding','20px').click(function(){ alert('hello'); });
其實原理就是在函數最后return this,即可接著之前的上下文環境繼續調用函數。

數組 foo 中的對象結構更改,然后從中挑選出一些符合條件的對象,并且把這些對象放進新數組 result 里。
var foo = [{
name: 'Stark',
age: 21
},{
name: 'Jarvis',
age: 20
},{
name: 'Pepper',
age: 16
}]

//我們希望得到結構稍微不同,age大于16的對象:
var result = [{
person: {
name: 'Stark',
age: 21
},
friends: []
},{
person: {
name: 'Jarvis',
age: 20
},
friends: []
}]
從直覺上我們很容易寫出這樣的代碼:
var result = [];

//有時甚至是普通的for循環
foo.forEach(function(person){
if(person.age > 16){
var newItem = {
person: person,
friends: [];
};
result.push(newItem);
}
})
然而用函數式的寫法,代碼可以優雅得多:
var result = foo
.filter(person => person.age > 16)
.map(person => ({
person: person,
friends: []
}))

數組求和:
var foo = [1, 2, 3, 4, 5];

//不優雅
function sum(arr){
var x = 0;
for(var i = 0; i < arr.length; i++){
x += arr[i];
}
return x;
}
sum(foo) // => 15

//優雅
foo.reduce((a, b) => a + b) // => 15

lodash里一些很好用的東西
lodash是一個JS工具庫,里面存在眾多函數式的方法和接口
1、_.flow解決函數嵌套過深
//很難看的嵌套
a(b(c(d(...args))));

//可以這樣改善
_.flowRight(a,b,c,d)(...args)

//或者
_.flow(d,c,b,a)(...args)

2、_.memoize加速數學計算
在寫一些Canvas游戲或者其他WebGL應用的時候,經常有大量的數學運算,例如:
Math.sin(1)
Math.sin()的性能比較差,如果我們對精度要求不是太高,我們可以使用 _.memoize 做一層緩存
var Sin = _.memoize(function(x){
return Math.sin(x);
})

Sin(1) //第一次使用速度比較慢
Sin(1) //第二次使用有了cache,速度極快
注意此處傳入的 x 最好是整數或者較短的小數,否則memoize會極其占用內存。
事實上,不僅是數學運算,任何函數式的方法都有可緩存性,這是函數式編程的一個明顯的優點

3、_.flatten解構嵌套數組
_.flatten([1, 2], [3, 4]); // => [1, 2, 3, 4]
這個方法和 Promise.all 結合十分有用處。
假設我們爬蟲程序有個 getFansList 方法,它可以根據傳入的值 x ,異步從粉絲列表中獲取第 x20 到 (x+1)20 個粉絲,現在我們希望獲得前1000個粉絲:
var works = [];

for (var i = 0; i < 50; i++) {
works.push(getFansList(i))
}

Promise.all(works)
.then(ArrayOfFansList=> _.flatten(ArrayOfFansList))
.then(result => console.log(result))

4、.once配合單例模式
有些函數會產生一個彈出框/遮罩層,或者負責app的初始化,因此這個函數是執行且只執行一次的。這就是所謂的單例模式,
.once大大簡化了我們的工作量
var initialize = _.once(createApplication);

initialize();
initialize();
// 這里實際上只執行了一次 initialize
// 不使用 once 的話需要自己手寫一個閉包

Generator + Promise改善異步流程
有時我們遇到這樣的情況:
getSomethingAsync()
.then( a => method1(a) )
.then( b => method2(b) )
.then( c => method3(a,b,c) ) //a和b在這里是undefined!!!
只用 Promise 的話,解決方法只有把 a、b 一層層 return 下去,或者聲明外部變量,把a、b放到 Promise 鏈的外部。但無論如何,代碼都會變得很難看。
用 Generator 可以大大改善這種情況(這里使用了Generator的執行器co):
import co from 'co';

function* foo(){
var a = yield getSomethingAsync();
var b = yield method1(a);
var c = yield method2(b);
var result = yield method3(a,b,c);
console.log(result);
}

co(foo());
當然,Generate 的用處遠不止如此,在異步遞歸中它能發揮更大的用處。比如我們現在需要搜索一顆二叉樹中value為100的節點,而這顆二叉樹的取值方法是異步的(比如它在某個數據庫中)
import co from 'co';

function* searchBinaryTree(node, value){
var nowValue = yield node.value();
if(nowValue == value){
return node;
}else if(nowValue < value){
var rightNode = yield node.getRightNode()
return searchBinaryTree(rightNode, value);
}else if(nowValue > value){
var leftNode = yield node.getLeftNode()
return searchBinaryTree(leftNode, value);
}
}

co(searchBinaryTree(rootNode, 100))

惰性求值
顧名思義,只有在需要用到的才去計算。這里強行設定一種情景,如一個加法函數:沒有惰性求值
function add(n1,n2){
if(n1<5){
return n1
}else{
return n1+n2
}
}
result = add(add(1,2),add(3,4)) //相當于add(3,4)的計算是浪費的。
result//3
惰性求值
function add(n1,n2){
return n1+n2;
}
function preAdd(n1,n2){
return function(){
return add(n1,n2)
}
}
function doAdd(fn1,fn2){
n = fn1()
if(n<5){
return n //只需要運行fn1,得到一個計算結果即可。
}else{
return add(fn1,fn2())
}
}
result = doAdd(preAdd(1,2),preAdd(3,4))
result//10
對比一下可知,在javascript中的惰性求值,相當于先把參數先緩存著,return一個真正執行的計算的函數,等到需要結果采去執行。這樣的好處在于比較節省計算,尤其有時候這個在函數是不一定需要這個參數的時候。

延遲計算
延遲計算是一類包含了很多類似 thunk 和 generator 規范概念的通用術語。延遲計算就和你所想的一樣:不會在必須做某件事情之前做任何事,盡可能長時間的延后。一個類比就是假如你有無限量的盤子要洗。你就不會將所有的盤子都放到水池中然后一次性清洗它們,我們可以偷懶一下,一次僅僅洗一個盤子。
在 Haskell 中,延遲計算的本質更加容易理解,所以我會從它說起。首先,我們需要理解程序是如何計算的。我們所使用的大部分語言使用的都是由內而外的規約,就像下面這樣:
square(3 + 4)
square(7) // 計算最內層的表達式
7 * 7
49
這也是比較明智的程序計算方式。不過我們先來看一下向外而內的規約。
square(3 + 4)
(3 + 4) * (3 + 4) // 計算最外層的表達式
7 * (3 + 4)
7 * 7
49
顯然,由外而內規約的方式不夠明智——我們需要計算兩次 3 + 4,所以程序共花費了 5 步。這有點糟糕。不過 Haskell 保留了對每個表達式的引用并且在它們由外而內規約時傳遞共享的引用。這樣,當 3 + 4 被首次計算后,這個表達式的引用會指向新的表達式 7。這樣我們就跳過了重復的步驟。
square(3 + 4)
(3 + 4) * (3 + 4) // 計算最外面的表達式
7 * 7 // 由于引用共享的存在,計算此時減少了一步
49
本質上,延遲計算就是引用共享的由外而內計算。
Haskell 在內部為你做了很多事情,并且這也意味著你可以像無限的列表一樣定義東西。比如,你可以遞歸地定義一個無限的列表。
ones = 1 : ones
假設現在有一個 take(n, list) 函數,它的第一個參數是一個 n 元素的列表。如果我們使用由內而外的規約,可能會出現無限遞歸計算一個列表,因為它是無限的。不過,借助由外而內計算,我們可以實現按需延遲計算!
然而,由于 JavaScript 和大多數編程語言都使用了由內而外的規約,我們復制這種架構的唯一方式就是將數組看成是函數,如下示例:
const makeOnes = () => {next: () => 1}
ones = makeOnes()
ones.next() // => 1
ones.next() // => 1
現在,我們已經基于相同的遞歸定義創建了一個延時計算無限列表的表達式。現在我們創建一個自然數的無限列表:
const makeNumbers = () => {
let n = 0
return {next: () => {
n += 1
return n
}
}
numbers = makeNumbers()
numbers.next() // 1
numbers.next() // 2
numbers.next() // 3
在 ES2015 中,確實為此實現了一個標準,并且稱為函數 generator。
function* numbers() {
let n = 0
while(true) {
n += 1
yield n
}
}
延遲可以帶來巨大的性能效益。比如,你可以對比一下每秒鐘 Lazy.js 計算與 Underscore 和 Lodash 的區別:

下面是解釋它的原理的一個很好的示例(Lazy.js 網站給出的)。假定你現在有一個巨大的數組(元素是人),并且你想對它執行某些轉換:
const results = _.chain(people)
.pluck('lastName')
.filter((name) => name.startsWith('Smith'))
.take(5)
.value()
完成這件事最原始的方式就是將所有的名字揀出來,過濾整個數組,然后使用前 5 個。這就是 Underscore.js 以及絕大多數類庫的做法。不過使用 generator,我們可以使用延遲計算 每次僅計算一個值,直到我們拿到了以 『Smith』開頭的名字。
Haskell 給我們最大的驚喜就是所有的這些都在語言內部借助由外而內規約和引用共享實現了。在 JavaScript 中,我們能夠借助于 Lazy.js,不過如果你想要自己創建這類東西,你就需要理解上述的每一步,返回一個新的 generator。想要拿到一個 generator 中的所有值,你就需要為它們調用 .next()。這個鏈方法會將數組編程一個 generator。然后,當你調用 .value()時,它就會反復的調用 next() 方法,直到沒有更多的值存在時。并且 .take(5) 可以確保不會去計算比你需要的更多的的計算!
現在回憶一下之前提到的定理:
list.map(inc).map(isZero) // => [false, true, false]
list.map(compose(isZero, inc)) // => [false, true, false]
延遲計算,在內部幫你完成了這類優化。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,864評論 0 38
  • JavaScript是一門很神奇的語言,作為一門現代化的語言,他有很多很有特色的東西,這些東西,讓我們看到了一個十...
    一只當飛行員的兔子閱讀 689評論 0 9
  • 1.函數參數的默認值 (1).基本用法 在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。
    趙然228閱讀 701評論 0 0
  • 編程范式 編程范式是:解決編程中的問題的過程中使用到的一種模式,體現在思考問題的方式和代碼風格上。這點很像語言,語...
    vivaxy閱讀 359評論 0 3
  • SwiftDay011.MySwiftimport UIKitprintln("Hello Swift!")var...
    smile麗語閱讀 3,850評論 0 6