高阶函数|博客系列
# 1. 函数式编程能解决的问题
- 可扩展性–我是否需要不断地重构代码来支持额外的功能?
- 易模块化–如果我更改了一个文件,另一个文件是否会受到影响?
- 可重用性–是否有很多重复的代码?
- 可测性–给这些函数添加单元测试是否让我纠结?
- 易推理性–我写的代码是否非结构化严重并难以推理?
# 2. 学习之前你需要了解的一些概念
# 函数输入
在数学中,函数总是获取一些输入值,然后给出一个输出值。
但在程序中,它或许有许多个输入值,或许没有。它或许有一个输出值( return 值),或许没有。
从上述的定义出发,所有的函数都需要输入。
大多数情况下,人们把函数的输入值称为 “arguments” 或者 “parameters” 。所以它到底是什么?
arguments 是你输入的值(实参), parameters 是函数中的命名变量(形参),用于接收函数的输入值。例子如下:
function fn (x, y) {
console.log(x, y)
}
fn(3, 4)
// 3, 4
3 和 4是函数 fn(..) 调用的 arguments。x 和 y 是 parameters,用于接收参数值(分别为 3 和 4 )。
在javascript中定义的形参和实参可以是不等的:
function fn (x, y) {
console.log(x, y)
}
fn(3)
// 3, undefined
你传入少于声明形参个数的实参,所有缺少的参数将会被赋予 undefined 变量,意味着你仍然可以在函数作用域中使用它,但值是 undefined。
# 输入计数
一个函数所“期望”的实参个数是取决于已声明的形参个数,即你希望传入多少参数。
如以下函数:
function fn (x, y, z) {
console.log(x, y, z)
}
fn期望三个参数,因为它声明了三个形参。这里有一个特殊的术语:Arity。Arity 指的是一个函数声明的形参数量。 fn(..) 的 Arity 是 3。
# 函数的length
你可能需要在程序运行时获取函数的 Arity,使用函数的 length 属性即可。
function fn (x, y, z) {
console.log(x, y, z)
}
fn.length // 3
提示: 函数的 length 属性是一个只读属性,并且它是在最初声明函数的时候就被确定了。它应该当做用来描述如何使用该函数的一个基本元数据。
引入ES6特性的一些函数的length:
function foo(x,y = 2) {
// ..
}
function bar(x,...args) {
// ..
}
function baz( {a,b} ) {
// ..
}
foo.length; // 1
bar.length; // 1
baz.length; // 1
# arguments
上面我们已经知道了,可以用函数的length属性来获取到这个函数的形参个数,但是在实际使用时,我们更需要知道的是函数传入的实参个数,此时可以使用每个函数都有的arguments对象(类数组),来获取到传入的实参个数。
如下面这个例子:
function foo(x,y,z) {
console.log( arguments.length ); // 2
}
foo( 3, 4 );
由于 ES5(特别是严格模式下)的 arguments 不被一些人认同,很多人尽可能地避免使用。尽管如此,它永远不会被移除,这是因为在 JS 中我们“永远不会”因为便利性而去牺牲向后的兼容性,但是大多数人还是不建议使用它。
所以你只需要知道以下几点:
- 当你需要知道参数个数的时候,
arguments.length还是可以用的。 - 不要通过
arguments[1]访问参数的位置。只要记住arguments.length。
# …ES6获取剩余参数
当你需要像数组那样访问参数,很有可能的原因是你想要获取的参数没有在一个规范的位置。我们如何处理?
ES6 救星来了!让我们用 ... 操作符声明我们的函数,也被当做 “spread”、“rest” 或者 “gather” (我比较偏爱)提及。
function foo(x,y,z,...args) {
// ..
}
看到参数列表中的 ...args 了吗?那就是 ES6 用来告诉解析引擎获取所有剩余的未命名参数,并把它们放在一个真实的命名为 args 的数组。args 无论是不是空的,它永远是一个数组。但它不包含已经命名的 x,y 和 z 参数,只会包含超出前三个值的传入参数.
function foo(x,y,z,...args) {
console.log( x, y, z, args );
}
foo(); // undefined undefined undefined []
foo( 1, 2, 3 ); // 1 2 3 []
foo( 1, 2, 3, 4 ); // 1 2 3 [ 4 ]
foo( 1, 2, 3, 4, 5 ); // 1 2 3 [ 4, 5 ]
你甚至可以直接在参数列中使用 ... 操作符,没有其他正式声明的参数也没关系:
function foo(...args) {
console.log(args)
}
foo(1, 2); // [1, 2]
现在 args 是一个由参数组成的完整数组,你可以尽情使用 args.length 来获取传入的参数。你也可以安全地使用 args[1] .
# 关于实参的小技巧
如果你希望调用函数的时候只传一个数组代替之前的多个参数,该怎么办?
function foo(...args) {
console.log( args[3] );
}
var arr = [ 1, 2, 3, 4, 5 ];
foo( ...arr ); // 4
…操作符在这里也被用到了。
在形参中使用的时候function foo(…args){},是将形参整合,形成一个数组:
function foo(x, y){}
// 经过...之后变为
function foo([x, y]){}
在实参中使用的时候foo(…arr),是将实参展开:
foo([1, 2, 3])
// 经过...之后变为
foo(1, 2)
你甚至可以多个值和…一起来使用:
function foo(...args) {
console.log( args[3] );
}
var arr = [ 2 ];
foo( 1, ...arr, 3, ...[4,5] ); // 4
// 相当于是foo(1, 2, 3, 4, 5); 所以获取到的args[3]为4
# 关于形参的小技巧
默认参数
在 ES6 中,形参可以声明默认值。当形参没有传入到实参中,或者传入值是 undefined,会进行默认赋值的操作:
function foo(x = 3) {
console.log( x );
}
foo(); // 3
foo( undefined ); // 3
foo( null ); // null
foo( 0 ); // 0
解构
现在有这么一个函数:
function foo(params) {
console.log(params)
}
foo([1, 2, 3])
我在拿到args数组之后,想要命名传入数组的第 1、2 个值,也许你可以这么做:
// example1
function foo(params) {
console.log(params) // [1, 2, 3]
var x = params[0] // 1
var y = params[1] // 2
var args = params.slice( 2 ) // [3] (args就是剩余的参数集合)
}
foo([1, 2, 3])
现在你可以用更酷的ES解构的方式来写这个函数:
// example
function foo( [x,y,...args] = [] ) {
console.log(x) // 1
console.log(y) // 2
console.log(args) // [3]
}
foo( [1,2,3] );
上面的这种[x, y, …args] = []就是一种数组解构,解构是通过你期望的模式来描述数据(对象,数组等),并分配(赋值)值的一种方式。
在这里例子中,解构告诉解析器,一个数组应该出现的赋值位置(即参数)。这种模式是:拿出数组中的第一个值,并且赋值给局部参数变量 x,第二个赋值给 y,剩下的则组成 args。
同样这种解构也可以用在对象中,称为对象解构:
function foo( {x,y} = {} ) {
console.log( x, y );
}
foo( {
y: 3
} ); // undefined 3
我们传入一个对象作为一个参数,它解构成两个独立的参数变量 x 和 y,从传入的对象中分配相应属性名的值。我们不在意属性值 x 到底存不存在对象上,如果不存在,它最终会如你所想被赋值为 undefined。
通过上面的学习,我们需要认识到很重要的一个原则:声明性代码通常比命令式代码更干净。
声明性代码也就是上面的example2,在定义形参的时候就给参数命好名,而example1就是命令式代码,它在拿到形参之后再进行命名。
# 随着输入而变化的函数
现在有这么一个函数:
function foo(x,y) {
if (typeof x == "number" && typeof y == "number") {
return x * y;
}
else {
return x + y;
}
}
明显地,这个函数会根据你传入的值而有所不同,比如:
foo( 3, 4 ); // 12
foo( "3", 4 ); // "34"
程序员这样定义函数的原因之一是,更容易通过同一个函数来重载不同的功能。乍一看这样设计一个函数好像很方便,使得我们的函数可以有很多不同的行为,其实通过不同的输入值让一个函数重载拥有不同的行为的技巧叫做特定多态(ad hoc polymorphism)。但在函数式编程中,要对方便的诱惑有警惕之心。因为你可以通过这种方式设计一个函数,即使可以立即使用,但这个设计的长期成本可能会让你后悔。
# 函数输出
在 JavaScript 中,函数只会返回一个值。下面的三个函数都有相同的 return 操作。
function foo() {}
function bar() {
return;
}
function baz() {
return undefined;
}
如果你没有 return 值,或者你使用 return;,那么则会隐式地返回 undefined 值。
如果想要尽可能靠近函数式编程的定义:使用函数而非程序,那么我们的函数必须永远有返回值。这也意味着他们必须明确地 return 一个值,通常这个值也不是 undefined。
