如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

原始碼終結者發表於2019-03-26

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

引言

函數語言程式設計的理論知識我已經 闡(胡)述(謅) 完了,沒看過的小夥伴,可以猛擊下面連線開啟穿越模式:

如何編寫高質量的函式 -- 打通任督二脈篇[理論卷]

下面我會從如何用 FP 編寫高質量的函式、分析原始碼裡面的技巧,以及實際工作中如何編寫,來展示如何打通你的任督二脈。

話不多說,下面就開始實戰吧。

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

如何用 FP 編寫高質量的函式

這裡我通過簡單的 demo 來說明一些技巧。技巧點如下:

注意函式中變數的型別和變數的作用域

如果是值型別 -- 組合函式/高階性

那你就要注意了,這可能是一個硬編碼,不夠靈活性,你可能需要進行處理了,如何處理呢?比如通過傳參來幹掉值型別的變數,下面舉一個簡單的例子。

程式碼如下:

document.querySelector('#msg').innerHTML = '<h1>Hello World'</h1>'
複製程式碼

我們來欣賞一下上面的程式碼,我來吐槽幾句:

第一:硬編碼味道很重,程式碼都是寫死的。

第二:擴充套件性很差,複用性很低,難道我要在其他地方進行 crtl c ctrl v 然後再手工改?

第三:如果我在 document.querySelector('#msg') 拿到物件後,不想 innerHTML ,我想做一些其他的事情,怎麼辦?

看了上面的三點,是不是感覺很 DTOK ,下面我就先向大家展示一下,如何完全重構這段程式碼。這裡我只寫 JS 部分:

程式碼如下:

// 使用到了組合函式,運用了函式的高階性等
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)

const documentWrite = document.write.bind(document)
const createNode = function(text) {
  return '<h1>' + text + '</h1>'
}
const setText = msg => msg

const printMessage = compose(
  documentWrite,
  createNode,
  setText
)

printMessage('hi~ godkun')
複製程式碼

效果如圖所示:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

完整程式碼我放在了下面兩個地址上,小夥伴可自行檢視。

codepen: codepen.io/godkun/pen/…

gist:gist.github.com/godkun/772c…

注意事項一:

compose 函式的執行順序是從右向左,也就是資料流是從右向左流,你可以把

const printMessage = compose(
  documentWrite,
  createNode,
  setText
)
複製程式碼

看成是下面這種形式:

documentWrite(createNode(setText(value)))
複製程式碼

注意事項二:

linux 世界裡,是遵循 pipe (管道) 的思想,也就是資料從左向右流,那怎麼把上面的程式碼變成 pipe 的形式呢?

很簡單,只需要把 const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value) 中的 reverse 幹掉就好了,寫成:

const compose = (...fns) => value => fns.reduce((acc, fn) => fn(acc), value)
複製程式碼

總結

是不是發現通過用函數語言程式設計進行重構後,這個程式碼變得非常的靈活,好處大致有如下:

  1. 大函式被拆成了一個個具有單一功能的小函式
  2. 硬編碼被幹掉了,變得更加靈活
  3. 使用了組合函式、高階函式來靈活的組合各個小函式
  4. 職責越單一,複用性會越好,這些小函式,我們都可以在其他地方,通過組合不同的小函式,來實現更多的功能。

上來我就寫了個 簡單 的開胃菜?

並不簡單,大家好好想一想,仔細體會一下。

思考題:這裡我甩貼一張小夥伴在群裡分享的圖:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

是不是感到頭皮發麻,這是我送個大家的禮物,大家可以嘗試把上面圖片的程式碼用函式式進行完全重構,加油。

如果是引用型別 -- 等冪性/引用透明性/資料不可變

下面輕鬆點,程式碼 demo 如下:

let arr = [1,3,2,4,5]
function fun(arr) {
  let result = arr.sort()
  console.log('result', result)
  console.log('arr', arr)
}
fun(arr)
複製程式碼

結果如下圖所示:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

看上面,你會發現陣列 arr 被修改了。由於 fun(arr) 函式中的引數 arr 是引用型別,如果函式體內對此引用所指的資料進行直接操作的話,就會有潛在的副作用,比如原陣列被修改了,這種情況下,改怎麼辦呢?

很簡單,在函式體內對 arr 這個引用型別進行建立副本。如下面程式碼:

