函数柯里化(★)
1、柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。简单来说,只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就称之为柯里化。
2、一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值(延迟执行),而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
// 普通的 add 函数
function add(x, y) {
return x + y;
}
// 柯里化后:先用一个函数接受 x 然后返回一个函数去处理 y 参数
function curryingAdd(x) {
return function (y) {
return x + y;
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
3、柯里化用途:
(1)参数复用:本质上是降低通用性,提高适用性。
// 示例一:多个不同域名下的请求
function ajax(type, url, data) {
var xhr = new XMLHttpRequest();
xhr.open(type, url, true);
xhr.send(data);
}
// 虽然 ajax 这个函数非常通用,但在重复调用的时候会参数冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")
// 利用柯里化
var ajaxCurry = curry(ajax);
// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");
// 以 POST 类型请求来自于 www.test.com 的数据
var postFromTest = post('www.test.com');
postFromTest("name=kevin");
// 示例二:多个不同校验规则的正则
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') // false
check(/[a-z]+/g, 'test') // true
// 利用柯里化
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
(2)提前返回
例如:兼容现代浏览器以及 IE 浏览器的事件添加方法,attachEvent 是 IE 的方法,addEventListener 是主流浏览器的方法。 - 只进行一次判断。
// 每次绑定事件的时候都要走一遍 if-else 的判断逻辑
var addEvent = function(el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};
// 柯里化逻辑之后
// 先自执行一次,之后调用就不会需要再走 if-else 判断
var addEvent = (function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
})();
(3)延迟执行:和 ES6 的bind
方法一样,只返回函数体,不执行。
①最简单的实现
var curry = function (fn) {
// args 获取第一个方法内的全部参数 - 闭包
var args = [].slice.call(arguments, 1);
return function () {
// 将后面方法里的全部参数和 args 进行合并
var newArgs = args.concat([].slice.call(arguments));
// 把合并后的参数通过 apply 作为 fn 的参数并执行
return fn.apply(this, newArgs);
};
};
// 调用
let add = (a,b) => a + b;
var addCurry = curry(add, 1);
addCurry(2);
// 但是不支持 addCurry(1)(2) 形式的调用
②增加递归实现addCurry(1)(2)
【补充知识点】
JS 函数的 length 属性: length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,那些已定义了默认值的参数不算在内,比如function(xx = 0
的 length 是 0。
// 第二版 - 支持多参数传递
var processCurry = function(fn, args = []) {
let _this = this; // 保留 this 上下文
let len = fn.length; // 函数必传的参数个数
return function() {
// concat 既可以拼接数字 3 也可以拼接数组 [3,4]
let _args = [].slice.apply(arguments).concat(args);
// 如果 concat 后的参数个数小于必传参数的 length ,则继续递归调用 - 收集参数
if (_args.length < len) {
return processCurry.call(_this, fn, _args)
}
// 参数收集完毕,则执行 fn
return fn.apply(this, _args);
}
}
// 测试
let multiple = (a, b, c) => a * b * c;
// 调用方式1
let processMultiple = processCurry(multiple);
let res = processMultiple(2)(3)(4); //24
// 调用方式2
let processMultiple1 = processCurry(multiple, 3);
let res1 = processMultiple1(4)(5); // 60