函数式编程 —— 将 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 StringPrototypeIndexOf = wrap(String.prototype.indexOf)
const StringPrototypeSubstr = wrap(String.prototype.substr)
StringPrototypeIndexOf('hello', 'e')  // 1
StringPrototypeSubstr('hello', 2, 3)  // "llo"
函数式编程 —— 将 JS 方法函数化的更多相关文章
- 小白的Python之路 day3  函数式编程,高阶函数
		函数式编程介绍 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的 ... 
- python 函数式编程:高阶函数,map/reduce
		python 函数式编程:高阶函数,map/reduce #函数式编程 #函数式编程一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数 #(一)高阶函数 f=abs f print ... 
- Learning Python 012 函数式编程 1 高阶函数
		Python 函数式编程 1 高阶函数 高阶函数 Q:什么是高阶函数? A:一个函数接收另一个函数作为参数,这种函数就称之为高阶函数. 简单举个例子: def add(x, y, f): return ... 
- Python之旅Day3   文件操作  函数(递归|匿名|嵌套|高阶)函数式编程   内置方法
		知识回顾 常见五大数据类型分类小结:数字.字符串.列表.元组.字典 按存值个数区分:容器类型(列表.字典.元组) 标量原子(数字.字符串) 按是否可变区分:可变(列表.字典) 不可变(数字.字符串.元 ... 
- Scala - 快速学习08 - 函数式编程:高阶函数
		函数式编程的崛起 函数式编程中的“值不可变性”避免了对公共的可变状态进行同步访问控制的复杂问题,能够较好满足分布式并行编程的需求,适应大数据时代的到来. 函数是第一等公民 可以作为实参传递给另外一个函 ... 
- javascript里的偏函数——本质函数式编程+闭包,返回函数
		最终效果: var greet = function(greeting, name) { return greeting + ' ' + name; }; var sayHelloTo = _.par ... 
- Scala实战高手****第12课:Scala函数式编程进阶(匿名函数、高阶函数、函数类型推断、Currying)与Spark源码鉴赏
		/** * 函数式编程进阶: * 1.函数和变量一样作为Scala语言的一等公民,函数可以直接赋值给变量 * 2.函数更常用的方式是匿名函数,定义的时候只需要说明输入参数的类型和函数体即可,不需要名称 ... 
- Python函数式编程:内置函数reduce 使用说明
		一.概述 reduce操作是函数式编程中的重要技术之一,其作用是通过对一个集合的操作,可以从中生成一个值.比如最常见的求和,求最大值.最小值等都是reduce操作的典型例子.python通过内置red ... 
- Python函数式编程:内置函数map()使用说明
		一.概述 map操作是函数式编程中的重要技术之一,其作用就是对一个集合中的每个元素做处理,生成一个新的元素,由这些新的元素组成一个新的集合的返回. 所以map操作后,产生的新集合的元素个数和原集合的元 ... 
随机推荐
- mzy,struts学习(一)
			大家都在讲struts已经过时了,现在都是前后台分离,没有必要去学一个淘汰的框架,但是怎么讲呢?我觉得,struts能够流行那么多年,肯定有它的原因,肯定有很多优秀和好的地方,有一个指导过我的人给我讲 ... 
- c++制表符
			1 #include<iostream> 2 using namespace std; 3 int main() 4 { 5 //制表符是8个字符,当在一个制表符开始的位置(明白什么位置是 ... 
- [转载]getContentPane()
			关于setContentPane()和getContentPane()的应用 我们可以在 JFrame 对象中添加 AWT 或者 Swing 组件.但是,虽然它有 add 方法,却不能直接 ... 
- linux 常用命令(四)——(centos7-centos6.8)Vim安装
			centos是默认安装了vi编辑器的,vim编辑器是没安装或者未完全安装的,个人习惯用vim,所以记录一下vim编辑器的安装: 1.查看vim相关软件信息: yum search vim 2.在线安装 ... 
- 【CSS复合选择器、元素显示模式、背景】前端小抄(3) - Pink老师自学笔记
			[CSS复合选择器.元素显示模式.背景]前端小抄(3) 本学习笔记是个人对 Pink 老师课程的总结归纳,转载请注明出处! 一.CSS的复合选择器 1.1 什么是复合选择器 在 CSS 中,可以根据选 ... 
- nacos配置
			server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: #nacos 服务 ... 
- eslint and stylelint config
			eslint: module.exports = { root: true, env: { browser: true, es6: true, node: true ... 
- 如何实现LRU缓存
			大家好,我是程序员学长,今天我们来聊一聊LRU缓存问题. Tips: LRU在计算机软件中无处不在,希望大家一定要了解透彻. 问题描述 设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设 ... 
- C#多线程开发-线程池03
			你好,我是阿辉. 前面2篇文章介绍了线程的基础知识和线程同步,下面我们来一起认识学习下,线程池的使用. 线程池 创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销.一般情况下,都会 ... 
- 哦?原来这就是 JVM 垃圾!
			大家都知道,JVM 有垃圾回收的机制,垃圾回收的前提是要知道:什么是垃圾!然后再是如何识别垃圾! 什么是垃圾 垃圾,本质上就是没有引用的对象(们),下面来介绍两种垃圾 1. 没有引用指向的对象 下图是 ... 
