使用函数的风格调用 JS 方法

2021-10-11 23:41:26 +08:00
 iqoo

前言

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 容易搞晕,因此我们拆开分析。

回顾绑定公式:

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 StringPrototypeIndexOf = wrap(String.prototype.indexOf)
const StringPrototypeSubstr = wrap(String.prototype.substr)

StringPrototypeIndexOf('hello', 'e')  // 1
StringPrototypeSubstr('hello', 2, 3)  // "llo"
1927 次点击
所在节点    程序员
9 条回复
learningman
2021-10-12 10:53:43 +08:00
总觉得没必要管这个,正常用户不会动这个,非法用户你在 js 做啥防护都是透明的还是白折腾
galikeoy
2021-10-12 12:56:04 +08:00
都无法确定代码执行环境的正确性了,你这些还有意义吗
markgor
2021-10-12 15:18:10 +08:00
虽然说每件大事都是由一堆看不起眼的小事组合而成的,
但是我觉得没这个必要...
就如 1# 2# 所说到的,
如果这种做法成为常态,那应该是编译器上干的事,把所有原生方法都克隆一份出来,再给出个文档介绍如何使用,而不是应用级别需要去考虑的。
---我不是专业前端,只是出于流程上进行考虑。
CPoet
2021-10-13 09:48:09 +08:00
过度考虑了哈,不如直接约定。
JerryCha
2021-10-13 11:27:37 +08:00
写一个 proto-method.js ,今年的 KPI 就有了
lisongeee
2021-10-13 11:43:26 +08:00
>不过同样值得注意,Reflect.apply 也未必是原生的,也有被用户重写的可能。因此该接口也需提前备份

既然它会被重写,那么如果用户先是重写了 Reflect.apply 再调用 你提供的方法,此时你拿到的就已经是修改过的 Reflect.apply,这种情况你怎么办?
iqoo
2021-10-13 15:29:15 +08:00
@lisongeee 这样就没办法了,前提是自己的代码最先运行,就像 node.js 里的案例一样。
iqoo
2021-10-13 15:34:39 +08:00
@learningman
@galikeoy
@markgor
@llzero54 防重写只是其中一个用法,并不是主目的。还有其他应用案例,比如用于函数式编程,一切都用函数实现,包括方法调用。
markgor
2021-10-13 17:03:52 +08:00
@iqoo
函数式 和 方法式 我觉得没必要要求统一...
当然仅仅是个人习惯而已,
针对前端 1 年以上经验的,哪种使用方法调用和哪种使用函数调用基本都了解了。
但是突如其来都变成函数式,他还要去追代码看......

我知道不能以我的个人习惯去否决其他人的个人习惯,
只能说对我而言意义不大,毕竟我只是个后端,前端太多东西让人眼花缭乱....

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/807175

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX