Read the originaljavascript
函數式編程與咱們以往的編程習慣有許多不一樣。這篇文章舉了一些JavaScript的例子,介紹了函數式編程中重要的概念。附加的文章會讓你更深刻的瞭解JavaScript中的函數式編程。html
本文源碼能夠在GitHub上找到,放在jsFunctionalProgramming倉庫中。java
我要感謝Csaba Hellinger的支持和投入,在他的幫助下我才完成這篇文章。git
函數式編程由Lambda Calculus演化而來,它是一個抽象數學的函數表述,咱們將思考怎麼把它運用在現實中。es6
函數式編程是聲明式編程的範式。github
函數式編程有如下具體特性:算法
避免狀態改變(可變的數據) - 函數式編程的特性之一就是:函數在應用中不會改變狀態,它們(functions)寧願從以前的狀態之上建立一個新的狀態。編程
函數聲明 vs 函數表達式 - 在函數式編程中,咱們定義和描述函數就像數學中的一個方法聲明。數組
冪等性 - 這意味着咱們用相同的參數調用一個函數(無論任什麼時候刻)它都會返回相同的結果,這也能夠避免狀態的改變。瀏覽器
這三個特性咋一看彷佛並無什麼意義,但若是咱們更深刻的分析,發如今如下三種狀況下使用函數式編程能充分發揮這三個特性:
並行的代碼執行 - 由於函數式編程有冪等性和避免狀態改變的特性,用函數方法編寫代碼會讓並行更容易,由於不會出現同步問題。
簡明、簡潔的代碼 - 由於函數式編程使用方法聲明的方式,代碼不會像面向過程編程同樣,有額外的算法步驟。
不一樣的編程思想 - 一旦你真正使用了一門函數式編程語言,你會擁有一種新的編程思想,當你構建應用時也會有新的點子。
javascript 是一門真正的(純粹的)函數式編程語言嗎?
不!JavaScript並非一門純粹的函數式編程語言...
它能夠很好的運用在函數式編程中,由於函數是第一性對象。若是在一門編程語言中,函數和其餘類型同樣,那麼這門語言中的函數就是第一型對象。舉個例子,函數能夠做爲參數傳遞給其餘函數,也能夠賦值給變量。
咱們將檢查一些函數是不是第一型對象,可是在這以前,咱們先構建一個代碼塊,咱們將像真正的函數式語言同樣使用JavaScript。
在大部分純函數式編程語言中(Haskell, Clean, Erlang),它們是沒有for
或者while
循環的,因此循環一個列表須要用到遞歸函數。純函數式編程語言有語言支持和最好的列表推導式和列表串聯。
這裏有一個函數實現了for
循環,咱們將在接下來的代碼中用到它,可是你也將看到它在JS中的侷限性,由於尾部調用優化並無被普遍的支持,但之後會好起來的。
function funcFor(first, last, step, callback) {
//
//遞歸inner函數
//
function inner(index) {
if((step > 0 && index >= last) || (step < 0 && index < last)) {
return;
}
callback(index);
//
//接下來進行尾部調用
//
inner(index + step);
}
//
//開始遞歸
//
inner(first);
}複製代碼
inner
函數包含了對中止遞歸的管理,它傳入參數index
去調用callback
,再遞歸調用inner(index + step)
確保循環傳遞到下一步。
遞歸是函數式編程的一個重要方面。
如今,讓咱們看看真正的函數式編程:
function applyIfAllNumbers(items, fn) {
if(areNumbers(items)) {
return funcMap(items, fn);
}
return [];
}複製代碼
applyIfAllNumbers
函數的目的是調用fn
函數,並把items
中的每一個數字做爲參數傳入,但前提是隻有在items
數組中都是數字的狀況下才去調用。
下面是驗證器函數:
function areNumbers(numbers) {
if(numbers.length == 0) {
return true;
}
else {
return isNumber(number[0]) && areNumbers(numbers.slice(1));
}
}
function isNumber(n) {
return isFinite(n) && +n === n;
}複製代碼
這段代碼簡單明瞭,若是參數是一個數字,isNumber
函數返回true
,不然返回false
。areNumbers
函數使用isNumber
函數判斷numbers
數組中是否全是數字(再提醒一次,遞歸經常被用來實現這種邏輯)。
另外一個例子是applyForNumbersOnly
:
function applyForNumbersOnly(items, fn) {
let numbers = filter(items, isNumber);
return funcMap(numbers, fn);
}複製代碼
這樣寫甚至更簡潔:
function applyForNumbersOnly(items, fn) {
return funcMap(filter(items, isNumber), fn);
}複製代碼
applyForNumbersOnly
調用fn
方法僅僅是爲了收集items
中的數字。
funcMap
函數在函數式編程中重現了著名的map
函數,可是這裏我藉助了funcForEach
函數來建立它:
function funcForEach(items, fn) {
return funcFor(0, items.length, 1, function(idx) {
fn(items[idx]);
});
}
function funcMap(items, fn) {
let result = [];
funcForEach(items, function(item) {
result.push(fn(item));
});
return result;
}複製代碼
最後還剩filter
函數,咱們再一次使用遞歸來實現過濾的邏輯。
function filter(input, callback) {
function inner(input, callback, index, output) {
if (index === input.length) {
return output;
}
return inner(
input,
callback,
index + 1,
callback(input[index]) ? output.concat(input[index]) : output;
);
}
return inner(input, callback, 0, []);
}複製代碼
在EcmaScript 2015 TCO文檔中有一些用例的定義,這門語言不久就將支持尾調用優化了。最關鍵的一點就是在你的代碼中使用use strict
模式,不然JS不能支持尾調用優化。
因爲沒有內置方法來檢測瀏覽器是否支持尾調動優化,如下代碼實現了這個功能:
"use static"
function isTCOSupported() {
const outerStackLen = new Error().stack.length;
//inner函數的name長度必定不能超過外部函數
return (function inner() {
const innerStackLen = new Error().stack.length;
return innerStackLen <= outerStackLen;
}());
}
console.log(isTCOSupported() ? "TCO Available" : "TCO N/A");複製代碼
這裏有一個重現Math.pow
函數的例子,它能從EcmaScript 2015的TCO中獲益。
這個pow函數的實現使用了ES6默認參數,讓它看上去更簡潔。
function powES6(base, power, result=base) {
if (power === 0) {
return 1;
}
if(power === 1) {
return result;
}
return powES6(base, power - 1, result * base);
}複製代碼
首先要提醒如下,powES6
函數有三個參數而不是兩個。第三個參數是計算後的值。咱們隨身攜帶return
是爲了實現讓咱們的遞歸調用變成真正的尾調用,讓JS可使用它的尾調用優化技術。
萬一咱們不能使用ES6的特性,那麼咱們不推薦使用遞歸去實現pow
函數,由於這門語言尚未提出有關遞歸的優化,這樣實現起來就很複雜了:
function recursivePow(base, power, result) {
if (power === 0) {
return 1;
}
else if(power === 1) {
return result;
}
return recursivePow(base, power - 1, result * base);
}
function pow(base, power) {
return recursivePow(base, power, base);
}複製代碼
咱們把遞歸計算放在了另外一個recursivePow
函數中,這個函數有三個參數,就像powES6
函數同樣。使用一個新函數並把base
做爲參數傳遞給它,以此實現ES6中的默認參數邏輯。
在這個頁面你能夠查看TCO在不一樣瀏覽器和平臺的支持狀況。
目前只有Safari 10是徹底支持TCO的瀏覽器(在寫這篇文章時),我將進行一些對於pow
的測試,來看看它的表現。
我使用了powES6
和pow
函數來進行測試:
"use strict";
function stressPow(n) {
var result = [];
for (var i=0; i<n; ++i) {
result.push(
pow(2, 0),
pow(2, 1),
pow(2, 2),
pow(2, 3),
pow(2, 4),
pow(2, 5),
pow(2, 10),
pow(2, 20),
pow(2, 30),
pow(1, 10000),
pow(2, 40),
pow(3, 10),
pow(4, 15),
pow(1, 11000),
pow(3.22, 125),
pow(3.1415, 89),
pow(7, 2500),
pow(2, 13000)
);
}
return result;
}
var start = performance.now();
var result_standard = stressPow(2500);
var duration = performance.now() - start;
console.log(result_standard);
console.log(`Duration: ${duration} ms.`);複製代碼
我在Chrome v55, Firefox v50, Safari v9.2 和 Safari v10上測試了以上代碼。
根據上面的數據,咱們得出Safari對遞歸函數的優化效率是最高的。Safari 10對尾調用的支持是最好的,速度比Chrome快了大約2.8倍。Firefox幾乎和Safari 9.2 同樣棒,這出乎了個人意料。
若是你很喜歡這篇文章,請點個贊哦。(譯者注:話說好長啊,好累啊。)
讓咱們繼續函數式!
PART 2 也即將發出,關於高階函數和例子,講解如何編寫函數式風格的代碼。
喜歡本文的朋友能夠關注個人微信公衆號,不按期推送一些好文。
本文由Rockjins Blog翻譯,轉載請與譯者聯繫。不然將追究法律責任。