let arr = [1,3,2,4,5]
function fun(arr) {
  let arrNew = arr.slice()
  let result = arrNew.sort()
  console.log('result', result)
  console.log('arr', arr)
}

fun(arr)
複製程式碼

通過 slice 來建立一個新的陣列,然後對新的陣列進行操作,這樣就達到了消除副作用的目的。這裡我只是舉一個例子,但是核心思想我已經闡述出來了,這裡已經體現了理論卷中的資料不可變的思想了。

如果函式體內引用變數的變化,會造成超出其作用域的影響,比如上面程式碼中對 arr 進行操作,影響到了陣列 arr 本身 。那這個時候,我們就需要思考一下,要不要採用不可變的思想,對引用型別進行處理。

注意有沒有明顯的指令式程式設計 -- 宣告式/抽象/封裝

注意函式裡面有沒有大量的 for 迴圈

為什麼說這個呢,因為這個很好判斷。如果有的話,就要思考一下需不需要對 for 迴圈進行處理,下文有對 for 迴圈的專門介紹。

注意函式裡面有沒有過多的 if/else

也是一樣的思想,過多的 if/else 也要根據情況去做相應的處理。

將程式碼本身進行引數化 -- 宣告式/抽象/封裝

標題的意識其實可以這樣理解,對函式進行高階化處理。當把函式當成引數的時候,也就是把程式碼本身當成引數了。

什麼情況下要考慮高階化呢。

當你優化到一定地步後,發現還是不夠複用性,這個時候就要考慮將引數進行函式化,這樣可以將引數變成可以提供更多功能的函式。

函式的高階化,往往在其他功能上得以體現,比如柯里化,組合。

將大函式變成可組合的小函式

通過上面例子的分析,我也向大家展示瞭如何將函式最小化。通過將大函式拆成多個具有單一職責的小函式,來提高複用性和靈活性。

函數語言程式設計的注意點

FP 不是萬能的,大家不要認為它很完美,它也有自己的缺點,下面我簡單的說兩點吧。

注意效能

進行 FP 時, 如果你使用的不恰當,是會造成效能問題的。比如你遞迴用的不恰當,比如你柯里化巢狀的過多。

注意可讀性

這裡我想說的是,在進行 FP 時,不要過度的抽象,過度的抽象會導致可讀性變差。

原始碼中的學習

看一下 Ramda.js 的原始碼

說到函數語言程式設計,那一定要看看 Ramda.js 的原始碼。ramda.js 的原始碼搞懂後,函數語言程式設計的思想也就基本沒什麼問題了。

關於 Ramda.js 可以看一下阮大的部落格:

Ramda 函式庫參考教程

看完了,那開始執行:

git clone git@github.com:ramda/ramda.git
複製程式碼

然後我們來分析原始碼,首先按照常規套路,看一下 source/index.js 檔案。

如圖所示:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

嗯好,我大概知道了,我們繼續分析。

看一下 add.js

import _curry2 from './internal/_curry2';
var add = _curry2(function add(a, b) {
  return Number(a) + Number(b);
});
export default add;
複製程式碼

看上面程式碼,我們發現,add 函式被包了一個 _curry2 函式。 下劃線代表這是一個內部方法,不暴露成 API 。這時,你再看其他函式,會發現都被包了一個 _curry1/2/3/N 函式。

如下圖所示:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

從程式碼中,我們可以知道,1/2/3/N 代表掉引數個數為 1/2/3/N 的函式的柯里化,而且會發現,所有的 ramda 函式都是經過柯里化的。

我們思考一個問題,為什麼 ramda.js 要對函式全部柯里化?

我們看一下普通的函式 f(a, b, c) 。如果只在呼叫的時候,傳遞 a 。會發現,JS 在執行呼叫時,會將 bc 設定為 undefined

從上面我們可以知道,JS 語言不能原生支援柯里化。非柯里化函式會導致缺少引數的實參變成 undefined 。繼續想會發現,ramda.js 對函式全部柯里化的目的,就是為了優化上面的場景。

下面,我們看一下 _curry2 程式碼,這裡為了可讀性,我對程式碼進行了改造,我把 _isPlaceholder 去掉了,假設沒有佔位符,同時把 _curry1 放在函式內,並且對過程進行了相應註釋。

二元引數的柯里化,程式碼如下:

