在開始瞭解函數式編程風格以前,咱們須要先思考和了解幾個概念。java
引言:如下言論只是我我的的見解一些梳理與記錄,若是有任何理解和認知上您認爲有問題,很是歡迎在評論區與我一塊兒討論,指出不足之處。編程
什麼是純函數?數組
當咱們建立一個函數,若是這個函數具有如下兩個特色:
1) 這個函數指定了輸入與輸出。而且當調用參數相同時
這個函數永遠返回相同的結果,而且不依賴於任何外部狀態或數據。
2) 這個函數不會發生任何突變(mutation)或產生任何反作用(effect)。
複製代碼
當知足以上兩點咱們就稱這個函數爲【純函數(Pure Function)】。 換言之,若是使用一個函數時候,不使用他的返回值可是確有意義或做用的話,說明這個函數是【非純函數】。bash
什麼是閉包?爲何會產生閉包?如何產生的?閉包
閉包就是一個函數包含着對另外一個函數的引用
複製代碼
在建立函數的時候,js 會產生相應的執行環境,在執行環境裏會生成活動對象、做用域鏈等。 執行環境下,js 首先會利用做用域進行變量提高,而後會按順序進行執行,此時會對變量進行賦值等操做, 執行完畢後會把執行環境從執行環境棧中彈出。 可是因爲有可能一個函數包含着對另外一個活動對象的引用 致使被引用的活動對象一直沒有被釋放,這就是js 閉包產生的緣由。架構
所以因爲 js 本質是everything is object,在互相引用的過程當中就會產生閉包。app
由此能夠延展:函數式編程
1) ES6 let 聲明之因此會產生「暫存死區」(既 let 代碼塊以上沒法使用 let 聲明的變量)的現象,
也是因爲 let 在執行環境中並無變量提高的過程。
2)ES6 const 沒法再次被賦值,也是由於它只有聲明階段,沒有賦值階段。因此 const 聲明的變量
也沒法再次被賦值。
複製代碼
所以閉包具備如下特色:函數
1) 函數嵌套函數
2) 函數內部能夠引用外部的參數和變量
3) 參數和變量不會被垃圾回收機制回收
複製代碼
什麼是高階函數?測試
當開發初期,咱們使用函數對業務邏輯和運算進行封裝,使得函數能夠根據咱們的入參進行相應邏輯運算與轉換。可是若是當一個函數的參數也是一個函數時,那麼這個函數的處理業務的複雜度增長,使得其成爲一個高階函數。咱們能夠經過一個例子來觀察高階函數區別於普通函數的特色。
咱們但願過濾這個數據,找到價格高於 30 元產品,首先咱們先使用一些經常使用的數組操做來完成:
const data = [
{ id: 1, food: "手撕麪包", price: 34 },
{ id: 2, food: "牛奶", price: 20 },
{ id: 3, food: "拿鐵", price: 26 },
{ id: 4, food: "卡布奇諾", price: 28 },
{ id: 5, food: "馥芮白", price: 40 },
{ id: 6, food: "摩卡", price: 32 },
{ id: 7, food: "耶加雪啡", price: 128 },
];
// 不使用函數式編碼
// way1
let one = [];
for (let i = 0, len = data.length; i < len; i++) {
if (data[i].price > 30) {
one.push(data[i].food);
}
}
console.log(one); // [ '手撕麪包', '馥芮白', '摩卡', '耶加雪啡' ]
// way2
let two = data.map(item => {
if (item.price > 30) { return item.food }
}).filter(l => typeof l != 'undefined');
console.log(two); // [ '手撕麪包', '馥芮白', '摩卡', '耶加雪啡' ]
// way3
let three = data.filter(l => l.price > 30).map( l => l.food);
console.log(three); // [ '手撕麪包', '馥芮白', '摩卡', '耶加雪啡' ]
複製代碼
上面的 way2, way3 咱們都是在 map 內部直接建立了函數去處理業務。事實上ES6中 map, filter, reduce, some, every 也都是 高階函數 ,由於他們也是接受一個函數,根據函數執行返回結果,即 const map = list => order => list.map(order)
。
當咱們使用上面的三種方式去過濾數據的時候,能夠發現,咱們關注點在於處理什麼數據 ?按照什麼條件處理?這也是其弊端所在,接下來咱們對條件和數據進行抽象:
// way4
// order 做爲條件傳入,等待 data 的輸入
const select = order => data => data.reduce((prev, next) => {
if (order(next)) prev = [...prev, next];
return prev;
}, []);
/// 業務部分
const condition = data => data.price > 30; // 抽象函數條件
const getDataByCondition = select(condition); // 生成數據獲取函數
const getConditionResult = getDataByCondition(data);
console.log(getConditionResult.map(l => l.food));
// [ '手撕麪包', '馥芮白', '摩卡', '耶加雪啡' ]
複製代碼
對比上面的三種實現,在 reduce 高階函數參與以後,咱們將條件和數據進行抽象,將過濾函數的關鍵點都提取了出來,爲此咱們創造了一個可以接受其餘函數進行邏輯處理的 高階函數:select。它使得咱們的關注點,從邏輯的判斷,轉換成了函數的編寫與合理化的命名。多種函數互相組合,互相賦能,這也是高階函數的魅力所在。
但仔細觀察上面的實現(way4),彷佛也存在問題,reduce 做爲一個高階函數,應該能夠進一步抽象,來應對跟多場景,所以咱們能夠進一步拓展:
// way 4 改版
// 抽象 reducer 內部函數
const select = order => data => data.reduce(order, []);
// 定義 條件生成函數
const conditionHandler = condition => (prev, next) => {
if (condition(next)) prev = [...prev, next];
return prev;
}
// 業務部分
const condition = item => item.price > 30; // 定義條件
const filterByPrice = conditionHandler(condition); // 生成過濾函數
const getDataByCondition = select(filterByPrice); // 生成數據處理函數
console.log(getConditionResult.map(l => l.food));
// [ '手撕麪包', '馥芮白', '摩卡', '耶加雪啡' ]
const condition = item => item.food === "摩卡"; // 修改條件
console.log(getConditionResult.map(l => l.food)); // [ '摩卡' ]
複製代碼
到這裏,咱們針對篩選這個業務場景,抽象了兩個可複用的高階函數,select 和 conditionHandler。我在使用這兩個函數去處理篩選業務時,個人關注點在於如何合理化命名函數,理解業務並建立對應的條件函數。由此咱們發現 函數式編程 的基本思想就是這種高度抽象的編程規範。而 高階函數 則是在這種編程思惟下所使用的一種編碼方式而已。
經過上面的例子,我已經能理解 高階函數 其實就是接受其餘函數做爲入參的一種函數而已。
知道了閉包和高階函數,那麼什麼是函數的柯里化呢 ?
以前對柯里化有必定了解的朋友必定知道柯里化函數的特色或者做用:
1) 參數可複用
2) 提早確認
3) 延遲運行
複製代碼
咱們經過一個簡單的例子來理解這三個特徵: 如何實現一個加法函數,使其能夠接受任意個參數和組合形式進行加法運算,即
add(1)(2)(3) // 6
add(1, 2, 3) // 6
add(1, 2)(3) // 6
代碼實現:
function curry(fn, scope, ...args) {
let
len = fn.length, // 拿到 函數 的 參數長度
prex = args, // 保存上一次的 prex
context = scope; // 保存做用域
let newFn = (...rest) => {
let last = prex.slice(0).concat(rest); // 合併入參
if (last.length < len) {
return curry.call(this, fn, context, ...last); // 繼續柯里化
} else {
return fn.call(context, ...last); // 獲執行函數
}
}
return newFn;
}
function add(a, b, c) {
return a + b + c;
}
let curryAdd = curry(add, add);
console.log(curryAdd(1)(2)(3)); // 6
console.log(curryAdd(1, 2, 3)); // 6
console.log(curryAdd(1, 2)(3)); // 6
複製代碼
上面的代碼我加了註釋,能夠看到,經過curry 高階函數 ,當傳入參數不足的時候,咱們利用閉包的特色,保存以前入參,而且返回一個新的函數來繼續等待接收參數,以達到 參數複用 和 延時執行 的目的。能夠看到函數通過柯里化以後,對簡單的 a + b + c 這個過程進行了抽象,爲這個簡單的 + 法操做進行賦能,讓你能夠控制每個變量並根據不一樣的狀況進行函數的簡單函數的複雜抽象來應對更多的狀況,從而達到 提早確認 的特色。
由此對於函數的柯里化,咱們已經有所領悟。即 柯里化(Currying)就是將須要多個參數的函數轉換爲一個函數的過程,當提供較少的參數時,返回一個等待剩餘參數的新函數。這就是函數的柯里化。
上面的加法還能夠繼續拓展,若是想支持無限個參數進行加法,應該怎麼作呢?
add(1,2)(3)(1)(2) // 9
add(1)(1)(1)(1)(1)...(n) // n * 1
咱們須要對上面的 curry 和 add 函數進行改造,來應對這種需求
function curry(fn, scope, ...args) {
let
prex = args, // 保存上一次的 參數
context = scope; // 保存做用域
let newFn = (...rest) => {
let last = [...prex, ...rest]; // 合併入參
return ( // 當沒有入參時,執行函數,不然繼續柯里化函數
rest.length > 0 ?
curry(fn, context, ...last) :
fn.call(context, ...last)
)
}
return newFn;
}
function add(...args) {
return args.reduce((last, next) => last + next, 0);
}
var curryAdd = curry(add);
console.log(curryAdd(1, 2, 3)()); // 6
console.log(curryAdd(1, 2)(3)()); // 6
console.log(curryAdd(1)(1)(1)(1)(1)()); // 5
console.log(curryAdd(1, 1)(1, 1)()); // 4
複製代碼
這樣就實現了多參版本的 add 方法。須要關注的是,在實現多參版本時,咱們對 add 函數進行改造,咱們使用了 reduce 這個高階函數 ,高階函數和柯里化的結合,使得咱們沒必要關注函數接受的每一個參數,而專一於爲函數進行賦能。這也是函數式編程的核心所在(專一於IO);
當咱們瞭解柯里化以後,咱們很容易理解 bind 函數(綁定指針,返回一個等待執行的函數) 的實現與原理了:
// 使用: fn.bind(this, ...args); | Function.prototype.bind = fn....
Function.prototype.bindFn = function (context) {
let bindedFn = this; // 拿到 bind 的函數
let args = Array.prototype.slice.call(arguments, 1); // 去除構造函數,拿到入參
let pendingFn = function (...rest) {
let last = [...args, rest];
return bindedFn.apply(context, ...last);
}
return pendingFn; // 返回一個待執行的函數,接受新的參數
}
複製代碼
waning: 上面的代碼只是對 Bind 原理的簡單實現,沒有考慮 bind 方法使用 new 建立的狀況。
到目前爲止,咱們在回顧一下到底什麼是函數的柯里化?相信咱們能夠更好的理解柯里化(Currying)就是將須要多個參數的函數轉換爲一個函數的過程,當提供較少的參數時,返回一個等待剩餘參數的新函數。
這個定義了。
函數式編程?聲明式編程?命令式編程?
到這裏咱們已經瞭解了 純函數 ,閉包,高階函數 以及函數的 柯里化 的定義、本質,以及他們之間的關係;當咱們看透了本質,針對某種業務場景進行抽象,或是但願編寫出可複用、易測試、易維護的代碼時,咱們可能考慮高階組件,高階函數的使用。那麼就可能須要在編程風格和架構設計上作出改變。
聲明式編程與命令式編程的區別
蔬菜(類) => 作成菜(方法), 接受入參(各類菜)
蔬菜.作成菜(牛油果,各類蔬菜); // 沙拉
蔬菜.作成菜(胡蘿蔔,青菜,油); // 炒蔬菜
複製代碼
聲明式編程關注點在於 「咱們須要獲得什麼」 ,這種編程方式只在意作什麼、要獲得什麼,抽象了實現細節,而 命令式編程 實現這種過程則更像是
洗乾淨(蔬菜)
混合(蔬菜,沙拉)
放入盤中(混合物)
複製代碼
能夠發現 命令式編程 關注點在於 「咱們如何去作」,更加在意計算機執行的步驟,一步一步告訴計算機先幹什麼,再幹什麼。把細節按照人類的思想以代碼的形式表現出來。這也是爲何命令式編程將直接致使代碼難以維護、難以測試、難以複用的緣由(部分業務場景)。
函數式編程與聲明式編程的關係
在 javascirpt 中,咱們將 函數 做爲參數進行傳遞,創造複雜度更高,功能更增強大的函數。咱們進行函數式編程,把函數做爲 「一等公民」。經過純函數, 遞歸, 內聚, 惰性計算, 映射等進行組合,使得代碼在實現 「要獲得什麼」 這個過程當中,獲取更強大的抽象與計算能力。所以 函數式編程 是 聲明式編程 的一部分。
總結
當一個函數指定輸入後,輸出永遠相同,而且沒有任何突變及反作用,那麼這個函數就是一個純函數
閉包就是一個函數包含着對另外一個函數的引用
接受其餘函數做爲入參的一種高級函數
將須要多個參數的函數轉換爲一個函數的過程,當提供較少的參數時,返回一個等待剩餘參數的新函數
命令式編程關注實現細節,聲明式編程關注實現結果,弱化並抽象細節。函數式編程是聲明式編程的一部分。