真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

如何編寫高質(zhì)量的JS函數(shù)(4)--函數(shù)式編程[實(shí)戰(zhàn)篇]

本文首發(fā)于 vivo互聯(lián)網(wǎng)技術(shù) 微信公眾號(hào)?
鏈接:https://mp.weixin.qq.com/s/ZoXYbjuezOWgNyJKmSQmTw
作者:楊昆

襄城網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、響應(yīng)式網(wǎng)站等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)于2013年開(kāi)始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)。

?【編寫高質(zhì)量函數(shù)系列】,往期精彩內(nèi)容:

《如何編寫高質(zhì)量的 JS 函數(shù)(1) -- 敲山震虎篇》介紹了函數(shù)的執(zhí)行機(jī)制,此篇將會(huì)從函數(shù)的命名、注釋和魯棒性方面,闡述如何通過(guò) JavaScript 編寫高質(zhì)量的函數(shù)。

?《如何編寫高質(zhì)量的 JS 函數(shù)(2)-- 命名/注釋/魯棒篇》從函數(shù)的命名、注釋和魯棒性方面,闡述如何通過(guò) JavaScript編寫高質(zhì)量的函數(shù)。

《如何 編寫高質(zhì)量的 JS 函數(shù)(3)-- 函數(shù)式編程[理論篇]》通過(guò)背景加提問(wèn)的方式,對(duì)函數(shù)式編程的本質(zhì)、目的、來(lái)龍去脈等方面進(jìn)行一次清晰的闡述。

本文會(huì)從如何用函數(shù)式編程思想編寫高質(zhì)量的函數(shù)、分析源碼里面的技巧,以及實(shí)際工作中如何編寫,來(lái)展示如何打通你的任督二脈。話不多說(shuō),下面就開(kāi)始實(shí)戰(zhàn)吧。

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

一、如何用函數(shù)式編程思想編寫高質(zhì)量的函數(shù)

這里我通過(guò)簡(jiǎn)單的 demo 來(lái)說(shuō)明一些技巧。技巧點(diǎn)如下:

1、注意函數(shù)中變量的類型和變量的作用域

(1)如果是值類型 -- 組合函數(shù)/高階性

這可能是一個(gè)硬編碼,不夠靈活性,你可能需要進(jìn)行處理了,如何處理呢?比如通過(guò)傳參來(lái)干掉值類型的變量,下面舉一個(gè)簡(jiǎn)單的例子。

代碼如下:

document.querySelector('#msg').innerHTML = '

Hello World'

'

我們來(lái)欣賞一下上面的代碼:

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

第二:擴(kuò)展性很差,復(fù)用性很低,難道我要在其他地方進(jìn)行 crtl c ctrl v 然后再手工改?

第三:如果在 document.querySelector('#msg')獲取對(duì)象后,不想 innerHTML ,我想做一些其他的事情,怎么辦?

OK ,下面我就先向大家展示一下,如何完全重構(gòu)這段代碼。這里我只寫 JS 部分:

代碼如下:// 使用到了組合函數(shù),運(yùn)用了函數(shù)的高階性等

const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)

const documentWrite = document.write.bind(document)
const createNode = function(text) {
  return '

' + text + '

' } const setText = msg => msg const printMessage = compose( documentWrite, createNode, setText ) printMessage('hi~ godkun')

效果如圖所示:

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

完整代碼我放在了下面兩個(gè)地址上,小伙伴可自行查看。

codepen:?codepen.io/godkun/pen/…

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

  • 注意事項(xiàng)一:

compose?函數(shù)的執(zhí)行順序是從右向左,也就是數(shù)據(jù)流是從右向左流,可以把

const printMessage = compose(
  documentWrite,
  createNode,
  setText
)

看成是下面這種形式:

documentWrite(createNode(setText(value)))
  • 注意事項(xiàng)二:

在 linux 世界里,是遵循 pipe (管道) 的思想,也就是數(shù)據(jù)從左向右流,那怎么把上面的代碼變成 pipe 的形式呢?

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

const compose = (...fns) => value => fns.reduce((acc, fn) => fn(acc), value)
  • 總結(jié)

是不是發(fā)現(xiàn)通過(guò)用函數(shù)式編程進(jìn)行重構(gòu)后,這個(gè)代碼變得非常的靈活,好處大致有如下:

  1. 函數(shù)被拆成了一個(gè)個(gè)具有單一功能的小函數(shù)

  2. 硬編碼被干掉了,變得更加靈活

  3. 使用了組合函數(shù)、高階函數(shù)來(lái)靈活的組合各個(gè)小函數(shù)

  4. 職責(zé)越單一,復(fù)用性會(huì)越好,這些小函數(shù),我們都可以在其他地方,通過(guò)組合不同的小函數(shù),來(lái)實(shí)現(xiàn)更多的功能。

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

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

這是我送個(gè)大家的禮物,大家可以嘗試把上面圖片的代碼用函數(shù)式進(jìn)行完全重構(gòu),加油。

(2)如果是引用類型 -- 等冪性/引用透明性/數(shù)據(jù)不可變

代碼 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)

