如何快速入門VUE3.0:進入學習
相關推薦:javascript學習教學
看過許多關於函數式程式設計的講解,但是其中大部分是停留在理論層面,還有一些是僅針對Haskell 等純函數式程式語言的。而本文旨在聊一聊我眼中的函數式程式設計在JavaScript 中的具體實踐,之所以是「我眼中的」 即我所說的僅代表個人觀點,可能和部分嚴格概念是有衝突的。
本文將略去一大堆形式化的概念介紹,重點介紹在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 做數組遍歷,減少了中間變數和運算。
而函數式寫法一和函數式寫法二之間的主要差異在於,可以考慮函數是否後續有復用的可能,如果沒有,則後者更優。
從上面函數式寫法二我們可以看出,函數式程式碼在寫的過程中,很容易造成橫向延展,也就是產生多層嵌套,以下我們舉個比較極端點的例子。
Javascript程式碼
// 計算數字總和// 一般寫法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 的鍊式寫法) Javascript程式碼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 模式。
Javascript程式碼
// 順序請求兩個介面// 回呼函數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 的鍊式結構,在高複雜度時,仍能縱向擴展,而且層次隔離很清晰。
常見的函數式程式設計模型
可以保留局部變數不被釋放的程式碼區塊,稱為一個閉包
閉包的概念比較抽象,相信大家都或多或少知道、用到這個特性
那麼閉包到底能帶給我們什麼好處?
先來看看如何建立一個閉包:
Javascript程式碼
// 建立一個閉包function makeCounter() { let k = 0; return function() { return ++k; }; } const counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2
makeCounter 這個函數的程式碼區塊,在傳回的函數中,對局部變數k ,進行了引用,導致局部變數無法在函數執行結束後,被系統回收掉,產生了閉包。而這個閉包的作用就是,「保留住「 了局部變量,使內層函數呼叫時,可以重複使用該變數;而不同於全域變量,該變數只能在函數內部被引用。
換句話說,閉包其實就是創造出了一些函數私有的」持久化變數「。
所以從這個例子,我們可以總結出,閉包的創造條件是:
存在內、外兩層函數內層函數對外層函數的局部變數進行了引用閉包的用途閉包的主要用途就是可以定義一些作用域局限的持久化變量,這些變數可以用來做快取或是計算的中間量等等。
Javascript程式碼
// 簡單的快取工具// 匿名函數創造了一個閉包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 作為一個高階函數,他接受一個函數參數作為映射的邏輯
Javascript代碼
// 數組中每一項加一,組成一個新數組// 一般寫法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 函數封裝了必要的操作,使我們只需要關心映射邏輯的函數實作即可,減少了程式碼量,也降低了副作用產生的風險。
給定一個函數的部分參數,產生一個接受其他參數的新函數
可能不常聽到這個名詞,但是用過undescore 或lodash 的人都見過他。
有一個神奇的_.partial 函數,它就是柯里化的實作
Javascript程式碼
// 取得目標檔案對基礎路徑的相對路徑// 一般寫法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 的路徑,而不是相對於任何路徑。柯里化可以使我們只關心函數的部分參數,使函數的用途更加清晰,呼叫更加簡單。
將多個函數的能力合併,創造一個新的函數
同樣你第一次見到他可能還是在lodash 中,compose 方法(現在叫flow)
Javascript代碼
// 數組中每個單詞大寫,做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 函數式編程,可能和許多傳統概念不同。我並不只認為高階函數算函數式編程,其他的諸如普通函數結合調用、鍊式結構等,我都認為屬於函數式編程的範疇,只要他們是以函數作為主要載體的。
而我認為函數式程式設計並不是必須的,它也不應該是一個強制的規定或要求。與物件導向或其他想法一樣,它也是其中一種方式。我們更多情況下,應該是幾者的結合,而不是侷限於概念。
相關推薦:javascript教學
以上就是一起聊聊JavaScript函數式程式設計的詳細內容,更多請關注php中文網其它相關文章!