函数也是值
在 JavaScript 中,函数和其他值一样也是值。你可以把它存进变量、放进数组、传给另一个函数,或者从函数中返回它。仅这一点就开启了一整套编程风格:
一旦你习惯把函数当作值来看待,高阶函数就不再神秘了。高阶函数就是接收函数作为参数、返回一个函数,或者两者兼有的函数。定义就这么简单。
接收函数作为参数
最常见的形式:一个接收回调并替你执行它的函数。你其实早就在不知不觉中用过这类函数了。
forEach 是高阶函数:它接收你的函数,并对每个元素调用一次。setTimeout 也是高阶函数:它接收你的函数,并在延迟之后调用它。你只需关注做什么,它们负责什么时候做、做几次。
自己写一个也是一样的。下面这个小函数只在条件为真时才执行回调:
action 是一个参数,只不过它恰好保存的是一个函数。用 action() 调用它,就会执行传进来的那个函数。
你真正会用到的三个:map、filter、reduce
数组自带一些高阶方法,可以取代你原本要写的大部分 for 循环。学会这三个,大量日常代码都会变得更短、更清晰。
map:转换每个元素
map 对每个元素调用一次你的函数,并把返回值收集到一个新数组中。长度相同,内容经过转换。原数组不会被修改。
filter:保留符合条件的元素
filter 保留回调返回真值的元素,丢弃其余元素。返回值是一个新数组,可能更短。
reduce:把数组归约成一个值
reduce 是最通用的一个。回调接收当前的累加器和当前元素;你返回什么,什么就成为下一个累加器。第二个参数(这里是 0)是初始值。
你可以把它们链式调用,这正是这种风格的价值所在:
从上往下读:保留已付款的订单,取出价格,求和。没有循环,没有可变的计数器,也不用担心差一错误。
从函数中返回函数
这是高阶函数的另一半:一个构建并返回另一个函数的函数:
multiplyBy(2) 运行一次,交回一个全新的函数。这个新函数仍然记得 factor,这就是闭包,后面会有专门的一页来讲。现在只需记住:用不同的参数调用 multiplyBy,就能从同一个模板得到不同的专用函数。
这种模式随处可见:
一个定义,两个可复用的函数。这比手写 warn 和 info 再让它们保持同步要好得多。
具名函数与内联回调
你可以传入一个内联的箭头函数,也可以按名字传入一个函数。两种方式都可行,选读起来更顺的那种:
当你传入 isEven(不带括号)时,你交出的是函数本身。如果加上 (),它就会被立即调用,传进去的是它的结果。这是初学者的常见错误:
nums.filter(isEven); // correct: passes the function
nums.filter(isEven()); // wrong: calls isEven with no args, passes the result
如果回调超过了几行,就把它提取出来并起个名字。周围的代码通常也会因此更好读。
完整示例
高阶函数在组合小部件时最能发挥作用。假设你有一个商品列表,想要找出价格实惠且有库存的商品名称,并转成大写:
每个辅助函数只做一件事,每个数组方法只做一次转换。整个流水线读起来像是对你想要什么的说明,而不是对如何循环的描述。
什么时候不该用它们
高阶方法很好用,但并非在所有场景下都能替代循环:
- 如果需要中途停止,用带
break的for或for...of,比试图从forEach中跳出要清楚。 - 如果在回调中执行异步操作,
map和forEach不会等待它。改用带await的for...of,或者Promise.all配合map。 - 如果回调在修改共享状态,你就偏离了这种风格的长处。改用普通循环,或者重构成返回新值的写法。
用在合适的地方,map、filter 和 reduce 能省掉日常代码中大部分循环样板代码。用在所有地方,就会开始损害可读性。选那个最能表达意图的工具。
下一步:对象
函数并不是唯一值得用来构建程序的值。对象是 JavaScript 中把相关数据和行为组织在一起的主力,你刚才过滤和映射的那些数组里,装的大多也是对象。这就是下一页的内容。
常见问题
JavaScript 中的高阶函数是什么?
高阶函数是至少满足以下一点的函数:接收另一个函数作为参数,或者返回一个函数作为结果。Array.prototype.map、setTimeout 和 addEventListener 都是高阶函数:它们接收一个回调,并替你执行它。
map、filter 和 reduce 有什么区别?
map 转换每个元素,返回一个长度相同的新数组。filter 保留回调返回真值的元素,返回一个(可能更短的)数组。reduce 逐个合并元素,把数组归约成一个值。三者都是接收回调的高阶函数。
为什么要从一个函数中返回另一个函数?
为了构建小巧、可配置的辅助函数,而不必重复逻辑。像 multiplyBy(n) 这样的函数会返回一个乘以 n 的新函数,于是 multiplyBy(2) 和 multiplyBy(10) 从同一个定义中得到两个专用函数。这种模式利用了闭包,在事件处理器、中间件和工具库中很常见。