結(jié)果如下圖所示:

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

看上面,你會(huì)發(fā)現(xiàn)數(shù)組 arr 被修改了。由于 fun(arr)?函數(shù)中的參數(shù) arr 是引用類型,如果函數(shù)體內(nèi)對(duì)此引用所指的數(shù)據(jù)進(jìn)行直接操作的話,就會(huì)有潛在的副作用,比如原數(shù)組被修改了,這種情況下,該怎么辦呢?

很簡(jiǎn)單,在函數(shù)體內(nèi)對(duì) arr 這個(gè)引用類型進(jìn)行創(chuàng)建副本。如下面代碼:

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)

通過(guò) slice 來(lái)創(chuàng)建一個(gè)新的數(shù)組,然后對(duì)新的數(shù)組進(jìn)行操作,這樣就達(dá)到了消除副作用的目的。這里只是舉一個(gè)例子,但是核心思想已經(jīng)闡述出來(lái)了,體現(xiàn)了理論卷中的數(shù)據(jù)不可變的思想了。

如果函數(shù)體內(nèi)引用變量的變化,會(huì)造成超出其作用域的影響,比如上面代碼中對(duì) arr 進(jìn)行操作,影響到了數(shù)組 arr 本身 。這時(shí)就需要思考一下,要不要采用不可變的思想,對(duì)引用類型進(jìn)行處理。

(3)注意有沒(méi)有明顯的命令式編程 -- 聲明式/抽象/封裝

注意函數(shù)里面有沒(méi)有大量的?for?循環(huán)

為什么說(shuō)這個(gè)呢,因?yàn)檫@個(gè)很好判斷。如果有的話,就要思考一下需不需要對(duì) for 循環(huán)進(jìn)行處理,下文有對(duì) for 循環(huán)的專門介紹。

注意函數(shù)里面有沒(méi)有過(guò)多的?if/else

也是一樣的思想,過(guò)多的 if/else 也要根據(jù)情況去做相應(yīng)的處理。

(4)將代碼本身進(jìn)行參數(shù)化 -- 聲明式/抽象/封裝

標(biāo)題的意識(shí)其實(shí)可以這樣理解,對(duì)函數(shù)進(jìn)行高階化處理。當(dāng)把函數(shù)當(dāng)成參數(shù)的時(shí)候,也就是把代碼本身當(dāng)成參數(shù)了。

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

當(dāng)優(yōu)化到一定地步后,發(fā)現(xiàn)還是不夠復(fù)用性,這時(shí)就要考慮將參數(shù)進(jìn)行函數(shù)化,這樣將參數(shù)變成可以提供更多功能的函數(shù)。

函數(shù)的高階化,往往在其他功能上得以體現(xiàn),比如柯里化,組合。

(5)將大函數(shù)變成可組合的小函數(shù)

通過(guò)上面例子的分析,我也向大家展示了如何將函數(shù)最小化。通過(guò)將大函數(shù)拆成多個(gè)具有單一職責(zé)的小函數(shù),來(lái)提高復(fù)用性和靈活性。

2、函數(shù)式編程的注意點(diǎn)

函數(shù)式編程?不是萬(wàn)能的,大家不要認(rèn)為它很完美,它也有自己的缺點(diǎn),如下兩點(diǎn):

(1)注意性能

