函數(shù)式編程

函數(shù)編程是已以函數(shù)作為主要載體的編程方式,用函數(shù)去拆解、抽象一般的表達(dá)式。

與命令式編程相比有哪些好處?

  • 語(yǔ)義清晰
  • 復(fù)用性高
  • 可維護(hù)性好
  • 作用局局限,副作用少

基本的函數(shù)式編程

1、數(shù)組中的每個(gè)字母的首字母大寫

一般寫法

  var list = ['apple', 'pen', 'style'];
  for(const i in list){
    const c = list[i][0];
    console.log(c.toLocaleLowerCase())
    list[i] = c.toUpperCase() + list[i].slice(1);
  }
  console.log(list)

函數(shù)式寫法一

  var list = ['apple', 'pen', 'style'];
  function upperFirst(word){
    return word[0].toUpperCase() + word.slice(1);
  }

  function wordToUpperCase(list){
    return list.map(upperFirst);
  }

  console.log(wordToUpperCase(list));

函數(shù)式寫法二

console.log(['apple', 'pen', 'style'].map(word => word[0].toUpperCase() + word.slice(1)));
當(dāng)情況變得復(fù)雜的時(shí)候,表達(dá)式寫法會(huì)遇到幾個(gè)問(wèn)題。
  1. 表意不明顯,逐漸難以維護(hù)。
  2. 復(fù)用性差,產(chǎn)生更多的代碼量。
  3. 會(huì)產(chǎn)生很多的中間變量。

函數(shù)式編程很好的解決以上的問(wèn)題,如函數(shù)式寫法一,它利用了函數(shù)封裝性將功能做拆解,并封裝為不同的函數(shù),再利用組合的調(diào)用達(dá)到目的。這樣做使表意更清晰,易于維護(hù)、復(fù)用以及擴(kuò)展。其次利用高階函數(shù),Array.map 代替 for ... of做數(shù)組遍歷,減少了中間變量和操作。

而函數(shù)式寫法一和函數(shù)式寫法二的區(qū)別在于,可以考慮后續(xù)函數(shù)復(fù)用的可能,如果沒(méi)有,則后者更優(yōu)。

鏈?zhǔn)絻?yōu)化

從上面的函數(shù)式寫法二中可以看出,函數(shù)式代碼在寫的過(guò)程中,很容易造成橫向延展,即產(chǎn)生多層嵌套。

計(jì)算數(shù)字之和

//一般寫法
console.log(1 + 2 + 3 - 4);
//函數(shù)式寫法
function sum(a, b) {
  return a + b;
}

function sub(a, b) {
  return a - b;
}

console.log(sub(sum(sum(1, 2), 3), 4);

隨著函數(shù)的嵌套層數(shù)不斷增多,導(dǎo)致代碼的可讀性下降,還容易產(chǎn)生錯(cuò)誤。

在這種情況下,我們可以考慮鏈?zhǔn)絻?yōu)化。

const utils = {
  chain(a) {
    this._temp = a;
    return this;
  },
  sum(b) {
    this._temp += b;
    return this;
  },
  sub(b) {
    this._temp -= b;
    return this;
  },
  value() {
    return this._temp;
  }
};

console.log(utils.chain(1).sum(2).sum(3).sub(4).value());

這樣改寫之后,結(jié)構(gòu)整體變得比較清晰。函數(shù)的嵌套和鏈?zhǔn)綄?duì)比還有一個(gè)很好的例子,就是回調(diào)函數(shù)和Promise模式。

順序請(qǐng)求兩個(gè)接口

//回調(diào)函數(shù)
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 是一個(gè)輕量級(jí)請(qǐng)求工具,支持 fetch,jsonp,ajax,無(wú)依賴
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());

隨著回調(diào)函數(shù)嵌套層級(jí)和單層復(fù)雜數(shù)增加,它會(huì)變得臃腫且難以維護(hù),而 Promise 的鏈?zhǔn)浇Y(jié)構(gòu),在高度復(fù)雜時(shí),仍能縱向擴(kuò)展,而且層次清晰。

常見(jiàn)的函數(shù)式編程模型

閉包