function _curry2(fn) {
  return function f2(a, b) {
    switch (arguments.length) {
      case 0:
        return f2;
      case 1:
        return _curry1(function (_b) {
          // 將引數從右到左依次賦值 1 2
          // 第一次執行時,是 fn(a, 1)
          return fn(a, _b);
        });
      default:
        // 引數長度是 2 時 直接進行計算
        return fn(a, b);
    }
  };
}

function _curry1(fn) {
  return function f1(a) {
    // 對引數長度進行判斷
    if (arguments.length === 0) {
      return f1;
    } else {
      // 通過 apply 來返回函式 fn(a, 1)
      return fn.apply(this, arguments);
    }
  };
}

const add = _curry2(function add(a, b) {
  return Number(a) + Number(b);
});

// 第一次呼叫是 fn(a, 1)
let r1  = add(1)
// 第二次呼叫是 fn(2,1)
let r2 = r1(2)
console.log('sss', r2)
複製程式碼

完整程式碼地址如下:

gist:gist.github.com/godkun/0d22…

codeopen:codepen.io/godkun/pen/…

上面的程式碼在關鍵處已經做了註釋,這裡我就不過多解釋細節了,小夥伴自行領悟。

柯里化的好處

看了上面對 ramda.js 原始碼中柯里化的分析,是不是有點收穫,就像上面說的,柯里化的目的是為了優化在 JS 原生下的一些函式場景。好處如下:

第一:從上面 add 函式可以知道,通過柯里化,可以讓函式在真正需要計算的時候進行計算,起到了延遲的作用,也可以說體現了惰性思想。

第二:通過對引數的處理,做到複用性,從上面的 add 函式可以知道,柯里化把多元函式變成了一元函式,通過多次呼叫,來實現需要的功能,這樣的話,我們就可以控制每一個引數,比如提前設定好不變的引數,從而讓程式碼更加靈活和簡潔。

PS: 柯里化命名的由來

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

關於 ramda 中的 composepipe -- 組合函式/管道函式

本文一開始,我就以一個例子向大家展示了組合函式 composepipe 的用法。

關於 ramda 中,composepipe 的實現我這裡就不再分析了,小夥伴自己看著原始碼分析一下。這裡我就簡潔說一下組合函式的一些個人看法。

個人對組合(管道也是組合)函式的看法

在我看來,組合是函數語言程式設計的核心,FP 的思想是要函式儘可能的小,儘可能的保證職責單一。這就直接確定了組合函式在 FP 中的地位,玩好了組合函式,FP 也就基本上路了。

和前端的元件進行對比來深刻的理解組合函式

函式的組合思想是程式導向的一種封裝,而前端的元件思想是面對物件的一種封裝。

實際工作中的實踐

寫一個整合錯誤,警告,以及除錯資訊的 tap 函式

故事的背景

實際工作中,你肯定會遇到下面這種接收和處理資料的場景。

程式碼如下:

// 虛擬碼
res => {
  // name 是字串,age 是數字
  if (res.data && res.data.name && res.data.age) {
    // TODO:
  }
}
複製程式碼

上面這樣寫,看起來好像也沒什麼問題,但是經不起分析。比如 name 是數字,age 返回的不是數字。這樣的話, if 中的判斷是能通過的,但是實際結果並不是你想要的。

那該怎麼辦呢?問題不大,跟著我一步步的優化就 OK 了。

進行第一次優化

res => {
  if (res.data && typeof res.data.name === 'string' && typeof res.data.age === 'number') {
    // TODO:
  }
}
複製程式碼

看起來是夠魯棒了,但是這段程式碼過於命令式,無法複用到其他地方,在其他的場景中,還要重寫一遍這些程式碼,很煩。

進行第二次優化

// is 是一個物件函式  虛擬碼
res => {
  if (is.object(res.data) && is.string(res.data.name) && is.number(res.data.age)) {
    // TODO:
  }
}
複製程式碼

可能有人要問,這是函數語言程式設計麼。現在我告訴你,這是 FP ,將過程抽象掉的行為也是一種函式式思想。上面程式碼,提高了複用性,將判斷的過程抽象成了 is 的物件函式中,這樣在其他地方都可以複用這個 is