進(jìn)行?函數(shù)式編程?時(shí), 如果使用不恰當(dāng),會(huì)造成性能問(wèn)題。比如遞歸用的不恰當(dāng),比如柯里化嵌套的過(guò)多。

(2)注意可讀性

在進(jìn)行函數(shù)式編程時(shí),不要過(guò)度的抽象,過(guò)度的抽象會(huì)導(dǎo)致可讀性變差。

二、源碼中的學(xué)習(xí)

1、看一下 Ramda.js 的源碼

說(shuō)到函數(shù)式編程,那一定要看看 Ramda.js 的源碼。Ramda.js 的源碼搞懂后,函數(shù)式編程的思想也就基本沒(méi)什么問(wèn)題了。

關(guān)于 Ramda.js 可以看一下阮大的博客:

Ramda 函數(shù)庫(kù)參考教程

看完了,那開(kāi)始執(zhí)行:

git clone git@github.com:ramda/ramda.git

然后我們來(lái)分析源碼,首先按照常規(guī)套路,看一下 source/index.js 文件。

如圖所示:

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

繼續(xù)分析,看一下 add.js。

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

看上面代碼,我們發(fā)現(xiàn),add 函數(shù)被包了一個(gè)?_curry2 函數(shù)。下劃線代表這是一個(gè)內(nèi)部方法,不暴露成 API 。這時(shí),再看其他函數(shù),會(huì)發(fā)現(xiàn)都被包了一個(gè)?_curry1/2/3/N 函數(shù)。

如下圖所示:

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

從代碼中可以知道,1/2/3/N 代表掉參數(shù)個(gè)數(shù)為 1/2/3/N 的函數(shù)的柯里化,而且會(huì)發(fā)現(xiàn),所有的 ramda 函數(shù)都是經(jīng)過(guò)柯里化的。

為什么 ramda.js 要對(duì)函數(shù)全部柯里化?

我們看一下普通的函數(shù) f(a, b, c)?。如果只在調(diào)用的時(shí)候,傳遞 a 。會(huì)發(fā)現(xiàn),JS 在運(yùn)行調(diào)用時(shí),會(huì)將 b 和 c 設(shè)置為 undefined 。

從上面可以知道,JS 語(yǔ)言不能原生支持柯里化。非柯里化函數(shù)會(huì)導(dǎo)致缺少參數(shù)的實(shí)參變成 undefined 。ramda.js 對(duì)函數(shù)全部柯里化的目的,就是為了優(yōu)化上面的場(chǎng)景。

下面,我們看一下?_curry2 代碼,這里為了可讀性,我對(duì)代碼進(jìn)行了改造,我把?_isPlaceholder 去掉了,假設(shè)沒(méi)有占位符,同時(shí)把?_curry1 放在函數(shù)內(nèi),并且對(duì)過(guò)程進(jìn)行了相應(yīng)注釋。

二元參數(shù)的柯里化,代碼如下:

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

function _curry1(fn) {
  return function f1(a) {
    // 對(duì)參數(shù)長(zhǎng)度進(jìn)行判斷
    if (arguments.length === 0) {
      return f1;
    } else {
      // 通過(guò) apply 來(lái)返回函數(shù) fn(a, 1)
      return fn.apply(this, arguments);
    }
  };
}

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

// 第一次調(diào)用是 fn(a, 1)
let r1  = add(1)
// 第二次調(diào)用是 fn(2,1)
let r2 = r1(2)
console.log('sss', r2)

完整代碼地址如下:

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

codeopen:codepen.io/godkun/pen/…

看了上面對(duì) ramda.js 源碼中柯里化的分析,是不是有點(diǎn)收獲,就像上面說(shuō)的,柯里化的目的是為了優(yōu)化在 JS 原生下的一些函數(shù)場(chǎng)景。好處如下:

  1. 從上面 add 函數(shù)可以知道,通過(guò)柯里化,可以讓函數(shù)在真正需要計(jì)算的時(shí)候進(jìn)行計(jì)算,起到了延遲的作用,也可以說(shuō)體現(xiàn)了惰性思想。

  2. 通過(guò)對(duì)參數(shù)的處理,做到復(fù)用性,從上面的 add 函數(shù)可以知道,柯里化把多元函數(shù)變成了一元函數(shù),通過(guò)多次調(diào)用,來(lái)實(shí)現(xiàn)需要的功能,這樣的話,我們就可以控制每一個(gè)參數(shù),比如提前設(shè)置好不變的參數(shù),從而讓代碼更加靈活和簡(jiǎn)潔。

