前言
柯里化和偏函数都是函数式编程里面重要的概念,我们今天来来点不一样的 反柯里化。
不过既然是反柯里化,就先了解一下其姊妹 柯里化和偏函数。
柯里化 和偏函数
1.1 柯里化
维基百科上说道:
柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
还是看个例子吧,比较直观:
function sum(a, b, c) { return a + b + c; } function currySum(a){ return function (b){ return function (c){ return a + b + c; } } } sum(1,2,3) // 6 currySum(1)(2)(3) // 6 复制代码
简单点说就两点:
- 多段传参
- 返回的函数,满足条件就是执行函数,返回结果
1.1.1 难点
通用的柯里化函数难点就是在于如何知道函数参数的长度, 常用的手段是:
Function.prototype.length
属性获取参数长度
缺点嘛,rest参数是不计算在长度里面的。
下面的代码,获取的length是2就是最好的事实。
function log(msg1, msg2, ...other){ console.log(msg1, msg2, ...other); } console.log(log.length); // 2 复制代码
- 柯里化的时候,显示的传入参数的长度
- 当然可以综合两者,一个函数默认值就ok了
function curry(fn, length = fn.length){ // .... } 复制代码
1.1.2 占位符
柯里化后来多个一个占位符的概念,啥意思,就是,这个参数我先不传,后面再传入。
看个lodash官方的例子。
var abc = function(a, b, c) { return [a, b, c]; }; var curried = _.curry(abc); // Curried with placeholders. curried(1)(_, 3)(2); // => [1, 2, 3] 复制代码
至于实现嘛,高级版本lodash.curry,平民版本 JavaScript专题之函数柯里化。
1.2 偏函数
和柯里化有相似之处,简单理解参数两次传递
- 第一次固定部分参数
- 第二次传入剩余参数
看个underscore官方的例子,这个例子也 出现了占位符,不过不影响理解。
var subtract = function(a, b) { return b - a; }; sub5 = _.partial(subtract, 5); sub5(20); => 15 // Using a placeholder subFrom20 = _.partial(subtract, _, 20); subFrom20(5); => 15 复制代码
至于实现, 高级版本 lodash.partial 以及underscore.partial, 贫民版本JavaScript专题之偏函数
到这里,偏函数的功能和bind
有相似之处。
不过,这些都不是今天的重点,今天的重点是反柯里化。
反柯里化
2.1 概念
反柯里化是一种拿来主义,把别人的东西拿过来用。
先看个例子: 我们常用来判断数据类型的Object.prototype.toString
function unCurry(fn) { return function (context) { return fn.apply(context, Array.prototype.slice.call(arguments, 1)); } } // 不使用反柯里化 Object.prototype.toString.call({}); // [object Object] // 反柯里化 const toString = unCurry(Object.prototype.toString); toString({}); // [object Object] toString(() => { }); // [object Function] toString(1); // [object Number] 复制代码
2.2 实现
2.2.1 基础版本:简单好理解
function unCurry(fn) { return function (context) { return fn.apply(context, Array.prototype.slice.call(arguments, 1)); } } 复制代码
2.2.2 原型版本: 入侵 + 便利
Function.prototype.unCurry = function(){ var self = this; return function(){ return Function.prototype.call.apply(self, arguments); } } 复制代码
这个版本入侵了原型,我不喜欢。 便利性倒还是有的。
理解上的难点两个
self = this
self等于函数本身,这里就是暂存函数Function.prototype.call.apply(self, arguments)
说起来费事,看下面的转换吧。
Function.prototype.call.apply(self, arguments) ==> Function.prototype.call.bind(self)(arguments) ==> self.call(arguments) ==> self.call(arguments[0], arguments[1-n]) // arguments[0]就是self函数的上下文了 复制代码
使用的话,也会略有变化
2.2.3 原型版本2
这个我不做解读,大家自行理解一波
Function.prototype.unCurry = function () { return this.call.bind(this); }; 复制代码
借助ES6, 下面的代码是不是也可以呢?
Function.prototype.unCurry = function () { return (...args) => this.call(...args) }; 复制代码
当前还有各种好玩的写法,更多详情可以参考# 柯里化&&反柯里化
2.3 使用场景
反柯里化是一种思路,其实现肯定是可以被其他方案替代的,但是多一种思路,就多一种手段。
2.3.1 判断数据类型
上面的demo已经演示过了。
2.3.2 数组push(高级编程中的例子)
const push = unCurry(Array.prototype.push); const arr = [1, 2, 3]; push(arr, 4, 5, 6); console.log(arr); 复制代码
2.3.3 复制数组
const clone = unCurry(Array.prototype.slice); var a = [1,2,3]; var b = clone(a); console.log("a==b", a === b); // a==b false console.log(a, b); // [ 1, 2, 3 ] [ 1, 2, 3 ] 复制代码
2.3.4 发送事件
const dispatch = unCurry(EventTarget.prototype.dispatchEvent); window.addEventListener("event-x", (ev) => { console.log("event-x", ev.detail); // event-x ok }) dispatch(window, new CustomEvent("event-x", { detail: "ok" })); 复制代码