函數式編程 —— 將 JS 方法函數化

前言

JS 調用方法的風格爲 obj.method(...),例如 str.indexOf(...)arr.slice(...)。但有時出於某些目的,我們不希望這種風格。例如 Node.js 的源碼中有很多 類似這樣的代碼

const {
  ArrayPrototypeSlice,
  StringPrototypeToLowerCase,
} = primordials

// ...
ArrayPrototypeSlice(arr, i)

爲什麼不直接使用 arr.slice() 而要多此一舉?

因爲 arr.slice() 實際調用的是 Array.prototype.slice,假如用戶重寫了這個方法,就會出現無法預期的結果。所以出於慎重,通常先備份原生函數,運行時只用備份的函數,而不用暴露在外的函數。

調用

備份原生函數很簡單,但調用它時卻有很多值得注意的細節。例如:

// 備份
var rawFn = String.prototype.indexOf
// ...

// 調用
rawFn.call('hello', 'e')    // 1

這種調用方式看起來沒什麼問題,但實際上並不嚴謹,因爲 rawFn.call() 仍使用了 obj.method(...) 風格 —— 假如用戶修改了 Function.prototype.call,那麼仍會出現無法預期的結果。

最簡單的解決辦法,就是用 ES6 中的 Reflect API:

Reflect.apply(rawFn, 'hello', ['e'])    // 1

不過同樣值得注意,Reflect.apply 也未必是原生的,也有被用戶重寫的可能。因此該接口也需提前備份:

// 備份
var rawFn = String.prototype.indexOf
var rawApply = Reflect.apply
// ...

// 調用
rawApply(rawFn, 'hello', ['e'])    // 1

只有這樣,才能做到完全無副作用。

簡化

有沒有更簡單的方案,無需用到 Reflect API 呢?

我們先實現一個包裝函數,可將 obj.method(...) 變成 method(obj, ...) 的風格:

function wrap(fn) {
  return function(obj, ...args) {
    return fn.call(obj, ...args)
  }
}
const StringPrototypeIndexOf = wrap(String.prototype.indexOf)
StringPrototypeIndexOf('hello', 'e')  // 1

運行沒問題,下面進入消消樂環節。

v1

即使沒有包裝函數,我們也可直接調用,只是稍顯累贅:

String.prototype.indexOf.call('hello', 'e')   // 1

既然參數都相同,這樣是否可行:

const StringPrototypeIndexOf = String.prototype.indexOf.call
StringPrototypeIndexOf('hello', 'e')  // ???

顯然不行!這相當於引用 Function.prototype.call,丟失了 String.prototype.indexOf 這個上下文。

如果給 call 綁定上下文,這樣就正常了:

const call = Function.prototype.call
const StringPrototypeIndexOf = call.bind(String.prototype.indexOf)
StringPrototypeIndexOf('hello', 'e')   // 1

整理可得:

const call = Function.prototype.call

function wrap(fn) {
  return call.bind(fn)
}

const StringPrototypeIndexOf = wrap(String.prototype.indexOf)
StringPrototypeIndexOf('hello', 'e')  // 1

v2

既然 wrap(fn)call.bind(fn) 參數都相同,那麼是否可繼續簡化,直接消除 wrap 函數?

和之前一樣,直接引用顯然不行,而是要預先綁定上下文。由於會出現兩個 bind 容易搞暈,因此我們拆開分析。

回顧綁定公式:

  • 綁定前 obj.method(...)

  • 綁定後 method.bind(obj)

call.bind(fn) 中,obj 爲 call,method 爲 bind。套入公式可得:

bind.bind(call)

其中第一個 bind 爲 Function.prototype.bind

整理可得:

const call = Function.prototype.call
const wrap = Function.prototype.bind.bind(call)

const StringPrototypeIndexOf = wrap(String.prototype.indexOf)
StringPrototypeIndexOf('hello', 'e')  // 1

v3

到此已沒有可消除的了,但我們可以用更短的函數名代替 Function.prototype,例如 Map、Set、URL 或者自定義的函數名。

出於兼容性,這裏選擇 Date 函數:

const wrap = Date.bind.bind(Date.call)
const StringPrototypeIndexOf = wrap(String.prototype.indexOf)
StringPrototypeIndexOf('hello', 'e')  // 1

結尾

現在我們可用更簡單、兼容性更好的方式,將方法函數化,並且無副作用:

const wrap = Date.bind.bind(Date.call)

const find = wrap(String.prototype.indexOf)
const mid = wrap(String.prototype.substr)

find('hello', 'e')  // 1
mid('hello', 2, 3)  // "llo"

用起來是不是也很簡單~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章