但是,程式碼還是有問題,一般來說,各個介面的返回資料都是 res.data 這種型別的。所以如果按照上面的程式碼,我們會發現,每次都要寫 is.object(res.data) 這是不能容忍的一件事。我們能不能做到不寫這個判斷呢?

當然可以,你完全可以在 is 裡面加一層對 data 的判斷,當然這個需要你把 data 作為引數 傳給 is

第三次優化

// is 是一個物件函式  虛擬碼
res => {
  if (is.string(res.data, data.name) && is.number(res.data, data.age)) {
    // TODO:
  }
}
複製程式碼

按照上面的寫法,is 系列函式會對第一個引數進行 object 型別判斷,會再次提高複用性。

好像已經很不錯了,但其實還遠遠不夠。

總結上面三次優化

為什麼還遠遠不夠

第一:有 if 語句存在,可能會有人說,if 語句存在有什麼的啊。現在我來告訴你,這塊有 if 為什麼不好。是因為 if 語句的 () 裡面,最終的值都會表現成布林值。所以這塊限制的很死,需要解決 if 語句的問題。

第二:is 函式功能單一,只能做到返回布林值,無法完成除錯列印錯誤處理等功能,如果你想列印和除錯,你又得在條件分支裡面各種 console.log ,然後這些程式碼依舊過於命令式,無法重用。其實,我們想一下,可以知道,這也是因為用了 if 語句造成的。

說完這些問題,那下面我們來解決吧。

進行函式式優化--第一階段

我們想一下,如果要做到高度抽象和複用的話,首先我們要把需要的功能羅列一下,大致如下:

第一個功能:檢查型別

第二個功能:除錯功能,可以自定義 console 的輸出形式

第三個功能:處理異常的功能(簡單版)

看到上面功能後,我們想一下函式式思想中有哪些武器可以被我們使用到。首先怎麼把不同的函式組合在一起。

PS:哈哈哈哈,你看你自己無意識間就說出了組合這個詞。

是的,你真聰明。現在,如何將小函式組合成一個完成特定功能的函式呢?想一下,你會發現,這裡需要用到函式的高階性,要將函式作為引數傳入多功能函式中。ok ,現在我們知道實現的大致方向了,下面我們來嘗試一下吧。

這裡我直接把我的實現過程貼出來了,有相應的註釋,程式碼如下:

/**
 * 多功能函式
 * @param {Mixed} value 傳入的資料
 * @param {Function} predicate  謂詞,用來進行斷言
 * @param {Mixed} tip  預設值是 value
 */
function tap(value, predicate, tip = value) {
  if(predicate(value)) {
    log('log', `{type: ${typeof value}, value: ${value} }`, `額外資訊:${tip}`)
  }
}

const is = {
  undef       : v => v === null || v === undefined,
  notUndef    : v => v !== null && v !== undefined,
  noString    : f => typeof f !== 'string',
  noFunc      : f => typeof f !== 'function',
  noNumber    : n => typeof n !== 'number',
  noArray     : !Array.isArray,
};

function log(level, message, tip) {
  console[level].call(console, message, tip)
}

const res1 = {data: {age: '', name: 'godkun'}}
const res2 = {data: {age: 66, name: 'godkun'}}

// 函式的組合,函式的高階
tap(res1.data.age, is.noNumber)
tap(res2.data.age, is.noNumber)
複製程式碼

結果圖如下:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

會發現當,age 不是 Number 型別的時候,就會列印對應的提示資訊,當時 Number 型別的時候,就不會列印資訊。

這樣的話,我們在業務中,就可以直接寫:

res => {
  tap(res.data.age, is.noNumber)
  // TODO: 處理 age
}
複製程式碼

不用 if 語句,如果有異常,看一下列印資訊,會一目瞭然的。

當然這樣寫肯定不能放到生產上的,因為 tap 不會阻止後續操作,我這樣寫的原因是:這個 tap 函式主要是用來開發除錯的。

但是,如果需要保證不符合的資料需要直接在 tap 處終止,那可以在 tap 函式裡面加下 return false return true 。然後寫成下面程式碼的形式:

res => {
  // if 語句中的返回值是布林值
  if (tap(res.data.age, is.noNumber)) {
    // TODO: 處理 age
  }
}
複製程式碼

但是這樣寫,會有個不好的地方。那就是用到了 if 語句,用 if 語句也沒什麼不好的。但退一步看 tap 函式,你會發現,還是不夠複用,函式內,還存在硬編碼的行為。

