摘要:前言函數式編程在前端已經成為了一個非常熱門的話題。整個過程就是體現了函數式編程的核心思想通過函數對數據進行轉換。高階函數函數式編程傾向于復用一組通用的函數功能來處理數據,它通過使用高階函數來實現。
前言
函數式編程在前端已經成為了一個非常熱門的話題。在最近幾年里,我們看到非常多的應用程序代碼庫里大量使用著函數式編程思想。
本文將略去那些晦澀難懂的概念介紹,重點展示在 JavaScript 中到底什么是函數式的代碼、聲明式與命令式代碼的區(qū)別、以及常見的函數式模型都有哪些?想閱讀更多優(yōu)質文章請猛戳GitHub博客
一、什么是函數式編程函數式編程是一種編程范式,主要是利用函數把運算過程封裝起來,通過組合各種函數來計算結果。函數式編程意味著你可以在更短的時間內編寫具有更少錯誤的代碼。舉個簡單的例子,假設我們要把字符串 functional programming is great 變成每個單詞首字母大寫,我們可以這樣實現:
var string = "functional programming is great"; var result = string .split(" ") .map(v => v.slice(0, 1).toUpperCase() + v.slice(1)) .join(" ");
上面的例子先用 split 把字符串轉換數組,然后再通過 map 把各元素的首字母轉換成大寫,最后通過 join 把數組轉換成字符串。 整個過程就是 join(map(split(str))),體現了函數式編程的核心思想: 通過函數對數據進行轉換。
由此我們可以得到,函數式編程有兩個基本特點:
通過函數來對數據進行轉換
通過串聯多個函數來求結果
二、對比聲明式與命令式命令式:我們通過編寫一條又一條指令去讓計算機執(zhí)行一些動作,這其中一般都會涉及到很多繁雜的細節(jié)。命令式代碼中頻繁使用語句,來完成某個行為。比如 for、if、switch、throw 等這些語句。
聲明式:我們通過寫表達式的方式來聲明我們想干什么,而不是通過一步一步的指示。表達式通常是某些函數調用的復合、一些值和操作符,用來計算出結果值。
//命令式 var CEOs = []; for(var i = 0; i < companies.length; i++){ CEOs.push(companies[i].CEO) } //聲明式 var CEOs = companies.map(c => c.CEO);
從上面的例子中,我們可以看到聲明式的寫法是一個表達式,無需關心如何進行計數器迭代,返回的數組如何收集,它指明的是做什么,而不是怎么做。函數式編程的一個明顯的好處就是這種聲明式的代碼,對于無副作用的純函數,我們完全可以不考慮函數內部是如何實現的,專注于編寫業(yè)務代碼。
三、常見特性 無副作用指調用函數時不會修改外部狀態(tài),即一個函數調用 n 次后依然返回同樣的結果。
var a = 1; // 含有副作用,它修改了外部變量 a // 多次調用結果不一樣 function test1() { a++ return a; } // 無副作用,沒有修改外部狀態(tài) // 多次調用結果一樣 function test2(a) { return a + 1; }透明引用
指一個函數只會用到傳遞給它的變量以及自己內部創(chuàng)建的變量,不會使用到其他變量。
var a = 1; var b = 2; // 函數內部使用的變量并不屬于它的作用域 function test1() { return a + b; } // 函數內部使用的變量是顯式傳遞進去的 function test2(a, b) { return a + b; }不可變變量
指的是一個變量一旦創(chuàng)建后,就不能再進行修改,任何修改都會生成一個新的變量。使用不可變變量最大的好處是線程安全。多個線程可以同時訪問同一個不可變變量,讓并行變得更容易實現。 由于 JavaScript 原生不支持不可變變量,需要通過第三方庫來實現。 (如 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 })函數是一等公民
我們常說函數是JavaScript的"第一等公民",指的是函數與其他數據類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。下文將要介紹的閉包、高階函數、函數柯里化和函數組合都是圍繞這一特性的應用
四、常見的函數式編程模型 1.閉包(Closure)如果一個函數引用了自由變量,那么該函數就是一個閉包。何謂自由變量?自由變量是指不屬于該函數作用域的變量(所有全局變量都是自由變量,嚴格來說引用了全局變量的函數都是閉包,但這種閉包并沒有什么用,通常情況下我們說的閉包是指函數內部的函數)。
閉包的形成條件:
存在內、外兩層函數
內層函數對外層函數的局部變量進行了引用
閉包的用途:
可以定義一些作用域局限的持久化變量,這些變量可以用來做緩存或者計算的中間量等。
// 簡單的緩存工具 // 匿名函數創(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
上面例子是一個簡單的緩存工具的實現,匿名函數創(chuàng)造了一個閉包,使得 store 對象 ,一直可以被引用,不會被回收。
閉包的弊端:持久化變量不會被正常釋放,持續(xù)占用內存空間,很容易造成內存浪費,所以一般需要一些額外手動的清理機制。
2.高階函數函數式編程傾向于復用一組通用的函數功能來處理數據,它通過使用高階函數來實現。高階函數指的是一個函數以函數為參數,或以函數為返回值,或者既以函數為參數又以函數為返回值。
高階函數經常用于:
抽象或隔離行為、作用,異步控制流程作為回調函數,promises,monads等
創(chuàng)建可以泛用于各種數據類型的功能
部分應用于函數參數(偏函數應用)或創(chuàng)建一個柯里化的函數,用于復用或函數復合。
接受一個函數列表并返回一些由這個列表中的函數組成的復合函數。
JavaScript 語言是原生支持高階函數的, 例如Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是JavaScript中內置的一些高階函數,使用高階函數會讓我們的代碼更清晰簡潔。
mapmap() 方法創(chuàng)建一個新數組,其結果是該數組中的每個元素都調用一個提供的函數后返回的結果。map 不會改變原數組。
假設我們有一個包含名稱和種類屬性的對象數組,我們想要這個數組中所有名稱屬性放在一個新數組中,如何實現呢?
// 不使用高階函數 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"]
// 使用高階函數 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)建一個新數組,其中包含所有通過回調函數測試的元素。filter 為數組中的每個元素調用一次 callback 函數, callback 函數返回 true 表示該元素通過測試,保留該元素,false 則不保留。filter 不會改變原數組,它返回過濾后的新數組。
假設我們有一個包含名稱和種類屬性的對象數組。 我們想要創(chuàng)建一個只包含狗(species: "dog")的數組。如何實現呢?
// 不使用高階函數 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);
// 使用高階函數 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 方法對調用數組的每個元素執(zhí)行回調函數,最后生成一個單一的值并返回。 reduce 方法接受兩個參數:1)reducer 函數(回調),2)一個可選的 initialValue。
假設我們要對一個數組的求和:
// 不使用高階函數 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
// 使用高階函數 const arr = [5, 7, 1, 8, 4]; const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0); console.log(sum)//25
我們可以通過下圖,形象生動展示三者的區(qū)別:
柯里化又稱部分求值,柯里化函數會接收一些參數,然后不會立即求值,而是繼續(xù)返回一個新函數,將傳入的參數通過閉包的形式保存,等到被真正求值的時候,再一次性把所有傳入的參數進行求值。
// 普通函數 function add(x,y){ return x + y; } add(1,2); // 3 // 函數柯里化 var add = function(x) { return function(y) { return x + y; }; }; var increment = add(1); increment(2);// 3
這里我們定義了一個 add 函數,它接受一個參數并返回一個新的函數。調用 add 之后,返回的函數就通過閉包的方式記住了 add 的第一個參數。那么,我們如何來實現一個簡易的柯里化函數呢?
function curryIt(fn) { // 參數fn函數的參數個數 var n = fn.length; var args = []; return function(arg) { args.push(arg); if (args.length < n) { return arguments.callee; // 返回這個函數的引用 } else { return fn.apply(this, args); } }; } function add(a, b, c) { return [a, b, c]; } var c = curryIt(add); var c1 = c(1); var c2 = c1(2); var c3 = c2(3); console.log(c3); //[1, 2, 3]
由此我們可以看出,柯里化是一種“預加載”函數的方法,通過傳遞較少的參數,得到一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的“緩存”,是一種非常高效的編寫函數的方法!
4.函數組合 (Composition)前面提到過,函數式編程的一個特點是通過串聯函數來求值。然而,隨著串聯函數數量的增多,代碼的可讀性就會不斷下降。函數組合就是用來解決這個問題的方法。
假設有一個 compose 函數,它可以接受多個函數作為參數,然后返回一個新的函數。當我們?yōu)檫@個新函數傳遞參數時,該參數就會「流」過其中的函數,最后返回結果。
//兩個函數的組合 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
給大家推薦一個好用的BUG監(jiān)控工具Fundebug,歡迎免費試用!
歡迎關注公眾號:前端工匠,你的成長我們一起見證!
珠峰架構課(強烈推薦)
MDN文檔
What is Functional Programming?
So You Want to be a Functional Programmer
理解 JavaScript 中的高階函數
我所了解的函數式編程
JS函數式編程指南
JavaScript函數式編程(一)
我眼中的 JavaScript 函數式編程
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://www.ezyhdfw.cn/yun/106575.html
摘要:本文的分享主要圍繞以下幾個方面能做什么常見應用場景介紹如何學習語法基礎實戰(zhàn)面向對象編程實戰(zhàn)練熟基礎小游戲項目的實現與實戰(zhàn)一能做什么一種編程語言往往可以應用于多方面,有些方面比較常用,有些方面極為常用。比如表示是一個空列表。 摘要:Python語言的教程雖然隨處可見,但是忙于日常業(yè)務/學習的你或許:一直想要找個時間學一點,但是又不知道該從何下手?本文將從Python能做什么,如何學習Py...
摘要:,,面向切面編程。,切點,切面匹配連接點的點,一般與切點表達式相關,就是切面如何切點。例子中,注解就是切點表達式,匹配對應的連接點,通知,指在切面的某個特定的連接點上執(zhí)行的動作。,織入,將作用在的過程。因為源碼都是英文寫的。 之前《零基礎帶你看Spring源碼——IOC控制反轉》詳細講了Spring容器的初始化和加載的原理,后面《你真的完全了解Java動態(tài)代理嗎?看這篇就夠了》介紹了下...
摘要:在創(chuàng)業(yè)初期,你招來的工程師必須是能夠獨當一面的大神隊友。要評估一個應聘者的真實水準,最佳方式就是結對編程。用微博的抓取消息并顯示在時間線上,就是個很好的考察應聘者的面試項目。不過結對編程再好使,也沒辦法讓你完全了解一個應聘者。 原文鏈接:10 Interview Questions Every JavaScript Developer Should Know 對大部分公司來說,招聘技...
摘要:而在的全年財報也表明,微信小程序日活躍賬戶數增長迅速,用戶人均日訪問量同比增長,而中長尾小程序也占到了小程序日均總訪問量的。在小游戲中,微信已有單月流水過億的杰出小游戲作品。據不完全統計,微信小程序已有余個入口。 showImg(https://segmentfault.com/img/remote/1460000018837984);小程序的平臺越來越多了,開發(fā)者的精力也越來越分散。...
摘要:美團的目標很明確,那就是把萬外賣小哥清理一大半,只留下三四線城市靠人工配送。現在的美團以他核心的三大業(yè)務板塊,乘著大疫情時代股價飆升的東風,最高峰時期可以跟騰訊阿里這樣的互聯網巨頭相比較。 ...
閱讀 2139·2023-04-26 01:59
閱讀 3427·2021-10-11 11:07
閱讀 3490·2021-09-22 15:43
閱讀 3535·2021-09-02 15:21
閱讀 2788·2021-09-01 10:49
閱讀 1035·2019-08-29 15:15
閱讀 3273·2019-08-29 13:59
閱讀 2964·2019-08-26 13:36