1、函数式编程的回顾
众所周知,函数允许咱们通过函数的length
属性来访问它们的参数数量。函数的 length
属性永远不会改变,因为该属性总是匹配函数的声明参数的数量。
但是,请看:
function howMany(a,b,c) {
console.log(howmany.length);
}
howMany(1,2); // 3
howMany(1,2,3,4); // 3
不可避免的,咱和咱们的函数要正确处理参数太少和太多的情况。
JavaScript 允许我们通过函数作用域内可用的 arguments
变量来访问传递给函数的所有参数。arguments
变量是一个包含函数调用时传递给该函数的所有参数的类数组列表。
所谓类数组是一个列表,它只有一个 length
属性,没有真正数组的其他属性。我们可以通过索引[]
来访问它,获取它的 length
属性,并使用循环语句迭代它。
其实,我们可以将其转换为“真实”数组,下面的代码将使参数列表对我们更加有用:
function showArgs() {
var args = [].slice.call(arguments);
}
[].slice.call(arguments)
是Array.prototype.slice.call(arguments)
的简写方式,我们只是利用了数组字面量的写法。
我想如果你稍微了解 ES6 新特性,你应该知道我们可以通过 spread operator(扩展操作) / rest parameters(剩余参数) 的帮助更容易地访问和“解开”我们的参数:
function howMany(...args) {
console.log("args:", args, ", length:", args.length);
}
howMany(1,2,3,4); // args: [1,2,3,4], length: 4 (a "real" array)!
2、Currying(函数柯里化)
Currying(函数柯里化)是把一个接受 N 个参数的函数转换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。使得每个函数都只接受1个参数。
例如经典求三角形周长函数:
function add(a,b,c) { return a+b+c; }
add()
函数接受 3 个参数并返回总和,我们可以把它转换成一个 Currying(柯里化) 函数,如下:
function curriedAdd(a) {
return function(b) {
return function(c) {
return a+b+c;
}
}
}
Currying(函数柯里化)是如何工作?
它的工作方式是通过为每个可能的参数嵌套函数,使用由嵌套函数创建的自然闭包来保留对每个连续参数的访问。
但是我们想要的是一种轻松的办法,可以将现有的带 N 个参数的函数转换成它的 Currying(柯里化)版本,而不用像 curriedAdd() 那样写出每个函数的 Currying(柯里化)版本。
2.1、编写一个通用的 curry()
理想情况下,这是我们想要设计的curry()
函数接口:
function foo(a,b,c){ return a+b+c; }
var curriedFoo = curry(foo);
curriedFoo(1,2,3); // 6
curriedFoo(1)(2,3); // 6
curriedFoo(1,2)(3); // 6
curriedFoo(1)(2)(3); // 6
我们的curry()
返回一个新的函数,允许我们用一个或多个参数来调用它,然后它将部分应用;直到它收到最后一个参数(基于原始函数的参数数量),此时它将返回使用所有参数调用原始函数的计算值。
而且,我们还需要存储传递的原始函数,所以一旦我们有了所有必需的参数,我们可以使用正确的参数调用原始函数并返回其结果。
这是我们curry()
的第一个尝试:
function curry(fn) {
return function curried() {
var args = [].slice.call(arguments);
return args.length >= fn.length ?
fn.apply(null, args) :
function () {
var rest = [].slice.call(arguments);
return curried.apply(null, args.concat(rest));
};
};
}
让我们详细解释一下
- 第 2 行:我们的 curry 函数返回一个新的函数,在这个例子中是一个名为 curried() 的命名函数表达式。
- 第 3 行:每次此函数被调用时,我们在 args 中存储传递给它的参数;
- 第 4 行:如果参数的数量大于等于原始函数的数量,那么
- 第 5 行:返回使用所有参数调用的原始函数
- 第 6 行:否则,返回一个接受更多参数的函数,当被调用时,将使用之前传递的原始参数与传递给新返回的函数的参数结合在一起,再次调用我们的 curried 函数。
我们来试一下我们原来的 add()
函数。
var curriedAdd = curry(add);
curriedAdd(1)(2,3); // 6
curriedAdd(1)(2)(3); // 6
但是,假设我们有一个具有函数的对象,它依赖于将适当的对象设置为该函数的调用上下文this
。我们便难以使用我们的curry函数来 Currying(柯里化)这个对象的方法。
var border = {
style: 'border',
generate: function(length, measure, type, color) {
return [this.style + ':', length + measure, type, color].join(' ') +';';
}
};
border.curriedGenerate = curry(border.generate);
border.curriedGenerate(2)('px')('solid')('#369')
// => "undefined: 2px solid #369;"
这样的操作得不到我们想要的结果,使用我们的curry()
函数作为一个方法修饰器似乎破坏了该方法所期望的对象上下文。我们必须保留原始的上下文,并确保并将其传递给已返回的curried
函数的连续调用。
function curry(fn) {
return function curried() {
var args = toArray(arguments),
context = this;
return args.length >= fn.length ?
fn.apply(context, args) :
function () {
var rest = toArray(arguments);
return curried.apply(context, args.concat(rest));
};
}
}
border.curriedGenerate(2)('px')('solid')('#369')
// => "border: 2px solid #369;"
现在我们的curry()
函数可以正确感知上下文了,并且可以在任何情况下用作函数装饰器。
但是此时curried
函数只能接受原始函数声明时的参数数量 – 不多不少。如果我们想 Currying(柯里化) 一个函数,这个函数具有可选的声明参数或可变数量参数(variadic 函数)时,那么前面的代码对我们并没有帮助。
3、Currying(柯里化) variadic(可变参数) 函数
待续。。。