如下圖所示:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

存在兩點問題:

第一點:把 console 的行為固定死了,導致不能設定 console.error() 等行為

第二點:不能丟擲異常,就算型別不匹配,也阻止不了後續步驟的執行

怎麼解決呢?

進行函式式優化--第二階段

簡單分析一下,這裡我們一般希望,先採用惰性的思想,讓一個函式確定好幾個引數,然後,我們再讓這個函式去呼叫其他不固定的引數。這樣做的好處是減少了相同引數的多次 coding ,因為相同的引數已經內建了,不用我再去傳了。

分析到這,你應該有所感悟。你會發現,這樣的行為其實就是柯里化,通過將多元函式變成可以一元函式。同時,通過柯里化,可以靈活設定好初始化需要提前確定的引數,大大提高了函式的複用性和靈活性。

對於柯里化,由於原始碼分析篇,我已經分析了 ramda 的柯里化實現原理,這裡我為了節省程式碼,就直接使用 ramda 了。

程式碼如下:

const R = require('ramda')
// 其實這裡你可以站在一個高層去把它們想象成函式的過載
// 通過傳參的不同來實現不同的功能
const tapThrow = R.curry(_tap)('throw', 'log')
const tapLog = R.curry(_tap)(null, 'log')

function _tap(stop, level, value, predicate, error=value) {
  if(predicate(value)) {
    if (stop === 'throw') {
      log(`${level}`, 'uncaught at check', error)
      throw new Error(error)
    }
    log(`${level}`, `{type: ${typeof value}, value: ${value} }`, `額外資訊:${error}`)
  }
}

const is = {
  undef       : v => v === null || v === undefined,
  notUndef    : v => v !== null && v !== undefined,
  noString    : f => typeof f !== 'string',
  noFunc      : f => typeof f !== 'function',
  noNumber    : n => typeof n !== 'number',
  noArray     : !Array.isArray,
};

function log(level, message, error) {
  console[level].call(console, message, error)
}

const res = {data: {age: '66', name: 'godkun'}}

function main() {
  // 不開啟異常忽略,使用 console.log 的 tapLog 函式
  // tapLog(res.data.age, is.noNumber)
  
  // 開啟異常忽略,使用 console.log 的 tapThrow 函式
  tapThrow(res.data.age, is.noNumber)
  console.log('能不能走到這')
}

main()
複製程式碼

程式碼地址如下:

gist: gist.github.com/godkun/d394…

關鍵註釋,我已經在程式碼中標註了。上面程式碼在第一次進行函式式優化的時候,在組合和高階的基礎上,加入了柯里化,從而讓函式變得更有複用性。

PS: 具有柯里化的函式,在我看來,也是體現了函式的過載性。

執行結果如下圖所示:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

會發現使用 tapThrow 函式時,當型別不匹配的時候,會阻止後續步驟的執行。

這次實踐的總結:

我通過多次優化,向大家展示了,如何一步步的去優化一個函式。從開始的命令式優化,到後面的函式式優化,從開始的普通函式,到後面的逐步使用了高階、組合、柯里的特性。從開始的有 if/else 語句到後面的逐步幹掉它,來獲得更高的複用性。通過這個實戰,小夥伴可以知道,如何循序漸進的使用函數語言程式設計,讓程式碼變得更加優秀。

思考題:上面的程式碼還可以繼續優化,這裡就不再繼續分析了,有興趣的小夥伴可以自行分享,也可以和我私聊交流。在鄙人看來,前期先運用高階、組合、柯里化做到這樣,就已經很不錯了。

為什麼要幹掉 for 迴圈

之前就有各種幹掉 for 迴圈的文章。各種討論,這裡我按照我的看法來解釋一下,為什麼會存在幹掉 for 迴圈這一說。

程式碼如下:

let arr = [1,2,3,4]
for (let i = 0; i < arr.length; i++) {
  // TODO: ...
}
複製程式碼

我們看上面這段程式碼,我來問一個問題:

上面這段程式碼如何複用到其他的函式中?

稍微想一下,大家肯定可以很快的想出來,那就是封裝成函式,然後在其他函式中進行呼叫。

大家為什麼會這樣想呢?