柯里化命名的由來(lái)

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

關(guān)于 ramda 中的 compose 和 pipe -- 組合函數(shù)/管道函數(shù)

本文一開(kāi)始,我就以一個(gè)例子向大家展示了組合函數(shù) compose 和 pipe 的用法。

關(guān)于 ramda 中,compose 和 pipe 的實(shí)現(xiàn)這里就不再分析了,小伙伴自己看著源碼分析一下。這里我就簡(jiǎn)潔說(shuō)一下組合函數(shù)的一些個(gè)人看法。

在我看來(lái),組合是函數(shù)式編程的核心,函數(shù)式編程的思想是要函數(shù)盡可能的小,盡可能的保證職責(zé)單一。這就直接確定了組合函數(shù)在?函數(shù)式編程中的地位,玩好了組合函數(shù),函數(shù)式編程?也就基本上路了。

和前端的組件進(jìn)行對(duì)比來(lái)深刻的理解組合函數(shù)

函數(shù)的組合思想是面向過(guò)程的一種封裝,而前端的組件思想是面對(duì)對(duì)象的一種封裝。

三、實(shí)際工作中的實(shí)踐

1、寫一個(gè)集成錯(cuò)誤,警告,以及調(diào)試信息的 tap 函數(shù)

故事的背景

實(shí)際工作中,會(huì)遇到下面這種接收和處理數(shù)據(jù)的場(chǎng)景。

代碼如下:

// 偽代碼
res => {
  // name 是字符串,age 是數(shù)字
  if (res.data && res.data.name && res.data.age) {
    // TODO:
  }
}

上面這樣寫,看起來(lái)好像也沒(méi)什么問(wèn)題,但是經(jīng)不起分析。比如 name 是數(shù)字,age 返回的不是數(shù)字。這樣的話, if 中的判斷是能通過(guò)的,但是實(shí)際結(jié)果并不是想要的。

那該怎么辦呢?問(wèn)題不大,跟著我一步步的優(yōu)化就 OK 了。

(1)進(jìn)行第一次優(yōu)化

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

看起來(lái)是夠魯棒了,但是這段代碼過(guò)于命令式,無(wú)法復(fù)用到其他地方,在其他的場(chǎng)景中,還要重寫一遍這些代碼。

(2)進(jìn)行第二次優(yōu)化

//?is?是一個(gè)對(duì)象函數(shù)?偽代碼
res => {
  if (is.object(res.data) && is.string(res.data.name) && is.number(res.data.age)) {
    // TODO:
  }
}

將過(guò)程抽象掉的行為也是一種函數(shù)式思想。上面代碼,提高了復(fù)用性,將判斷的過(guò)程抽象成了 is 的對(duì)象函數(shù)中,這樣在其他地方都可以復(fù)用這個(gè) is 。

但是,代碼還是有問(wèn)題,一般來(lái)說(shuō),各個(gè)接口的返回?cái)?shù)據(jù)都是 res.data 這種類型的。所以如果按照上面的代碼,我們會(huì)發(fā)現(xiàn),每次都要寫 is.object(res.data)?這是不能容忍的一件事。我們能不能做到不寫這個(gè)判斷呢?

當(dāng)然可以,你完全可以在 is 里面加一層對(duì) data 的判斷,當(dāng)然這個(gè)需要你把 data 作為參數(shù) 傳給 is 。

(3)第三次優(yōu)化

//?is?是一個(gè)對(duì)象函數(shù)?偽代碼
res => {
  if (is.string(res.data, data.name) && is.number(res.data, data.age)) {
    // TODO:
  }
}

按照上面的寫法,is 系列函數(shù)會(huì)對(duì)第一個(gè)參數(shù)進(jìn)行 object 類型判斷,會(huì)再次提高復(fù)用性。

好像已經(jīng)很不錯(cuò)了,但其實(shí)還遠(yuǎn)遠(yuǎn)不夠。

