理解闭包前,先引入一个概念,作用域链
用我自己理解的讲:在一段程序中,程序内的变量、函数等都被串在这条链上,当我们使用这些变量、函数时,程序就会在这条链中搜索,如果没有找到你调用的变量或函数,就会出现问题
举个例子:
var a = 0,b = 1
function add(a, b){
return a + b
}
//上面的a,b还有add就被串在了这个程序的作用域链中
console.log(add(a, b))
//调用add(a, b),程序就会在作用域链上搜索,找到add方法和a、b变量
这里我们的变量和函数都是定义在全局中(最外层)的,如果定义在函数内呢?
var a = 0
function foo(){
var bar = 0
console.log(a)//0
}
//在foo外层访问bar变量
console.log(bar)//报错Uncaught ReferenceError: bar is not defined
为什么在这里报错说bar没有定义呢?
按照我的理解,作用域链分为子作用域链和父作用域链,本例变量a和函数foo串在外层作用域链中,而bar串在foo函数内的作用域链中,两条链的关系就和父子一样
当我们在全局(最外层)中访问a变量(console.log(a)),程序就会优先在全局作用域链(也就是上面的父作用域链)中查找a变量
当我们在foo函数中访问bar变量(console.log(bar),程序就会优先在foo作用域链中查找bar变量
到这里一切安好,直到我们在全局中访问foo函数中的bar变量,就出了问题,bar定义在foo作用域链中,程序在父作用域链中进行搜索,并不会进入子作用域链中,自然是搜索不到的,程序就会认为我们没有定义这个变量,就会报错
但我们在foo方法中访问a变量,为什么又可以了呢?
原来程序不仅仅会在子作用域链中搜索,还会上升到父作用域链,这时候foo链中找不到,就会去全局链中查找,自然能访问到变量a
到这里我们了解了程序在作用域链上的搜索是有范围的,它的范围就称作JavaScript中的执行上下文(简称上下文)
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。(《JavaScript高级程序设计(第四版)》)
到这里就很明确啦,js中的函数有属于自己的作用域,在它自有的作用域中可以访问外部,但外部不能反向访问函数内部的变量
明白了作用域链和执行上下文的概念,接下来就看看闭包是什么
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数。(《JavaScript高级程序设计(第四版)》)
function foo(){
var a = 0
return function(){
return a++
}
}
var bar = foo()
bar()
console.log(a)//1
bar()
console.log(a)//2
这个例子乍一看有些奇怪,且等我为你一一道来
第一步很简单,我们定义了一个foo函数,在foo函数里定义了一个a变量
第二步,我们return了一个返回值,与往常不同的是,这次返回的值是一个匿名函数,匿名函数中实现了a++的功能
第三步,在下方我们定义了bar,调用foo函数,让它等于foo函数的返回值,也就是我们在第二步中定义的匿名函数,现在我们就可以认为,bar是一个函数,是foo中返回的匿名函数
第四步,我们就可以调用bar方法,实现a++的操作
现在你可能不明白我想要做什么,不要急,思考一个问题:这个例子的作用域链是什么样的呢?试着画一画
答案:
bar被赋值了foo中的匿名函数,bar自然就连接在了foo函数的作用域链上,根据我们上面提到的,作用域链内部可以访问外部,也就是说,bar可以访问foo中的变量a,也可以访问外部的全局变量
那我们为什么不直接在全局中定义a变量,然后对它操作呢?
实际上,如果我们将每个要使用的变量都定义为全局变量,就会导致全局变量过多,这对维护与优化来说并不是好事情
使用了闭包,我们将要使用的变量放在函数作用域中,使用函数作用域返回的函数来操作这个变量,就避免了将变量定义在全局中,在我们不需要它的时候直接为bar赋值为null,没办法再调用到那个匿名函数,程序就会自动检测到并回收foo作用域链,为我们清理出内存
这里对作用域链与闭包只做了简单介绍,像更深入理解请移步红宝书(《JavaScript高级程序设计(第四版)》)
扩展
使用自执函数优化闭包:
var bar = (function(){
var a = 0
return function(){
a++;
}
})
bar()
console.log(a)//1
bar()
console.log(a)//2
与前一个例子中不同的是,我没有定义foo函数,直接用一个自执行函数替代了foo,我们需要的只是foo对应的函数作用域链,它本身我们并不关心,所以我们可以像本例这样进行简写