• 函数式编程 —— 将 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"
    

    用起来是不是也很简单~

  • 相关阅读:
    如何用JS判断身份证格式
    ELK+log4j笔记
    JQM进阶:page事件执行过程
    jqm入门页面及对话框
    windows下mongodb的安装
    Bootstarp相关类
    兼容性及相关问题总结
    浏览器加载和渲染html的顺序
    【转】关于top、clientTop、scrollTop、offsetTop等
    pl_sql操作--激活提升权限scott用户
  • 原文地址:https://www.cnblogs.com/index-html/p/js-method-functional-style.html
Copyright © 2020-2023  润新知