閉包是由函數(shù)以及創(chuàng)建該函數(shù)的詞法環(huán)境組合而成。
可以保留局部變量不被釋放的代碼塊,稱為閉包。

//創(chuàng)建一個(gè)閉包
function makeCounter() {
  let k = 0;

  return function() {
    return ++k;
  };
}

const counter = makeCounter();

console.log(counter());  // 1
console.log(counter());  // 2

makeCounter 這個(gè)函數(shù)的代碼塊,在返回的函數(shù)中,對(duì)局部變量 k ,進(jìn)行了引用,導(dǎo)致局部變量無(wú)法在函數(shù)執(zhí)行結(jié)束后,被系統(tǒng)回收掉,從而產(chǎn)生了閉包。而這個(gè)閉包的作用就是,“保留住“ 了局部變量,使內(nèi)層函數(shù)調(diào)用時(shí),可以重復(fù)使用該變量;而不同于全局變量,該變量只能在函數(shù)內(nèi)部被引用。

換句話說(shuō),閉包其實(shí)就是創(chuàng)造出了一些函數(shù)私有的 ”持久化變量“。

所以從這個(gè)例子,我們可以總結(jié)出,閉包的創(chuàng)造條件是:

1、存在內(nèi)、外兩層函數(shù)
2、內(nèi)層函數(shù)對(duì)外層函數(shù)的局部變量進(jìn)行了引用

閉包的用途

閉包的主要用途就是可以定義一些作用域局限的持久化變量,這些變量可以用來(lái)做緩存或者中間量等等。

簡(jiǎn)單的緩存工具

//匿名函數(shù)創(chuàng)建一個(gè)閉包
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

上面的例子是一個(gè)簡(jiǎn)單的緩存工具的實(shí)現(xiàn),匿名函數(shù)創(chuàng)建了一個(gè)閉包,使得 store 對(duì)象,一直可以被引用,不會(huì)被回收。

閉包的弊端

持久化的變量不會(huì)被正常釋放,持續(xù)占有內(nèi)存空間,很容易造成內(nèi)存浪費(fèi),所以一般需要一些額外手動(dòng)的清除機(jī)制。

高階函數(shù)

接受或者返回一個(gè)函數(shù)的函數(shù)稱為高階函數(shù)。

JavaScript 預(yù)言師原生支持高階函數(shù)的,因?yàn)?JavaScript 的函數(shù)是一等公民,它既可以作為參數(shù)又可以作為另一個(gè)函數(shù)的返回值使用。

我們經(jīng)常在JavaScript中見(jiàn)到雨多原生的高階函數(shù),例如 Array.map, Array.reduce , Array.filter。

下面以 map 為例,看看它是如何使用的。

map(映射)

映射是對(duì)集合而言的,即把集合的每一項(xiàng)都做相同的變換,產(chǎn)生一個(gè)新的集合。

map 作為一個(gè)高階函數(shù),它接受一個(gè)函數(shù)的參數(shù)作為映射的邏輯。

數(shù)組中的每一項(xiàng)加一,組成一個(gè)新數(shù)組。
//一般寫法
const arr = [4, 5, 6, 7];
const rs = [];
for(const n of arr){
  rs.push(n + 1);
}
console.log(rs)

// map改寫
const arr = [1, 2, 3, 4];
const rs = arr.map(n => ++n);
console.log(rs)

上面的一般寫法,利用 for ...of 循環(huán)的方式遍歷數(shù)組會(huì)產(chǎn)生額外的操作,而且有改變?cè)瓟?shù)組的風(fēng)險(xiǎn),而 map 函數(shù)封裝了必要的操作,使我們僅需要關(guān)心映射的邏輯的函數(shù)實(shí)現(xiàn)即可,減少了代碼量,也降低了副作用產(chǎn)生的風(fēng)險(xiǎn)。

柯里化

給定一個(gè)函數(shù)的部分參數(shù),生成一個(gè)接受其他參數(shù)的新函數(shù)。

可能不常聽(tīng)到這個(gè)名詞,但是用過(guò) undescore 或 lodash 的人都見(jiàn)過(guò)他。

有一個(gè)神奇的 _.partial 函數(shù),它就是柯里化的實(shí)現(xiàn)

// 獲取目標(biāo)文件對(duì)基礎(chǔ)路徑的相對(duì)路徑


// 一般寫法
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');

通過(guò) _.partial ,我們得到了新的函數(shù) relativeFromBase ,這個(gè)函數(shù)在調(diào)用時(shí)就相當(dāng)于調(diào)用 path.relative ,并默認(rèn)將第一個(gè)參數(shù)傳入 BASE ,后續(xù)傳入的參數(shù)順序后置。

本例中,我們真正想完成的操作是每次獲得相對(duì)于 BASE 的路徑,而非相對(duì)于任何路徑??吕锘梢允刮覀冎魂P(guān)心函數(shù)的部分參數(shù),使函數(shù)的用途更加清晰,調(diào)用更加簡(jiǎn)單。

組合(Composing)

將多個(gè)函數(shù)的能力合并,創(chuàng)造一個(gè)新的函數(shù)。
同樣你第一次見(jiàn)到他可能還是在 lodash 中,compose 方法(現(xiàn)在叫 flow)

// 數(shù)組中每個(gè)單詞大寫,做 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 將轉(zhuǎn)大寫和轉(zhuǎn) Base64 的函數(shù)的能力合并,生成一個(gè)新的函數(shù)。方便作為參數(shù)函數(shù)或后續(xù)復(fù)用。

函數(shù)式編程的特點(diǎn)

1. 函數(shù)式一等公民

指的是函數(shù)和其他的數(shù)據(jù)類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數(shù),傳入另一個(gè)函數(shù),或者作為其他函數(shù)的返回值。

2. 只用表達(dá)式,不用語(yǔ)句

表達(dá)式是一個(gè)單純的運(yùn)算過(guò)程,總有返回值;語(yǔ)句是執(zhí)行某種操作,沒(méi)有返回值。函數(shù)式編程的需求,只使用表達(dá)式,不使用語(yǔ)句,也就是說(shuō)每一步都是單純的運(yùn)算,而且都有返回值。

原因是函數(shù)式編程的開(kāi)發(fā)動(dòng)機(jī),一開(kāi)始就是為了處理運(yùn)算(computation),不考慮系統(tǒng)的讀寫(I/O)。"語(yǔ)句"屬于對(duì)系統(tǒng)的讀寫操作,所以就被排斥在外。

當(dāng)然,實(shí)際應(yīng)用中,不做I/O是不可能的。因此,編程過(guò)程中,函數(shù)式 

編程只要求把I/O限制到最小,不要有不必要的讀寫行為,保持計(jì)算過(guò)程的單純性。

3. 沒(méi)有"副作用"

所謂"副作用",指的是函數(shù)內(nèi)部與外部互動(dòng)(最典型的情況,就是修改全局變量的值),產(chǎn)生運(yùn)算以外的其他結(jié)果。

函數(shù)式編程強(qiáng)調(diào)沒(méi)有"副作用",意味著函數(shù)要保持獨(dú)立,所有功能就是返回一個(gè)新的值,沒(méi)有其他行為,尤其是不得修改外部變量的值。

4. 不修改狀態(tài)

上一點(diǎn)已經(jīng)提到,函數(shù)式編程只是返回新的值,不修改系統(tǒng)變量。因此,不修改變量,也是它的一個(gè)重要特點(diǎn)。

在其他類型的語(yǔ)言中,變量往往用來(lái)保存"狀態(tài)"(state)。不修改變量,意味著狀態(tài)不能保存在變量中。函數(shù)式編程使用參數(shù)保存狀態(tài),最好的例子就是遞歸。下面的代碼是一個(gè)將字符串逆序排列的函數(shù),它演示了不同的參數(shù)如何決定了運(yùn)算所處的"狀態(tài)"。

function reverse(string) {
 if(string.length == 0) {
   return string;
 } else {
  return reverse(string.substring(1, string.length)) + string.substring(0, 1);
 }
}

5. 引用透明

引用透明(Referential transparency),指的是函數(shù)的運(yùn)行不依賴于外部變量或"狀態(tài)",只依賴于輸入的參數(shù),任何時(shí)候只要參數(shù)相同,引用函數(shù)所得到的返回值總是相同的。

原文鏈接
我眼中的 JavaScript 函數(shù)式編程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容