是因為 for 迴圈是一種命令控制結構,你發現它很難被插入到其他操作中,也發現了 for 迴圈很難被複用的現實。

其實,當你說出這個答案的時候。這個關於為什麼要幹掉 for 迴圈的討論就已經結束了。

因為你在封裝 for 迴圈時,就是在抽象 for 迴圈,就是在把 for 迴圈給隱藏掉,就是在把過程給隱藏掉,就是在告訴使用者,你只需要調我封裝的函式,而不需要關心內部實現。

於是乎,JS 就誕生了諸如 map filter reduce 等這種將迴圈過程隱藏掉的函式。底層本質上還是用 for 實現的,只不過是把 for 迴圈隱藏了,如果按照業界內的說話逼格,就是把 for 迴圈幹掉了。這就是宣告式程式設計在前端中的應用之一。所以,其實大家每天都在寫函數語言程式設計,只不過你意識不到而已。

你是如何處理陣列變換的

三種方式:

第一種:傳統的迴圈結構 - 比如 for 迴圈

第二種:鏈式

第三種:函式式組合

這裡我就不具體舉例子了,很簡單,前面的例子基本都涵蓋了,小夥伴們自行實踐一下。

如何利用函式的純潔性來進行快取

為什麼在編寫函式時,要考慮快取?

一句話,避免計算重複值。計算就意味著消耗各種資源,而做重複的計算,就是在浪費各種資源。

純潔性和快取有什麼關係?

我們想一下可以知道,純函式總是為給定的輸入返回相同的輸出,那既然如此,我們當然要想到可以快取函式的輸出。

那如何做函式的快取呢?

記住一句話:給計算結果賦予唯一的鍵值並持久化到快取中。

大致 demo 程式碼:

function mian(key) {
  let cache = {}
  cache.hasOwnProperty(key) ?
    main(key) :
    cache[key] = main(key)
}
複製程式碼

上面程式碼是一種最簡單的利用純函式來做快取的例子。下面我們來實現一個非常完美的快取函式。

給原生 JS 函式加上自動記憶化的快取機制

程式碼如下:

Function.prototype.memorized = () => {
  let key = JSON.stringify(arguments)
  
  // 快取實現
  this._cache = this._cache || {}
  this._cache[key] = this._cache[key] || this.apply(this, arguments)
  return this._cache[key]
  
}

Function.prototype.memorize = () => {
  let fn = this
  // 只記憶一元函式
  if (fn.length === 0 || fn.length > 1) return fn
  return () => fn.memorized.apply(fn, arguments)
}
複製程式碼

程式碼地址如下:

gist: gist.github.com/godkun/5251…

通過擴充套件 Function 物件,我們就可以充分利用函式的記憶化來實現函式的快取。

上面函式快取實現的好處有以下兩點:

第一:消除了可能存在的全域性共享的快取

第二:將快取機制抽象到了函式的內部,使其完全與測試無關,只需要關係函式的行為即可

備註

  • 實戰部分,我沒有提到函子知識,不代表我沒有實踐過,正是因為我實踐過,才決定不提它,因為對於前端來說,有時候你要顧及整個團隊的技術,組合和柯里還有高階函式等還是可以很好的運用到實踐中的,函子暫時就算了吧,以後有機會我會單獨寫一篇關於函式式高階玩法的文章。
  • 小夥伴們看實戰篇的時候,一定要結合理論篇一起看,這樣才能無縫連線。
  • 最後說一句,實戰篇也是寫的頭皮發麻,俺不容易啊,點個贊支援一下俺吧。

參考

參考連結

參考書籍

  • JavaScript ES6 函數語言程式設計入門經典
  • JavaScript 函數語言程式設計指南
  • Haskell 趣學指南
  • 其他電子書

交流

如何編寫高質量函式系列文章如下(不包含本篇):

這個系列還在持續更新中,歡迎關注,下一篇是關於設計模式的。

可以關注我的掘金部落格或者 github 來獲取後續的系列文章更新通知。掘金系列技術文章彙總如下,覺得不錯的話,點個 star 鼓勵一下。

github.com/godkun/blog

我是原始碼終結者,歡迎技術交流。

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

也可以進 前端狂想錄群 大家一起頭腦風暴。有想加的,因為人滿了,可以先加我好友,我來邀請你進群。

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

風之語

最後:尊重原創,轉載請註明出處哈?

相關文章