柯里化是啥?
柯里化是一种技术, 柯里是个人 。
什么人?
一个搞数学和逻辑学的人。
他发明的这个?
不是, 这个概念是Moses Schönfinkel发明, 由Gottlob Frege引入的。
那为什么以柯里命名呢?
可能和人大多知钱学森,却不知郭永怀一样吧。
柯里化概念
在数学和计算机科学中,局部套用是一种技术,它将接受多个参数(或元组参数)的函数求值转换为对一系列函数求值,每个函数都有一个参数。
用人话来说,就是利用闭包去缓存参数,然后返回一个拥有缓存参数的函数,当累计参数数量与原函数参数数量相等时,函数执行。
案例
如果想要做一个专门检查数据类型的库,一般实现方法如下:
代码片段编号:1
const tools = {
isString(arg){
return Object.prototype.toString.call(arg) === '[object String]'
},
isNumber(arg){
return Object.prototype.toString.call(arg) === '[object Number]'
},
isObject(arg){
return Object.prototype.toString.call(arg) === '[object Object]'
},
isArray(arg){
return Object.prototype.toString.call(arg) === '[object Array]'
},
...等等
}
console.log(tools.isString('')) //true
但是给人一种很low的感觉,因为重复代码太多了,改造如下。
代码片段编号:2
function typeFn(type){
return function (value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
}
const tools = {};
const Arr = ['String', 'Number', 'Object', 'Array'];
Arr.forEach(item => {
tools[`is${item}`] = typeFn(item)
})
console.log(tools.isString('')) //true
虽然实现了需要的功能,但这只是闭包应用,不是柯里化。
我们再换一个片场,比如现在你接手了一个既古老又庞大项目,项目中有如下函数:
代码片段编号:3
function typeFn(type, value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
很明显,这个函数就是来判断数据类型的。现在由于新的需求,你需要这个老函数,因为它写的并不错。但是你又要让他变得具体一点,比如像代码片段1那样分门别类,因为传两个参数让你感觉到啰嗦,但是你又不能像代码片段2一样修改他,因为冒然的修改历史代码后果很严重。这时候就需要借助柯里化来实现了。
代码片段编号:4
const currying = function(fn,...arr) {
let len = fn.length
return function(...arg) {
arr = [...arr, ...arg]
if(arr.length < len){
return currying(fn, ...arr)
}
return fn.call(this, ...arr)
};
};
const tools = {};
const Arr = ['String', 'Number', 'Object', 'Array'];
Arr.forEach(item => {
tools[`is${item}`] = currying(typeFn, item)
})
console.log(tools.isString('')) //true
在这个写法中typeFn,通过currying的柯里化包装,得到一个新的函数,包装过程中缓存了typeFn的第一个参数,之后再次调用,只需要传另一个参数就可以了,是不是方便多了。
这么写会不会造成内存泄漏?
柯里化还有一种应用场景,就是延迟执行。
好了,换片场了,现在有一个狠狠奇葩的情况,你要写一个商品展示的页面,这个页面初始化前,你需要先向后台请求一个接口A,这个接口返回给你的信息如下:
{
listApi: [
'https://api.taobao.com/goodslist',
'https://api.jingdong.com/goodslist',
'https://api.pinduoduo.com/goodslist',
],
key: 'price',
}
listApi字段表示,接下来你要请求的所有接口,length是固定的3个,但是接口名可能会变,这些接口会返回真正的商品列表数据,且数据格式一致,如下:
//https://api.taobao.com/goodslist,返回的数据
{
list: [
{ name: '电动趿拉板', price: 100, moodes: 325}
]
}
//https://api.jingdong.com/goodslist,返回的数据
{
list: [
{ name: '太阳能手电', price: 150, moodes: 210}
]
}
//https://api.pinduoduo.com/goodslist,返回的数据
{
list: [
{ name: '手摇洗衣机', price: 90, moodes: 550}
]
}
所有商品根据和A接口key字段的值一致的字段进行排序,但愿我说明白了。。
在此要先声明,Promise.all 和发布订阅模式也能实现,但是这里只用柯里化,也就是代码片段4的currying函数。
function tableList(key, List1, List2, List3 ){
let arr = [...List1, ...List2, ...List3];
arr.sort((o1, o2) => o1[key] - o2[key])
console.log(arr)
}
let fn = currying(tableList)
//A接口请求
setTimeout(()=>{
let Adata = {
listApi: [
'https://api.taobao.com/goodslist',
'https://api.jingdong.com/goodslist',
'https://api.pinduoduo.com/goodslist',
],
key: 'price',
}
fn = fn(Adata.key) // 在这里缓存第一个参数
//淘宝接口请求
setTimeout(()=>{
let taobaoList =[
{ name: '电动趿拉板', price: 100, moodes: 325}
]
fn = fn(taobaoList)
},2000)
//京东接口请求
setTimeout(()=>{
let jingdongList = [
{ name: '太阳能手电', price: 150, moodes: 210}
]
fn = fn(jingdongList)
},1000)
//拼多多接口请求
setTimeout(()=>{
let pinduoduoList = [
{ name: '手摇洗衣机', price: 90, moodes: 550}
]
fn = fn(pinduoduoList)
},3000)
},1000)
4秒后输出
[
{ name: '手摇洗衣机', price: 90, moodes: 550},
{ name: '电动趿拉板', price: 100, moodes: 325},
{ name: '太阳能手电', price: 150, moodes: 210}
]
柯里化的弊端也很明显,就是一定要事先确定函数的参数个数,实现此类场景还是使用Promise.all 和发布订阅模式比较合适,在此只为展示。
函数的bind方法就是柯里化
function.bind(thisArg[, arg1[, arg2[, ...]]])
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
//最新MDN对bind的Polyfill写法
Function.prototype.bind = function (content, ...arg) {
let _this = this
if (typeof _this !== 'function') {
throw new TypeError('Function.prototype.bind - ' +
'what is trying to be bound is not callable');
}
return function (...argument) {
return _this.apply(content, [...arg, ...argument])
}
}
function sum(a, b, c){
let d = this.d
return a + b + c + d
}
//注意返回的是新函数
//新函数中的this为 {d: 4}
let a = sum.bind({d: 4}, 1)
console.log(a(2, 3)) // 10
原来MDN的写法比这个复杂得多,因为考虑到了原函数的原型对象,以及函数可能用作构造函数使用,也就是使用new,类似下面这样。
//这是很久以前MDN对bind的Polyfill写法
Function.prototype.bind = function (context, ...args) {
if (typeof this !== "function") {
throw new TypeError("Bind must be called on a function");
}
let _this = this,
fn = new Function,
Bound = function (...arguments) {
return _this.apply(
this instanceof fn && context ? this : context,
[...args, ...arguments]
);
};
fn.prototype = this.prototype
Bound.prototype = new fn;
return Bound;
};
//下面是 new 修改 this 的相关代码:
this instanceof fn && context ? this : context
// ... 以及:
fn.prototype = this.prototype;
Bound.prototype = new fn();
我们并不会详细解释这段代码做了什么(这非常复杂并且不在我们的讨论范围之内),不过简单来说,这段代码会判断硬绑定函数是否是被 new 调用,如果是的话就会使用新创建
的 this 替换硬绑定的 this。
——《你不知道的JavaScript(上卷)》94页
从MDN修改bind的写法和相关书籍的解释可以看出,解释器内部的bind方法执行过程还是比较复杂的,无论你用代码怎么模拟都有欠缺,所以还是尽量使用原生bind方法。如果要支持低版本浏览器,请到 这里寻找解决方案。
npm install function-bind
Function.prototype.bind = require("function-bind")
反柯里化
再说反柯里化之前,先探讨一个问题,你是不是曾经和我有如下相同的疑问
Object.prototype.toString.call('a') //'[object String]'
//疑问:这是一个Object的原型方法,为什么执行对象可以是任何类型呢?
let a = {0:0,1:1,2:2,length:3}
Array.prototype.slice.call(a) //[0, 1, 2]
Array.prototype.push.call(a, 3)
consoel.log(a) //{0:0,1:1,2:2,3:3,length:4}
....
Array.prototype.slice.call('abc') //['a', 'b', 'c']
//疑问:这是一些Array的原型方法,为什么执行对象可以伪数组和String呢?
let a = ['ABc','Err','BlUe']
String.prototype.toLowerCase.call(a).split(',')
// ["abc", "err", "blue"]
//疑问:这是一个String的原型方法,为什么执行对象可以是Array呢?
解除疑惑第一步,首先看看call和apply干了什么
Function.prototype.call = function (context, ...args) {
//这段代码有必要:如 a = {a:Function.prototype.call}
//a.a() 没有意义
if (typeof this !== "function") {
throw new TypeError(`caller is not a function`);
}
//为这两个值时,指针会指向global,严格模式为undefined
//例子var obj = {a(){console.log(this)}}
//使用内置的call,obj.a.call(null) // window
if(context === undefined || context === null){
context = window
}
//其他所有值要转换为object
//例子var obj = {a(){console.log(this)}}
//使用内置的call,obj.a.call('') // String {""}
context = Object(context)
//在context的原型上添加方法,使函数调用时指向context
//使用Symbol确保添加的属性不会覆盖原有属性,且不可枚举
//总之原则就是,添加一个对原对象影响尽可能小的属性
let key = Symbol(),
pro = Object.getPrototypeOf(context);
Object.defineProperty(pro, key ,{
value: this,
configurable: true
})
let result = context[key](...args)
delete pro[key]
return result;
}
Function.prototype.apply = function (context, arr = []) {
if (typeof this !== "function") {
throw new TypeError(`caller is not a function`);
}
if(context === undefined || context === null){
context = window
}
context = Object(context)
let key = Symbol(),
pro = Object.getPrototypeOf(context);
Object.defineProperty(pro, key ,{
value: this,
configurable: true
})
let result = context[key](...arr)
delete pro[key]
return result;
}
在模拟代码中,call 和apply在执行时,先把执行函数定义在了context的原型对象上,然后用context去调用这个函数,这个this可不就是指向context了吗。
执行方式是知道了,但是还有一个疑问,执行过程是什么呢?这个关乎内置构造器的原型方法的写法,比如Array.prototype.slice,如果context能够为伪数组,就可以推断这个方法并没有对context的数据类型做校验,而只是对传入的数据检验了有没有length属性,有没有类似数组的索引属性。
//模仿内部实现
Array.prototype.slice = function (begin, end) {
var i, cloned = [],
size, len = this.length;
// 为“begin”处理负值
var start = parseInt(begin) || 0;
start = (start >= 0) ? start : Math.max(0, start + len);
// 为“end”处理负值
var over = end === undefined ? len : parseInt(end);
over = over == over ? Math.min(over, len) : 0;
if (over < 0) {
over = len + over;
}
// 切片的实际期望大小
size = over - start;
if (size > 0) {
cloned = new Array(size);
if (this.charAt) {
for (i = 0; i < size; i++) {
cloned[i] = this.charAt(start + i);
}
} else {
for (i = 0; i < size; i++) {
cloned[i] = this[start + i];
}
}
}
return cloned;
}
类似Array.prototype.slice这种不去校验调用者的本质类型,而是只要调用者具备特定的属性或方法,能通过执行测试,就能使用的行为。称作鸭子类型测试。
鸭子类型
![](https://img.haomeiwen.com/i8793674/51f49bb4c09bc04b.png)
美国印第安纳诗人詹姆斯·惠特科姆·莱利(1849年-1916年)可能是这个说法的创造者。他写道:“当我看到一只鸟,它走路像鸭子,游泳像鸭子,叫声像鸭子,我就称其为鸭子。”
套用主流观点:“当我看到一个人,他穿个格子衫,背个双肩包,头上没啥毛,我就称其为程序员”
(怎么把鸭子和程序员扯上关系了。。还好我不是程序员)
这种归纳推理方法有点像马克思主义哲学中描述的“形而上学”:用孤立、静止、片面的方式看待问题。
JavaScript与鸭子类型
JavaScript是动态语言,编译器将数据的类型检查安排在运行时而不是编译时,并在编译器的输出中包含用于运行时类型检查的代码。这些附加的内容使这门语言享受鸭子类型的大多数益处,仅有的缺点是需要在编译时识别和指定这些动态数据。
为了很好地解释,我使用函数执行过程,因为它体现了这门语言的鸭子特性且更具体。
function test(a, b, c){
console.log(a, b, c)
}
我们知道JavaScript的函数参数是按值传递,而不是引用,相当于给参数做了一个备份,所以上面函数相当于这个。
function test(a, b, c){
var a = a;
var b = b;
var c = c;
console.log(a, b, c)
}
编译器生成了检测数据类型的代码,解释器执行时根据这些代码去判断数据类型并存入内存。
![](https://img.haomeiwen.com/i8793674/6ddc8c1223077db7.png)
如果是静态类型的语言,例如Java,函数参数也是按值传递,但是参数在定义时就已经确定了类型,编译时无需生成用于类型检测的代码。
public void test(int a, char b, Object c){
System.out.print(""+a+b+c);
}
![](https://img.haomeiwen.com/i8793674/1899019eb7d5a694.png)
弊端:这样的方式要求JavaScript程序员在任何时候都必须很好地理解自己正在操作的代码,哪怕这并不是自己创造的。
微软出的TypeScript就是为了弥补JavaScript这一弊端而产生的。
扯得太远了,不过个人认为了解反柯里化之前先了解这些是有必要的。因为反柯里化本身并不难,而且用处也不多,反而不如前面提到的那些衍生问题重要。
反柯里化的三种写法
Function.prototype.unCurring = unCurring
function unCurring() {
if (typeof this !== "function") {
throw new TypeError(`caller is not a function`);
}
return (context, ...arg) => this.apply(context, arg)
}
function unCurring() {
if (typeof this !== "function") {
throw new TypeError(`caller is not a function`);
}
return (...arg) => this.call(...arg)
//或者这样
//return (arg) => Function.prototype.call.apply(this, arg);
}
function unCurring() {
if (typeof this !== "function") {
throw new TypeError(`caller is not a function`);
}
return Function.prototype.call.bind(this)
}
(...arg) => this.call(...arg)
与 (arg) => Function.prototype.call.apply(this, arg)
为什么等价?
设:fn为一个任意自定义函数
fn.call( context, 1, 2, 3 )
与
Function.prototype.call.apply( fn, [context, 1, 2, 3] );
为啥他俩是等价的?
设:Function.prototype.call = A
那么
Function.prototype.call.apply( fn, [context, 1, 2, 3] ) 等于
A.apply( fn, [context, 1, 2, 3] )
根据apply的执行原理,可知
A.apply( fn, [context, 1, 2, 3] ) 等于
fn.A( context, 1, 2, 3 )
那么在换算回来
伪代码
fn.(Function.prototype.call)( context, 1, 2, 3 )
最后得:fn.call( context, 1, 2, 3 )
只需要记住其中一个就可以了,看看他能干什么。
var push = Array.prototype.push.unCurring();
var a = {}
push(a, 1, 2, 3)
console.log(a) // {0: 1, 1: 2, 2: 3, length: 3}
柯里化是参数越来越少,而反柯里化正好是越来越多,通过上面这个例子,使用反柯里化生成了一个伪数组的push方法,但说实话这个玩意真不知道有什么作用。反柯里化的应用还需要探索。
网友评论