(4)總結(jié)上面三次優(yōu)化

  • 有 if 語(yǔ)句存在,可能會(huì)有人說(shuō),if 語(yǔ)句存在有什么的啊?,F(xiàn)在我來(lái)告訴你,這塊有 if 為什么不好。是因?yàn)?if 語(yǔ)句的?()?里面,最終的值都會(huì)表現(xiàn)成布爾值。所以這塊限制的很死,需要解決 if 語(yǔ)句的問(wèn)題。

  • is 函數(shù)功能單一,只能做到返回布爾值,無(wú)法完成調(diào)試打印錯(cuò)誤處理等功能,如果你想打印和調(diào)試,你又得在條件分支里面各種 console.log ,然后這些代碼依舊過(guò)于命令式,無(wú)法重用。其實(shí),我們想一下,可以知道,這也是因?yàn)橛昧?if 語(yǔ)句造成的。

說(shuō)完這些問(wèn)題,那下面我們來(lái)解決吧。

(1)進(jìn)行函數(shù)式優(yōu)化--第一階段

如果要做到高度抽象和復(fù)用的話,首先把需要的功能羅列一下,大致如下:

第一個(gè)功能:檢查類型

第二個(gè)功能:調(diào)試功能,可以自定義 console 的輸出形式

第三個(gè)功能:處理異常的功能(簡(jiǎn)單版)

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

現(xiàn)在,如何將小函數(shù)組合成一個(gè)完成特定功能的函數(shù)呢?

想一下,你會(huì)發(fā)現(xiàn),這里需要用到函數(shù)的高階性,要將函數(shù)作為參數(shù)傳入多功能函數(shù)中。ok ,現(xiàn)在我們知道實(shí)現(xiàn)的大致方向了,下面我們來(lái)嘗試一下吧。

這里我直接把我的實(shí)現(xiàn)過(guò)程貼出來(lái)了,有相應(yīng)的注釋,代碼如下:

