javascript
我眼中的JavaScript函数式编程
JavaScript 函數式編程是一個存在了很久的話題,但似乎從 2016 年開始,它變得越來越火熱。這可能是因為 ES6 語法對于函數式編程更為友好,也可能是因為諸如 RxJS (ReactiveX) 等函數式框架的流行。
看過許多關于函數式編程的講解,但是其中大部分是停留在理論層面,還有一些是僅針對 Haskell 等純函數式編程語言的。而本文旨在聊一聊我眼中的函數式編程在?JavaScript?中的具體實踐,之所以是 “我眼中的” 即我所說的僅代表個人觀點,可能和部分?嚴格概念?是有沖突的。
本文將略去一大堆形式化的概念介紹,重點展示在 JavaScript 中到底什么是函數式的代碼、函數式代碼與一般寫法有什么區別、函數式的代碼能給我們帶來什么好處以及常見的一些函數式模型都有哪些。
我理解的函數式編程
我認為函數式編程可以理解為,以函數作為主要載體的編程方式,用函數去拆解、抽象一般的表達式
與命令式相比,這樣做的好處在哪?主要有以下幾點:
- 語義更加清晰
- 可復用性更高
- 可維護性更好
- 作用域局限,副作用少
基本的函數式編程
下面例子是一個具體的函數式體現
// 數組中每個單詞,首字母大寫// 一般寫法 const arr = ['apple', 'pen', 'apple-pen']; for(const i in arr){const c = arr[i][0];arr[i] = c.toUpperCase() + arr[i].slice(1); }console.log(arr);// 函數式寫法一 function upperFirst(word) {return word[0].toUpperCase() + word.slice(1); }function wordToUpperCase(arr) {return arr.map(upperFirst); }console.log(wordToUpperCase(['apple', 'pen', 'apple-pen']));// 函數式寫法二 console.log(arr.map(['apple', 'pen', 'apple-pen'], word => word[0].toUpperCase() + word.slice(1)));當情況變得更加復雜時,表達式的寫法會遇到幾個問題:
函數式編程很好的解決了上述問題。首先參看?函數式寫法一,它利用了函數封裝性將功能做拆解(粒度不唯一),并封裝為不同的函數,而再利用組合的調用達到目的。這樣做使得表意清晰,易于維護、復用以及擴展。其次利用?高階函數,Array.map?代替?for…of?做數組遍歷,減少了中間變量和操作。
而?函數式寫法一?和?函數式寫法二?之間的主要差別在于,可以考慮函數是否后續有復用的可能,如果沒有,則后者更優。
鏈式優化
從上面?函數式寫法二?中我們可以看出,函數式代碼在寫的過程中,很容易造成?橫向延展,即產生多層嵌套,下面我們舉個比較極端點的例子。
// 計算數字之和// 一般寫法 console.log(1 + 2 + 3 - 4)// 函數式寫法 function sum(a, b) {return a + b; }function sub(a, b) {return a - b; }console.log(sub(sum(sum(1, 2), 3), 4);本例僅為展示?橫向延展?的比較極端的情況,隨著函數的嵌套層數不斷增多,導致代碼的可讀性大幅下降,還很容易產生錯誤。
在這種情況下,我們可以考慮多種優化方式,比如下面的?鏈式優化?。
// 優化寫法 (嗯,你沒看錯,這就是 lodash 的鏈式寫法) const utils = {chain(a) {this._temp = a;return this;},sum(b) {this._temp += b;return this;},sub(b) {this._temp -= b;return this;},value() {const _temp = this._temp;this._temp = undefined;return _temp;} };console.log(utils.chain(1).sum(2).sum(3).sub(4).value());這樣改寫后,結構會整體變得比較清晰,而且鏈的每一環在做什么也可以很容易的展現出來。函數的嵌套和鏈式的對比還有一個很好的例子,那就是?回調函數 和 Promise 模式。
// 順序請求兩個接口// 回調函數 import $ from 'jquery'; $.post('a/url/to/target', (rs) => {if(rs){$.post('a/url/to/another/target', (rs2) => {if(rs2){$.post('a/url/to/third/target');}});} });// Promise import request from 'catta'; // catta 是一個輕量級請求工具,支持 fetch,jsonp,ajax,無依賴 request('a/url/to/target').then(rs => rs ? $.post('a/url/to/another/target') : Promise.reject()).then(rs2 => rs2 ? $.post('a/url/to/third/target') : Promise.reject());隨著回調函數嵌套層級和單層復雜度增加,它將會變得臃腫且難以維護,而?Promise?的鏈式結構,在高復雜度時,仍能縱向擴展,而且層次隔離很清晰。
常見的函數式編程模型
閉包(Closure)
可以保留局部變量不被釋放的代碼塊,被稱為一個閉包
閉包的概念比較抽象,相信大家都或多或少知道、用到這個特性
那么閉包到底能給我們帶來什么好處?
先來看一下如何創建一個閉包:
// 創建一個閉包 function makeCounter() {let k = 0;return function() {return ++k;}; }const counter = makeCounter();console.log(counter()); // 1 console.log(counter()); // 2makeCounter?這個函數的代碼塊,在返回的函數中,對局部變量?k?,進行了引用,導致局部變量無法在函數執行結束后,被系統回收掉,從而產生了閉包。而這個閉包的作用就是,“保留住“ 了局部變量,使內層函數調用時,可以重復使用該變量;而不同于全局變量,該變量只能在函數內部被引用。
換句話說,閉包其實就是創造出了一些函數私有的 ”持久化變量“。
所以從這個例子,我們可以總結出,閉包的創造條件是:
閉包的用途
閉包的主要用途就是可以定義一些作用域局限的持久化變量,這些變量可以用來做緩存或者計算的中間量等等。
// 簡單的緩存工具 // 匿名函數創造了一個閉包 const cache = (function() {const store = {};return {get(key) {return store[key];},set(key, val) {store[key] = val;}} }());cache.set('a', 1); cache.get('a'); // 1上面例子是一個簡單的緩存工具的實現,匿名函數創造了一個閉包,使得?store?對象 ,一直可以被引用,不會被回收。
閉包的弊端
持久化變量不會被正常釋放,持續占用內存空間,很容易造成內存浪費,所以一般需要一些額外手動的清理機制。
高階函數
接受或者返回一個函數的函數稱為高階函數
聽上去很高冷的一個詞匯,但是其實我們經常用到,只是原來不知道他們的名字而已。JavaScript 語言是原生支持高階函數的,因為 JavaScript 的函數是一等公民,它既可以作為參數又可以作為另一個函數的返回值使用。
我們經常可以在 JavaScript 中見到許多原生的高階函數,例如?Array.map?,?Array.reduce?,?Array.filter
下面以?map?為例,我們看看他是如何使用的
map (映射)
映射是對集合而言的,即把集合的每一項都做相同的變換,產生一個新的集合
map?作為一個高階函數,他接受一個函數參數作為映射的邏輯
// 數組中每一項加一,組成一個新數組// 一般寫法 const arr = [1,2,3]; const rs = []; for(const n of arr){rs.push(++n); } console.log(rs)// map改寫 const arr = [1,2,3]; const rs = arr.map(n => ++n);上面一般寫法,利用?for...of?循環的方式遍歷數組會產生額外的操作,而且有改變原數組的風險
而?map?函數封裝了必要的操作,使我們僅需要關心映射邏輯的函數實現即可,減少了代碼量,也降低了副作用產生的風險。
柯里化(Currying)
給定一個函數的部分參數,生成一個接受其他參數的新函數
可能不常聽到這個名詞,但是用過?undescore?或?lodash?的人都見過他。
有一個神奇的?_.partial?函數,它就是柯里化的實現
// 獲取目標文件對基礎路徑的相對路徑// 一般寫法 const BASE = '/path/to/base'; const relativePath = path.relative(BASE, '/some/path');// _.parical 改寫 const BASE = '/path/to/base'; const relativeFromBase = _.partial(path.relative, BASE);const relativePath = relativeFromBase('/some/path');通過?_.partial?,我們得到了新的函數?relativeFromBase?,這個函數在調用時就相當于調用?path.relative?,并默認將第一個參數傳入?BASE?,后續傳入的參數順序后置。
本例中,我們真正想完成的操作是每次獲得相對于?BASE?的路徑,而非相對于任何路徑。柯里化可以使我們只關心函數的部分參數,使函數的用途更加清晰,調用更加簡單。
組合(Composing)
將多個函數的能力合并,創造一個新的函數
同樣你第一次見到他可能還是在?lodash?中,compose?方法(現在叫?flow)
// 數組中每個單詞大寫,做 Base64// 一般寫法 (其中一種) const arr = ['pen', 'apple', 'applypen']; const rs = []; for(const w of arr){rs.push(btoa(w.toUpperCase())); } console.log(rs);// _.flow 改寫 const arr = ['pen', 'apple', 'applypen']; const upperAndBase64 = _.partialRight(_.map, _.flow(_.upperCase, btoa)); console.log(upperAndBase64(arr));_.flow?將轉大寫和轉 Base64 的函數的能力合并,生成一個新的函數。方便作為參數函數或后續復用。
自己的觀點
我理解的 JavaScript 函數式編程,可能和許多傳統概念不同。我并不只認為?高階函數?算函數式編程,其他的諸如普通函數結合調用、鏈式結構等,我都認為屬于函數式編程的范疇,只要他們是以函數作為主要載體的。
而我認為函數式編程并不是必須的,它也不應該是一個強制的規定或要求。與面向對象或其他思想一樣,它也是其中一種方式。我們更多情況下,應該是幾者的結合,而不是局限于概念。
from:?http://developer.51cto.com/art/201703/535299.htm
總結
以上是生活随笔為你收集整理的我眼中的JavaScript函数式编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每个前端开发者必会的二十个JavaScr
- 下一篇: JavaScript中的遍历详解