前言

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 StringPrototypeIndexOf = wrap(String.prototype.indexOf)
const StringPrototypeSubstr = wrap(String.prototype.substr) StringPrototypeIndexOf('hello', 'e') // 1
StringPrototypeSubstr('hello', 2, 3) // "llo"

函数式编程 —— 将 JS 方法函数化的更多相关文章

  1. 小白的Python之路 day3 函数式编程,高阶函数

    函数式编程介绍   函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的 ...

  2. python 函数式编程:高阶函数,map/reduce

    python 函数式编程:高阶函数,map/reduce #函数式编程 #函数式编程一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数 #(一)高阶函数 f=abs f print ...

  3. Learning Python 012 函数式编程 1 高阶函数

    Python 函数式编程 1 高阶函数 高阶函数 Q:什么是高阶函数? A:一个函数接收另一个函数作为参数,这种函数就称之为高阶函数. 简单举个例子: def add(x, y, f): return ...

  4. Python之旅Day3 文件操作 函数(递归|匿名|嵌套|高阶)函数式编程 内置方法

    知识回顾 常见五大数据类型分类小结:数字.字符串.列表.元组.字典 按存值个数区分:容器类型(列表.字典.元组) 标量原子(数字.字符串) 按是否可变区分:可变(列表.字典) 不可变(数字.字符串.元 ...

  5. Scala - 快速学习08 - 函数式编程:高阶函数

    函数式编程的崛起 函数式编程中的“值不可变性”避免了对公共的可变状态进行同步访问控制的复杂问题,能够较好满足分布式并行编程的需求,适应大数据时代的到来. 函数是第一等公民 可以作为实参传递给另外一个函 ...

  6. javascript里的偏函数——本质函数式编程+闭包,返回函数

    最终效果: var greet = function(greeting, name) { return greeting + ' ' + name; }; var sayHelloTo = _.par ...

  7. Scala实战高手****第12课:Scala函数式编程进阶(匿名函数、高阶函数、函数类型推断、Currying)与Spark源码鉴赏

    /** * 函数式编程进阶: * 1.函数和变量一样作为Scala语言的一等公民,函数可以直接赋值给变量 * 2.函数更常用的方式是匿名函数,定义的时候只需要说明输入参数的类型和函数体即可,不需要名称 ...

  8. Python函数式编程:内置函数reduce 使用说明

    一.概述 reduce操作是函数式编程中的重要技术之一,其作用是通过对一个集合的操作,可以从中生成一个值.比如最常见的求和,求最大值.最小值等都是reduce操作的典型例子.python通过内置red ...

  9. Python函数式编程:内置函数map()使用说明

    一.概述 map操作是函数式编程中的重要技术之一,其作用就是对一个集合中的每个元素做处理,生成一个新的元素,由这些新的元素组成一个新的集合的返回. 所以map操作后,产生的新集合的元素个数和原集合的元 ...

随机推荐

  1. 浅谈C#更改令牌ChangeToken

    前言 在上篇文章浅谈C#取消令牌CancellationTokenSource一文中我们讲解了CancellationTokenSource,它的主要功能就是分发一个令牌,当我取消令牌我可以进行一些回 ...

  2. Kickstart无人值守原理及简介

    原文转自:https://www.cnblogs.com/itzgr/p/10029461.html作者:木二 目录 一 简介及原理 二 搭建无人值守步骤 三 PXE介绍 四 Kickstart简介 ...

  3. for循环练习之打印三角形

    public class TestDemo01 { /** * 打印三角形 * 1.打印空格 * 2.打印三角形 */ public static void main(String[] args) { ...

  4. Kafka详细教程加面试题

    一.部署kafka集群 启动zookeeper服务: zkServer.sh start 修改配置文件config/server.properties #broker 的全局唯一编号,不能重复 bro ...

  5. SciPy笔记

    一.简介 SciPy 是一个开源的 Python 算法库和数学工具包.Scipy 是基于 Numpy 的科学计算库,用于数学.科学.工程学等领域,很多有一些高阶抽象和物理模型需要使用 Scipy.Sc ...

  6. ElasticAlert基于聚合告警

    背景 最近公司网站经常被漏洞扫描,虽然并没有什么漏洞给对方利用,但是每次扫描我们也必须要察觉到,如果扫描的量太大,可以考虑从公有云的安全组上禁用掉这个IP,所以需要统计指定时间内每个IP的访问次数,这 ...

  7. C# Dapper基本三层架构使用 (四、Web UI层)

    三层架构的好处,一套代码无论WinForm还是Web都可以通用,只写前台逻辑就可以了,现在展示Web调用三层的示例 首先在项目中创建一个Web MVC5项目,目前项目目录如下 在Web项目Web.co ...

  8. 【曹工杂谈】Maven IOC容器的下半场:Google Guice

    Maven容器的下半场:Guice 前言 在前面的文章里,Maven底层容器Plexus Container的前世今生,一代芳华终落幕,我们提到,在Plexus Container退任后,取而代之的底 ...

  9. go语言游戏服务端开发(一)——架构

    五邑隐侠,本名关健昌,12年游戏生涯. 本教程以Go语言为例.   网络游戏程序分为客户端和服务端.客户端负责图形渲染.交互和一些简单校验处理,服务端负责业务逻辑处理.数据存储. 我们开发一个游戏de ...

  10. 将dict.define转化成dict.txt

    在使用捷通智能灵云外呼系统V6.1时.需要大量使用到模式码,也就是正则表达式.而老版本365还是使用场景文件. 当要将老版本改编成新版本的时候,需要需要将dict.define文件中的一行行的词条用& ...