一文帶你了解JavaScript函數(shù)式編程?
原創(chuàng)【51CTO.com原創(chuàng)稿件】
前言
函數(shù)式編程在前端已經(jīng)成為了一個非常熱門的話題。在最近幾年里,我們看到非常多的應用程序代碼庫里大量使用著函數(shù)式編程思想。
本文將略去那些晦澀難懂的概念介紹,重點展示在 JavaScript 中到底什么是函數(shù)式的代碼、聲明式與命令式代碼的區(qū)別、以及常見的函數(shù)式模型都有哪些?
一、什么是函數(shù)式編程
函數(shù)式編程是一種編程范式,主要是利用函數(shù)把運算過程封裝起來,通過組合各種函數(shù)來計算結(jié)果。函數(shù)式編程意味著你可以在更短的時間內(nèi)編寫具有更少錯誤的代碼。舉個簡單的例子,假設我們要把字符串 functional programming is great
變成每個單詞首字母大寫,我們可以這樣實現(xiàn):
- var string = 'functional programming is great';
- var result = string
- .split(' ')
- .map(v => v.slice(0, 1).toUpperCase() + v.slice(1))
- .join(' ');
上面的例子先用 split 把字符串轉(zhuǎn)換數(shù)組,然后再通過 map 把各元素的首字母轉(zhuǎn)換成大寫,最后通過 join 把數(shù)組轉(zhuǎn)換成字符串。 整個過程就是 join(map(split(str)))
,體現(xiàn)了函數(shù)式編程的核心思想: 通過函數(shù)對數(shù)據(jù)進行轉(zhuǎn)換。
由此我們可以得到,函數(shù)式編程有兩個基本特點:
-
通過函數(shù)來對數(shù)據(jù)進行轉(zhuǎn)換
-
通過串聯(lián)多個函數(shù)來求結(jié)果
二、對比聲明式與命令式
接下來我們先介紹兩種編程范式:
-
命令式:我們通過編寫一條又一條指令去讓計算機執(zhí)行一些動作,這其中一般都會涉及到很多繁雜的細節(jié)。命令式代碼中頻繁使用語句,來完成某個行為。比如 for、if、switch、throw 等這些語句。
-
聲明式:我們通過寫表達式的方式來聲明我們想干什么,而不是通過一步一步的指示。表達式通常是某些函數(shù)調(diào)用的復合、一些值和操作符,用來計算出結(jié)果值。
- //命令式
- var CEOs = [];
- for(var i = 0; i < companies.length; i++){
- CEOs.push(companies[i].CEO)
- }
-
- //聲明式
- var CEOs = companies.map(c => c.CEO);
從上面的例子中,我們可以看到聲明式的寫法是一個表達式,無需關(guān)心如何進行計數(shù)器迭代,返回的數(shù)組如何收集,它指明的是做什么,而不是怎么做。函數(shù)式編程的一個明顯的好處就是這種聲明式的代碼,對于無副作用的純函數(shù),我們完全可以不考慮函數(shù)內(nèi)部是如何實現(xiàn)的,專注于編寫業(yè)務代碼。
三、常見特性
很多時候我們?nèi)ゲ殚喓瘮?shù)式編程的相關(guān)資料,經(jīng)常會看到以下幾個特性:
無副作用
指調(diào)用函數(shù)時不會修改外部狀態(tài),即一個函數(shù)調(diào)用 n 次后依然返回同樣的結(jié)果。
- var a = 1;
- // 含有副作用,它修改了外部變量 a
- // 多次調(diào)用結(jié)果不一樣
- function test1() {
- a++
- return a;
- }
-
- // 無副作用,沒有修改外部狀態(tài)
- // 多次調(diào)用結(jié)果一樣
- function test2(a) {
- return a + 1;
- }
透明引用
指一個函數(shù)只會用到傳遞給它的變量以及自己內(nèi)部創(chuàng)建的變量,不會使用到其他變量。
- var a = 1;
- var b = 2;
- // 函數(shù)內(nèi)部使用的變量并不屬于它的作用域
- function test1() {
- return a + b;
- }
- // 函數(shù)內(nèi)部使用的變量是顯式傳遞進去的
- function test2(a, b) {
- return a + b;
- }
不可變變量
指的是一個變量一旦創(chuàng)建后,就不能再進行修改,任何修改都會生成一個新的變量。使用不可變變量最大的好處是線程安全。多個線程可以同時訪問同一個不可變變量,讓并行變得更容易實現(xiàn)。 由于 JavaScript 原生不支持不可變變量,需要通過第三方庫來實現(xiàn)。 (如 Immutable.js,Mori 等等)
- var obj = Immutable({ a: 1 });
- var obj2 = obj.set('a', 2);
- console.log(obj); // Immutable({ a: 1 })
- console.log(obj2); // Immutable({ a: 2 })
函數(shù)是一等公民
我們常說函數(shù)是JavaScript的"第一等公民",指的是函數(shù)與其他數(shù)據(jù)類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數(shù),傳入另一個函數(shù),或者作為別的函數(shù)的返回值。下文將要介紹的閉包、高階函數(shù)、函數(shù)柯里化和函數(shù)組合都是圍繞這一特性的應用
四、常見的函數(shù)式編程模型
常見的函數(shù)式編程模型有閉包、高階函數(shù)、函數(shù)柯里化以及函數(shù)組合,以下將一一詳細介紹:
1.閉包(Closure)
如果一個函數(shù)引用了自由變量,那么該函數(shù)就是一個閉包。何謂自由變量?自由變量是指不屬于該函數(shù)作用域的變量(所有全局變量都是自由變量,嚴格來說引用了全局變量的函數(shù)都是閉包,但這種閉包并沒有什么用,通常情況下我們說的閉包是指函數(shù)內(nèi)部的函數(shù))。
閉包的形成條件:
-
存在內(nèi)、外兩層函數(shù)
-
內(nèi)層函數(shù)對外層函數(shù)的局部變量進行了引用
閉包的用途: 可以定義一些作用域局限的持久化變量,這些變量可以用來做緩存或者計算的中間量等。
- // 簡單的緩存工具
- // 匿名函數(shù)創(chuàng)造了一個閉包
- const cache = (function() {
- const store = {};
- return {
- get(key) {
- return store[key];
- },
- set(key, val) {
- store[key] = val;
- }
- }
- }());
- console.log(cache) //{get: ƒ, set: ƒ}
- cache.set('a', 1);
- cache.get('a'); // 1
上面例子是一個簡單的緩存工具的實現(xiàn),匿名函數(shù)創(chuàng)造了一個閉包,使得 store 對象 ,一直可以被引用,不會被回收。
閉包的弊端:持久化變量不會被正常釋放,持續(xù)占用內(nèi)存空間,很容易造成內(nèi)存浪費,所以一般需要一些額外手動的清理機制。
2.高階函數(shù)
函數(shù)式編程傾向于復用一組通用的函數(shù)功能來處理數(shù)據(jù),它通過使用高階函數(shù)來實現(xiàn)。高階函數(shù)指的是一個函數(shù)以函數(shù)為參數(shù),或以函數(shù)為返回值,或者既以函數(shù)為參數(shù)又以函數(shù)為返回值。
高階函數(shù)經(jīng)常用于:
-
抽象或隔離行為、作用,異步控制流程作為回調(diào)函數(shù),promises,monads等
-
創(chuàng)建可以泛用于各種數(shù)據(jù)類型的功能
-
部分應用于函數(shù)參數(shù)(偏函數(shù)應用)或創(chuàng)建一個柯里化的函數(shù),用于復用或函數(shù)復合。
-
接受一個函數(shù)列表并返回一些由這個列表中的函數(shù)組成的復合函數(shù)。
JavaScript 語言是原生支持高階函數(shù)的, 例如Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是JavaScript中內(nèi)置的一些高階函數(shù),使用高階函數(shù)會讓我們的代碼更清晰簡潔。
map
map() 方法創(chuàng)建一個新數(shù)組,其結(jié)果是該數(shù)組中的每個元素都調(diào)用一個提供的函數(shù)后返回的結(jié)果。map 不會改變原數(shù)組。
假設我們有一個包含名稱和種類屬性的對象數(shù)組,我們想要這個數(shù)組中所有名稱屬性放在一個新數(shù)組中,如何實現(xiàn)呢?
- // 不使用高階函數(shù)
- var animals = [
- { name: "Fluffykins", species: "rabbit" },
- { name: "Caro", species: "dog" },
- { name: "Hamilton", species: "dog" },
- { name: "Harold", species: "fish" },
- { name: "Ursula", species: "cat" },
- { name: "Jimmy", species: "fish" }
- ];
- var names = [];
- for (let i = 0; i < animals.length; i++) {
- names.push(animals[i].name);
- }
- console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]
- // 使用高階函數(shù)
- var animals = [
- { name: "Fluffykins", species: "rabbit" },
- { name: "Caro", species: "dog" },
- { name: "Hamilton", species: "dog" },
- { name: "Harold", species: "fish" },
- { name: "Ursula", species: "cat" },
- { name: "Jimmy", species: "fish" }
- ];
- var names = animals.map(x=>x.name);
- console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]
filter
filter() 方法會創(chuàng)建一個新數(shù)組,其中包含所有通過回調(diào)函數(shù)測試的元素。filter 為數(shù)組中的每個元素調(diào)用一次 callback 函數(shù), callback 函數(shù)返回 true 表示該元素通過測試,保留該元素,false 則不保留。filter 不會改變原數(shù)組,它返回過濾后的新數(shù)組。
假設我們有一個包含名稱和種類屬性的對象數(shù)組。 我們想要創(chuàng)建一個只包含狗(species: "dog")的數(shù)組。如何實現(xiàn)呢?
- // 不使用高階函數(shù)
- var animals = [
- { name: "Fluffykins", species: "rabbit" },
- { name: "Caro", species: "dog" },
- { name: "Hamilton", species: "dog" },
- { name: "Harold", species: "fish" },
- { name: "Ursula", species: "cat" },
- { name: "Jimmy", species: "fish" }
- ];
- var dogs = [];
- for (var i = 0; i < animals.length; i++) {
- if (animals[i].species === "dog") dogs.push(animals[i]);
- }
- console.log(dogs);
- // 使用高階函數(shù)
- var animals = [
- { name: "Fluffykins", species: "rabbit" },
- { name: "Caro", species: "dog" },
- { name: "Hamilton", species: "dog" },
- { name: "Harold", species: "fish" },
- { name: "Ursula", species: "cat" },
- { name: "Jimmy", species: "fish" }
- ];
- var dogs = animals.filter(x => x.species === "dog");
- console.log(dogs); // {name: "Caro", species: "dog"}
- // { name: "Hamilton", species: "dog" }
reduce
reduce 方法對調(diào)用數(shù)組的每個元素執(zhí)行回調(diào)函數(shù),最后生成一個單一的值并返回。 reduce 方法接受兩個參數(shù):1)reducer 函數(shù)(回調(diào)),2)一個可選的 initialValue。
假設我們要對一個數(shù)組的求和:
- // 不使用高階函數(shù)
- const arr = [5, 7, 1, 8, 4];
- let sum = 0;
- for (let i = 0; i < arr.length; i++) {
- sum = sum + arr[i];
- }
- console.log(sum);//25
- // 使用高階函數(shù)
- const arr = [5, 7, 1, 8, 4];
- const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0);
- console.log(sum)//25
我們可以通過下圖,形象生動展示三者的區(qū)別:
3.函數(shù)柯里化
柯里化又稱部分求值,柯里化函數(shù)會接收一些參數(shù),然后不會立即求值,而是繼續(xù)返回一個新函數(shù),將傳入的參數(shù)通過閉包的形式保存,等到被真正求值的時候,再一次性把所有傳入的參數(shù)進行求值。
- // 普通函數(shù)
- function add(x,y){
- return x + y;
- }
- add(1,2); // 3
- // 函數(shù)柯里化
- var add = function(x) {
- return function(y) {
- return x + y;
- };
- };
- var increment = add(1);
- increment(2);// 3
這里我們定義了一個 add 函數(shù),它接受一個參數(shù)并返回一個新的函數(shù)。調(diào)用 add 之后,返回的函數(shù)就通過閉包的方式記住了 add 的第一個參數(shù)。
4.函數(shù)組合 (Composition)
前面提到過,函數(shù)式編程的一個特點是通過串聯(lián)函數(shù)來求值。然而,隨著串聯(lián)函數(shù)數(shù)量的增多,代碼的可讀性就會不斷下降。函數(shù)組合就是用來解決這個問題的方法。 假設有一個 compose 函數(shù),它可以接受多個函數(shù)作為參數(shù),然后返回一個新的函數(shù)。當我們?yōu)檫@個新函數(shù)傳遞參數(shù)時,該參數(shù)就會「流」過其中的函數(shù),最后返回結(jié)果。
- //兩個函數(shù)的組合
- var compose = function(f, g) {
- return function(x) {
- return f(g(x));
- };
- };
-
- //或者
- var compose = (f, g) => (x => f(g(x)));
- var add1 = x => x + 1;
- var mul5 = x => x * 5;
- compose(mul5, add1)(2);// =>15
參考文章
作者介紹
浪里行舟,慕課網(wǎng)認證作者,前端愛好者,立志往全棧工程師發(fā)展,從事前端一年多,目前技術(shù)棧有vue全家桶、ES6以及l(fā)ess等,樂于分享,最近一年寫了五六十篇原創(chuàng)技術(shù)文章,得到諸多好評!
【51CTO原創(chuàng)稿件,合作站點轉(zhuǎn)載請注明原文作者和出處為51CTO.com】