第四章 闭包与高阶函数
4.1 理解闭包
简而言之,闭包就是一个内部函数,是在另一个函数内部的函数,比如
function outer() {
function inner() {
}
}
这就是闭包,函数inner称为闭包函数,闭包如此强大的原因在于它对作用域链(或作用域层级)的访问。
闭包有3个可访问的作用域:
- 1.在它自身声明之内声明的变量
function outer() {
function inner() {
let a = 5;
console.log(a)
}
inner() // 调用inner函数
}
// tips: inner函数在outer函数的外部是不可见的
当inner函数被调用时,控制台返回5,因为闭包函数可以访问所有在其声明内部声明的变量。
- 2.对全局变量的访问
现将上面的代码片段修改为:
let global = "global"
function outer() {
function inner() {
let a = 5
console.log(global)
}
inner() // 调用inner函数
}
现在当inner函数执行后,打印出变量global,如此,闭包就能访问全局变量了
- 3.对外部函数变量的访问
再修改一下函数
let global = "global"
function outer() {
let outer = "outer"
function inner() {
let a = 5
console.log(outer)
}
inner() // 调用inner函数
}
当inner函数执行后,打印出变量outer,看起来是合理的,但却是一个非常重要的闭包属性。
闭包能够访问外部函数的变量,此处外部函数的含义是包裹闭包函数的函数。
闭包可以访问外部函数的参数,比如将outer函数添加一个参数,在inner函数中尝试访问它,是可以拿到该参数的。
闭包还有一个重要概念: 闭包可以记住它的上下文。
var fn = (arg) => {
let outer = "Visible"
let innerFn = () => {
console.log(outer)
console.log(arg)
}
return innerFn
}
var closureFn = fn(5)
closureFn()
=>Visible
=>5
解析
1.var closureFn = fn(5)
,这里fn被参数5调用,它返回了innerFn,
2.当innerFn被返回时,javascript执行引擎视innerFn为一个闭包,并且相应地设置了它的作用域
闭包有3个作用于层级,这3个层级(arg、outer值将被设置到inner的作用域层级中)在innerFn返回时都被设置了,如此closureFn通过作用域链被调用时就记住了arg、outer值。
3.最后调用closureFn时,因为它已经记住了它的上下文(作用域,也就是outer和arg),因此对console.log的调用才能正确打印出结果。
回顾上章的sortBy函数
const sortBy = (property) => {
return (a, b) => {
var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0
return result
}
}
当我们以如下方式调用sortBy时,sortBy("firstname")
发生了以下事情:
sortBy函数返回了一个接受两个参数的新函数
(a, b) => { /* 实现 */ }
闭包的返回函数能够范围sortBy函数的参数property,该函数只有在sortBy被调用时才会返回。而这时property参数会被替换为一个值。因此返回函数将在其生命周期中持有该上下文:
// 通过闭包持有的作用域
property = "passedValue"
(a, b) => { /* 实现 */ }
由于返回函数在它的上下文中持有property的值,所以它将在合适并且需要的时候使用返回值。
4.2 真实的高阶函数
- tap函数
const tap = (value) =>
(fn) => (
typeof(fn) === 'function' && fn(value),
console.log(value)
)
// tap函数接受一个value,并返回一个包含value的闭包函数,该函数将被执行。
javascript中,(exp1, exp2)的含义是它将执行两个参数并返回第二个表达式的结果,即exp2。
运行tap函数
tap("fun")((it) => console.log("value is ", it))
=> value is fun
=> fun
那么这个函数有什么用处,假设遍历一个来自服务器的数组,并发现数据错了,想调试一下,看看数组内究竟包含了什么,会如何做?抛弃命令式的方法,用函数是的方法来看,这正是使用tap函数,对于此场景可以这样做
forEach([1, 2, 3], (a) =>
tap(a)(()=>{
console.log(a)
})
)
- unary函数
array原型有一个默认的方法称为map,map是一个与之前定义的forEach函数非常相似的函数(遍历),唯一的区别是map返回了回调函数的结果。
假设要使一个数组加倍并得到结果,可以使用map函数以如下方式实现:
[1, 2, 3].map((a) => { return a * a })
=>[1, 4, 9]
这里map用3个参数调用了函数,分别是element、index和arr,假设要把字符串数组解析为整数数组,有一个内置的函数叫做parseInt,它接受两个参数parse和radix,如果可能,该函数会把传入的parse转换成数字。如果把parseInt传给map函数,map会把index的值传给parseInt的radix参数,这将产生意想不到的行为。
['1', '2', '3'].map(parseInt)
=> [1, NaN, NaN]
这个结果并不是我们期望的,我们需要把parseInt函数转换成为另一个只接受一个参数的函数。
用unary函数可以做到,它的任务是接受一个给定的多参数函数,并把它转换成一个只接受一个参数的函数,如下:
const unary = (fn) =>
fn.length === 1
? fn
: (arg) => fn(arg)
检查传入的fn是否有一个长度为1的参数列表(可通过length属性查看),有就什么也不做;没有就返回一个新函数,只接收一个参数arg,并且该参数调用fn,重新运行上面的问题。
['1', '2', '3'].map(unary(parseInt))
=> [1, 2, 3]
此处unary函数返回了一个新函数(parseInt的克隆体),它只接收一个参数,如此map函数传入的index、arr参数就不会对程序产生影响。
也有像binary一样的函数,它们转换函数,使其接受相应的参数。
- once函数
只运行一次给定的函数,比如只想设置一次第三方库,或者初始化一次支付设置。
const once = (fn) => {
let done = false
return function () {
return done ? undefined : (done =true), fn.apply(this, arguments)
}
}
接受一个参数fn,并且通过调用它的apply方法返回结果。声明了一个done变量,初始值为false, 返回的函数会形成一个覆盖它的闭包作用域,因此,返回的函数会访问并检查done是否为true,是则返回undefined,否则将done设置为true(阻止了下一次执行),并用必要的参数调用fn
apply函数允许设置函数的上下文,并为给定的函数传递参数。
var doPayment = once(() => {
console.log("Payment is done")
})
doPayment()
=> Payment is done
doPayment()
=> undefined
- memoized函数
假设有一个纯函数名为factorial,计算给定数字的阶乘
var factorial = (n) => {
if (n === 0) {
return 1
}
// 递归
return n*facotrial(n-1)
}
该函数只依赖它的参数执行,其它不需要。有一个局限性,无法重用之前的计算结果,memoized函数能够记住其计算结果
const memoized = (fn) => {
const lookupTable = {}
return (arg) => lookupTable[arg] || (lookupTable[arg] = fn(arg))
}
声明lookupTable的局部变量,它在返回函数的闭包上下文中,返回函数将接受一个参数并检查它是否在lookupTable中,如果在则返回对应的值(lookupTable[arg]
);否则,使用新的输入作为key,fn的结果作为value,更新lookupTable对象(lookupTable[arg] = fn(arg)
)
现在将上面的factorial用memoized函数改写
let fastFactorial = memoized((n) => {
if (n === 0) return 1
// 递归
return n *fastFactorial(n-1)
})
调用fastFactorial
fastFactorial(5)
=> 120
它以同样的方式运行,但是比之前快得多,运行fastFactorial时,会检查lookupTable对象。
附上
第三章地址:高阶函数
第二章地址:JavaScript函数基础
第一章地址:函数编程简介