长亭百川云 - 文章详情

函数式编程 —— 将 JS 方法函数化 - EtherDream

博客园 - EtherDream

43

2024-07-20

前言

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

性能提升

相比方法调用,函数调用无需访问全局的原型成员,理论上性能会更快。我们来验证下,例如将 substring 函数化:

const str = 'abcdef'

const wrap = Date.bind.bind(Date.call)
const substring = wrap(String.prototype.substring)

console.time('t1')
for (let i = 0; i < 1e8; i++) {
  str.substring(0, 4)
}
console.timeEnd('t1')    // ~65ms

console.time('t2')
for (let i = 0; i < 1e8; i++) {
  substring(str, 0, 4)
}
console.timeEnd('t2')    // ~48ms

在 v8 引擎上,函数式调用比普通的方法调用少了 1/4 的开销。不过在其他 JS 引擎上,例如 FireFox 和 Safari,函数式反而慢好几倍。看来 v8 对此有专门的优化。

如果你的 JS 只运行在 Chromium 或 Node.js 上,那么性能敏感的地方可尝试使用函数式调用进行优化。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2