/** * 多功能函數(shù) * @param {Mixed} value 傳入的數(shù)據(jù) * @param {Function} predicate 謂詞,用來(lái)進(jìn)行斷言 * @param {Mixed} tip 默認(rèn)值是 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'}}

// 函數(shù)的組合,函數(shù)的高階
tap(res1.data.age, is.noNumber)
tap(res2.data.age, is.noNumber)

結(jié)果圖如下:

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

會(huì)發(fā)現(xiàn)當(dāng),age 不是 Number 類型的時(shí)候,就會(huì)打印對(duì)應(yīng)的提示信息,當(dāng)是 Number 類型的時(shí)候,就不會(huì)打印信息。

這樣的話,在業(yè)務(wù)中就可以直接寫:

res => {
  tap(res.data.age, is.noNumber)
  // TODO: 處理 age
}

不用 if 語(yǔ)句,如果有異常,看一下打印信息,會(huì)一目了然的。

當(dāng)然這樣寫肯定不能放到生產(chǎn)上的,因?yàn)?tap 不會(huì)阻止后續(xù)操作,我這樣寫的原因是:這個(gè) tap 函數(shù)主要是用來(lái)開(kāi)發(fā)調(diào)試的。

但是,如果需要保證不符合的數(shù)據(jù)需要直接在 tap 處終止,那可以在 tap 函數(shù)里面加下 return false return true 。然后寫成下面代碼的形式:

res => {
  // if 語(yǔ)句中的返回值是布爾值
  if (tap(res.data.age, is.noNumber)) {
    // TODO: 處理 age
  }
}

但是這樣寫,會(huì)有個(gè)不好的地方。那就是用到了 if 語(yǔ)句,用 if 語(yǔ)句也沒(méi)什么不好的。但退一步看 tap 函數(shù),你會(huì)發(fā)現(xiàn),還是不夠復(fù)用,函數(shù)內(nèi),還存在硬編碼的行為。

如下圖所示:

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

存在兩點(diǎn)問(wèn)題:

第一點(diǎn):把 console 的行為固定死了,導(dǎo)致不能設(shè)置 console.error()?等行為。

第二點(diǎn):不能拋出異常,就算類型不匹配,也阻止不了后續(xù)步驟的執(zhí)行。

怎么解決呢?

進(jìn)行函數(shù)式優(yōu)化--第二階段

簡(jiǎn)單分析一下,這里先采用惰性的思想,讓一個(gè)函數(shù)確定好幾個(gè)參數(shù),然后再讓這個(gè)函數(shù)去調(diào)用其他不固定的參數(shù)。這樣做的好處是減少了相同參數(shù)的多次 coding ,因?yàn)橄嗤膮?shù)已經(jīng)內(nèi)置了,不用再去傳了。

分析到這,你會(huì)發(fā)現(xiàn),這樣的行為其實(shí)就是柯里化,通過(guò)將多元函數(shù)變成可以一元函數(shù)。同時(shí),通過(guò)柯里化,可以靈活設(shè)置好初始化需要提前確定的參數(shù),大大提高了函數(shù)的復(fù)用性和靈活性。

對(duì)于柯里化,由于源碼分析篇,我已經(jīng)分析了 ramda 的柯里化實(shí)現(xiàn)原理,這里我為了節(jié)省代碼,就直接使用 ramda 了。

代碼如下:

const R = require('ramda')
// 其實(shí)這里你可以站在一個(gè)高層去把它們想象成函數(shù)的重載
// 通過(guò)傳參的不同來(lái)實(shí)現(xiàn)不同的功能
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() {
  // 不開(kāi)啟異常忽略,使用 console.log 的 tapLog 函數(shù)
  // tapLog(res.data.age, is.noNumber)

  // 開(kāi)啟異常忽略,使用 console.log 的 tapThrow 函數(shù)
  tapThrow(res.data.age, is.noNumber)
  console.log('能不能走到這')
}

main()

代碼地址如下:

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

關(guān)鍵注釋,我已經(jīng)在代碼中標(biāo)注了。上面代碼在第一次進(jìn)行函數(shù)式優(yōu)化的時(shí)候,在組合和高階的基礎(chǔ)上,加入了柯里化,從而讓函數(shù)變得更有復(fù)用性。

PS: 具有柯里化的函數(shù),在我看來(lái),也是體現(xiàn)了函數(shù)的重載性。

執(zhí)行結(jié)果如下圖所示:

如何編寫高質(zhì)量的 JS 函數(shù)(4) --函數(shù)式編程[實(shí)戰(zhàn)篇]

會(huì)發(fā)現(xiàn)使用 tapThrow 函數(shù)時(shí),當(dāng)類型不匹配的時(shí)候,會(huì)阻止后續(xù)步驟的執(zhí)行。

我通過(guò)多次優(yōu)化,向大家展示了,如何一步步的去優(yōu)化一個(gè)函數(shù)。從開(kāi)始的命令式優(yōu)化,到后面的函數(shù)式優(yōu)化,從開(kāi)始的普通函數(shù),到后面的逐步使用了高階、組合、柯里的特性。從開(kāi)始的有 if/else 語(yǔ)句到后面的逐步干掉它,來(lái)獲得更高的復(fù)用性。通過(guò)這個(gè)實(shí)戰(zhàn),大家可以知道,如何循序漸進(jìn)的使用函數(shù)式編程,讓代碼變得更加優(yōu)秀。

2、為什么要干掉 for 循環(huán)

之前就有各種干掉 for 循環(huán)的文章。各種討論,這里按照我的看法來(lái)解釋一下,為什么會(huì)存在干掉 for 循環(huán)這一說(shuō)。

代碼如下:

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

我們看上面這段代碼,我來(lái)問(wèn)一個(gè)問(wèn)題:上面這段代碼如何復(fù)用到其他的函數(shù)中?

稍微想一下,大家肯定可以很快的想出來(lái),那就是封裝成函數(shù),然后在其他函數(shù)中進(jìn)行調(diào)用。

因?yàn)?for 循環(huán)是一種命令控制結(jié)構(gòu),它很難被插入到其他操作中,也發(fā)現(xiàn)了 for 循環(huán)很難被復(fù)用的現(xiàn)實(shí)。

當(dāng)你在封裝 for 循環(huán)時(shí),就是在抽象 for 循環(huán),把它隱藏掉。就是在告訴用戶,你只需要調(diào)封裝的函數(shù),而不需要關(guān)心內(nèi)部實(shí)現(xiàn)。

于是乎,JS 就誕生了諸如 map filter reduce 等這種將循環(huán)過(guò)程隱藏掉的函數(shù)。底層本質(zhì)上還是用 for 實(shí)現(xiàn)的,只不過(guò)是把 for 循環(huán)隱藏了,如果按照業(yè)界內(nèi)的說(shuō)話逼格,就是把 for 循環(huán)干掉了。這就是聲明式編程在前端中的應(yīng)用之一。

你是如何處理數(shù)組變換的

三種方式:

第一種:傳統(tǒng)的循環(huán)結(jié)構(gòu) - 比如 for 循環(huán)

第二種:鏈?zhǔn)?/p>

第三種:函數(shù)式組合

3、如何利用函數(shù)的純潔性來(lái)進(jìn)行緩存

在編寫函數(shù)時(shí),要考慮緩存是為了避免計(jì)算重復(fù)值。計(jì)算就意味著消耗各種資源,而做重復(fù)的計(jì)算,就是在浪費(fèi)各種資源。

純潔性和緩存有什么關(guān)系?我們想一下可以知道,純函數(shù)總是為給定的輸入返回相同的輸出,那既然如此,我們當(dāng)然要想到可以緩存函數(shù)的輸出。

那如何做函數(shù)的緩存呢?記住一句話:給計(jì)算結(jié)果賦予唯一的鍵值并持久化到緩存中。

大致 demo 代碼:

function mian(key) {
  let cache = {}
  cache.hasOwnProperty(key) ?
    main(key) :
    cache[key] = main(key)
}

上面代碼是一種最簡(jiǎn)單的利用純函數(shù)來(lái)做緩存的例子。下面實(shí)現(xiàn)一個(gè)非常完美的緩存函數(shù)。

給原生?JS?函數(shù)加上自動(dòng)記憶化的緩存機(jī)制

代碼如下:

Function.prototype.memorized = () => {
  let key = JSON.stringify(arguments)

  // 緩存實(shí)現(xiàn)
  this._cache = this._cache || {}
  this._cache[key] = this._cache[key] || this.apply(this, arguments)
  return this._cache[key]

}

Function.prototype.memorize = () => {
  let fn = this
  // 只記憶一元函數(shù)
  if (fn.length === 0 || fn.length > 1) return fn
  return () => fn.memorized.apply(fn, arguments)
}

代碼地址如下:

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

通過(guò)擴(kuò)展 Function 對(duì)象,我們就可以充分利用函數(shù)的記憶化來(lái)實(shí)現(xiàn)函數(shù)的緩存。

上面函數(shù)緩存實(shí)現(xiàn)的好處有以下兩點(diǎn):

第一:消除了可能存在的全局共享的緩存

第二:將緩存機(jī)制抽象到了函數(shù)的內(nèi)部,使其完全與測(cè)試無(wú)關(guān),只需要關(guān)系函數(shù)的行為即可

四、備注

實(shí)戰(zhàn)部分,我沒(méi)有提到函子知識(shí),不代表我沒(méi)有實(shí)踐過(guò),正是因?yàn)槲覍?shí)踐過(guò),才決定不提它,因?yàn)閷?duì)于前端來(lái)說(shuō),有時(shí)候你要顧及整個(gè)團(tuán)隊(duì)的技術(shù),組合和柯里還有高階函數(shù)等還是可以很好的滿足基本需求的。

小伙伴們看實(shí)戰(zhàn)篇的時(shí)候,一定要結(jié)合理論篇一起看,這樣才能無(wú)縫連接。

五、參考

1、參考鏈接

  • 圖解 Monad

  • monad wiki

  • What is a monad?-stackoverflow

  • 讀書(shū)筆記: 范疇論

2、參考書(shū)籍

  • JavaScript ES6 函數(shù)式編程入門經(jīng)典
  • JavaScript 函數(shù)式編程指南
  • Haskell 趣學(xué)指南
  • 其他電子書(shū)

當(dāng)前題目:如何編寫高質(zhì)量的JS函數(shù)(4)--函數(shù)式編程[實(shí)戰(zhàn)篇]
網(wǎng)站網(wǎng)址:http://weahome.cn/article/pdgoch.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部