JavaScript 函数柯里化与反柯里化
JavaScript 函数柯里化(currying)与反柯里化(uncurrying)
JavaScript 函数柯里化
函数柯里化(function currying) 是高级函数中的一种较为普遍的应用技巧,其基本方法和函数绑定一样,即使用一个闭包返回一个函数。函数柯里化的特点是,当调用柯里化函数时,返回的函数还需要设置一些传入的参数。
柯里化函数常见的动态创建步骤是:调用另一个函数并为它传入要柯里化的函数和必要参数。
通用的柯里化函数写法如下:
function curry(fn){
var args = Array.prototype.slice.call(arguments,1);
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
}
}
function add(num1,num2){
return num1+num2;
}
var curriedAdd = curry(add,3,4);
console.log(curriedAdd());// 7
以上实现过程中,curry 方法主要作用是将除fn以外所有传入的参数集中起来,一起传入fn中执行,此函数没有考虑执行环境。
currying 更常用的场景是部分求值,即一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来,待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
以下是一个通用的 currying 应用案例
var currying = function(fn){
var args = [];
return function(){
if(arguments.length === 0){
return fn.apply(this, args);
}else {
[].push.apply(args, arguments);
return arguments.callee; // 返回自身,包含当前正在执行的函数
}
}
};
var cost = (function(){
var money = 0;
return function(){
for(var i=0,l=arguments.length;i<l;i++){
money += arguments[i];
}
return money;
}
})();
var cost = currying(cost);
cost(100);
cost(200);
cost(300);
console.log(cost());// 求值并输出
以上代码中,当调用函数 cost()时,如果明确地带上一些参数,就表示此时并不进行真正的求值,而是将参数保存起来,此时 cost 函数返回的是另一个函数,只有以不带参数的形式执行cost()时,才将之前保存的所有参数进行运算。
函数反柯里化
鸭子类型 的思想是动态语言的特点,当调用一个对象的某个方法时,并不关心该对象原本是设计来干什么的。
JavaScript 中也有很多时候需要让对象去借用一个原本不属于它的方法。
常有以下应用场景:
(function(){
Array.prototype.push.call(arguments,4);
console.log(arguments); // [1,2,3,4]
})(1,2,3);
let arr = new Array();
Object.prototype.toString.call(arr) === "[object Array]"; //
以上代码第一部分是类数组对象去借用 Array.prototype 的方法,也是 call 和 apply 常见的应用场景。第二部分是数组对象借用 Object.prototype 的方法来判断其类型。使用 call 和 this 可以把任意对象当作 this 传入某个方法,有了此操作,方法中用到this的地方都可以不局限与原本限定的对象,拓宽了对象方法的适用性。
以下是 反柯里化(uncurrying) 的一种实现方式之一
Function.prototype.uncurrying = function(){
var self = this;
return function(){
var obj = Array.prototype.shift.call(arguments);
return self.apply(obj, arguments);
}
}
// 此时的 obj 是传入的指定执行环境对象
反柯里化可以理解成原本是以 a.b(c)
形式调用的方法可以转换成 b(a,c)
的形式调用,后者的 a 还可以换成其他对象,这就扩大了函数的使用范围。
例如,可以利用上述的反柯里化的代码将 Array.prototype.push 方法转换成一个通用的 push 方法。
let push = Array.prototype.push.uncurrying();
let obj = {
"length": 1,
"0": 1
}
push(obj,2);
console.log(obj); // {0: 1, 1: 2, length: 2}
经过上述代码的第一行代码,push 函数的嗯作用就可以和 Array.prototype.push 一样了。
在 uncurrying 定义的过程中 反柯里化的实现是在调用 uncurrying
时将原有的方法当成 self ,返回一个函数,函数传入的第一个参数是新的执行环境对象,但是执行的方法实际是在 self 代表的原函数中实现的。
通过 uncurrying 此方法,可以将其他方法集中复制在一个对象上,例如可以将 Array 原型上的方法复制到 Array 对象上:
for(let i=0,fn,ary=['push','shift','unshift','forEach'];fn=ary[i++];){
Array[fn] = Array.prototype[fn].uncurrying();
};
let obj = {
"length":3,
"0":1,
"1":2,
"2":3
};
Array.push(obj,4);
let first = Array.shift(obj);
console.log(first); // 1
Array.forEach(obj, function(i,n){
console.log(n)// i 是值 n 是索引,此处分别输出 0,1,2
});
Array 对象上复制了'push','shift','unshift','forEach']
数组中的原型方法,扩宽了应用环境。
另一种 uncurrying 的实现方式为:
Function.prototype.uncurrying = function(){
var self = this;
return function(){
return Function.prototype.call.apply(self, arguments);
}
}
这一种写法与上述第一种写法效果完全相同,因为传入的参数 arguments 是 call 的所有参数,则第一个参数还是运行环境的对象。
经典面试题
实现一个 sum 方法,使计算结果能够满足如下效果:
sum(x,y) = sum(x)(y) = x+y;
当然函数参数可扩展。
解题思路分析:
因为,可以进行类似链式调用,所以每次调用返回的应该是一个函数,以便下一次调用,不传入参数时可以通过 重写 toString() 方法,实现求和。也就是可以先把所有传入的参数都存起来,等到需要的时候再触发一起求和,这个思想和 currying 很相似,所以可以用函数柯里化来实现。
// 不使用柯里化的解决方法,每次都先算一遍
function sum(num){
let sumNum = 0;
let args = [].slice.call(arguments);
sumNum += args.reduce(function(a,b){
return a+b;
})
let curryFun = function(numB){
let argsF = [].slice.call(arguments);
if(arguments.length === 0){
return sumNum;
}else {
sumNum += argsF.reduce(function(a,b){
return a+b;
})
return curryFun;
}
};
curryFun.valueOf = function(){
return sumNum;
}
curryFun.toString = function(){
return sumNum;
}
return curryFun;
}
let x = 2;
let y = 5;
console.log(sum(x,y));
console.log(sum(x)(y));
以上方法基本有了柯里化的影子,就是把每次调用的结果先求和存在sumNum 中,但是整个过程每次都计算一遍还是和柯里化有差距。
以下代码是柯里化解决函数求和的整个过程:
function sum(){
let _arr = [].slice.call(arguments);
let addFun = function(){
// 利用解构赋值暂存传入的参数
_arr.push(...arguments);
return addFun;
}
addFun.toString = function(){
return _arr.reduce(function(x,y){
return x+y;
});
}
// 返回的是函数
return addFun;
}
sum(1,3); // 4
sum(5)(6)(7); // 18
node 里面不会主动调用 toString 方法,需要在浏览器环境下,才会可能调用执行toString。
函数绑定
函数绑定是为了实现,要创建一个函数,可以在特定的 this 环境中以指定参数调用另一个函数。
JavaScript 库中实现了一个可以将函数绑定到指定环境的函数,此函数一般叫 bind();
一个简单的 bind函数接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。
简单的bind 函数如下所示:
function bind(fn, context){
return function(){
return fn.apply(context, arguments);
};
}
该函数实现的详细过程是,在 bind 函数中创建了一个闭包,闭包使用apply()调用传入的参数,并给apply() 传递 context 对象和参数。上述函数中arguments对象是内部函数的,并不是 bind() 函数的。
绑定函数的应用场景如下:
var handler = {
message: "Event handled",
handleClick: function(event){
console.log(this.message+":"+event.type);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click",bind(handler.handleClick,handler));
上述的 bind 函数创建了一个保持了执行环境的函数,并将其传给 EventUtil.addHandler()。handler.handleClick() 方法和平时一样获得了 event 对象,因为所有的参数都通过被绑定的函数直接传给了它。
EventUtil.addHandler(btn, "click",bind(handler.handleClick,handler));
中bind() 方法那里本来是一个函数,现在也是一个函数,这个函数是 bind 闭包返回的函数,并且绑定了指定的环境。
此外,支持 原生 bind() 方法的浏览器有IE9+、Firefox4+ 和 Chrome…
函数绑定与函数柯里化联用
函数柯里化也经常与函数绑定一起组合使用,例如以下代码构造出的 bind 函数就使用了函数柯里化:
function bind(fn,context){
var args = Array.prototype.slice.call(arguments, 2);
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(context, finalArgs);
}
}
bind 函数需要同时接受一个函数 fn 和一个object 对象 context,此时表示给被绑定的函数的参数从第三个开始,并且fn的执行环境转换成了传入的参数 context。此时调用 bind 就可以返回绑定到给定环境的函数。
使用带有柯里化的bind函数可以给事件传递额外的参数:
var handler = {
message: "Event handled",
handleClick: function(name, event){
console.log(this.message+":"+name+":"+event.type);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click",bind(handler.handleClick,handler,"my-btn"));
ES5 中的 bind() 方法也实现函数柯里化,只需传递除执行环境外的另一个参数就好。例如:
EventUtil.addHandler(btn,"click",handler.handleClick.bind(handler,"my-btn"));
JavaScript 中的柯里化函数和绑定函数提供了强大的动态函数创建功能。
评论