关于闭包
众所周知,JavaScript的一大特色就是闭包的存在,能够让内部函数访问到外部函数的变量。那么,我们该怎样认识闭包,以及在ES6标准下闭包会是怎样的呢?
从一段代码开始
var test = function () {
var arr = []
for(var i = 0; i < 5; i++){
arr.push(function () {
return i*i
})
}
return arr
}
var test1 = test()
console.log(test1[0]())
console.log(test1[1]())
console.log(test1[2]())
老生常谈的闭包典型例子,不用说,三个控制台打印的结果均为25(这种明显有坑的题大概面试答案都能猜出来...),让我们来一步一步解析这段代码:
- 创建了一个循环
我们在这个for循环内部,为数组push了5个函数,均为
function () {
return i*i
}
我们期望这里内部函数i能够获取到每次循环的i值,但是显然没能如我们所料。这是为什么呢,我们暂且不表,继续看下去
- 调用了函数test
我们将test这个函数执行,并赋给test1。这样test1就成为了一个数组,并且是其内部元素均为函数。需要注意,这里内部的函数都还未执行!它们还没有在任何地方被调用! - 打印其中部分值
在打印这一步能够清楚的看到,我们开始调用其内部的函数。注意调用的时间,是在打印这一步,那么此时的i值为几呢?显然,此时我们已经在循环外部,i值为5。所以这也是为何打印值均为25。
那么,怎样能够得到我们预期的效果呢?或者说,怎样让循环在每次执行的时候,都使得内部函数能够记住外部传入的i值呢?我们开始想到作用域。
在JavaScript中,不存在块级作用域(先不讨论ES6),只有函数作用域。作用域的好处在于我们的内部函数是可以访问外部函数的变量的,这一点非常重要!
回头看代码,我们需要让循环中每次push的函数都记住当前循环下的i,那么我们就需要一个立即执行函数,让每次循环直接得出结果,那么我们来改造一下这段代码:
var test = function () {
var arr = []
for(var i = 0; i < 5; i++){
arr.push(function (n) {
return n*n
}(i))
}
return arr
}
var test1 = test()
console.log(test1)
这样循环每次执行时,将i作为参数实时传到内部函数中,这样便实现了保存每次i的值。但是和上面函数不一样的地方是,我们这里获得的test1,实际是一个由数字组成的数组,而不是函数,这我们的目标是不符的。
我们需要的是数组内每个函数都能保存其创建时的i值,那么简单,在之前立即执行函数的基础上,让它的返回值也成为一个函数就行,而参数既然能往下传一层,当然也可以传两层,于是我们动手:
var test = function () {
var arr = []
for(var i = 0; i < 5; i++){
arr.push(function (n) {
return function () {
return n * n
}
}(i))
}
return arr
}
var test1 = test()
console.log(test1[0]())
console.log(test1[1]())
console.log(test1[2]())
结果为:0, 1, 4。
可以看到,我们只是在之前的基础上进行了一点加工,让外层匿名函数的返回时也变为一个函数,让内层函数的返回值为我们需要的结果,由于内层函数保持着对传进来的参数i的引用,导致i的值一直保存在内存中没有被释放,使得我们在调用这个函数的时候仍然能获得每次i的值,这也就是我们所说的闭包的实现。
阶段小结
由上面我们能够获得这样的结论:闭包能够保存变量的引用,使得函数内部的变量在其他地方也能够使用,但是弊端也存在,会显著消耗内存。
看看ES6
我们在ES6中获得了两个新的声明方式:let和const,这里我们仅讨论let。
先来看看MDN的说明:
let允许你声明一个作用域被限制在块级中的变量、语句或者表达式。与var关键字不同的是,var声明的变量只能是全局或者整个函数块的。
我们看到了什么?看到了之前在JavaScript中不存在的块级作用域,替代该死的、有极大误导性的函数作用域。现在我们拥有了let,该怎样实现之前的例子呢?简单到你无法想象:
var test = function () {
const arr = []
for(let i = 0; i < 5; i++){
arr.push(function () {
return i*i
})
}
return arr
}
var test1 = test()
console.log(test1[0]())
console.log(test1[1]())
console.log(test1[2]())
结果:0, 1, 4.
就是如此简单,和最开始相比,整个函数我们只替换了2处:const arr = []
以及let i = 0
,由于let声明的块级作用域,每次循环的i都会被直接固定下来而不会受其他地方的影响,轻松实现了闭包的效果。
所以在ES6环境下,我们不需要用立即执行函数的形式来hack实现闭包,只需要使用新的声明方式,一切迎刃而解。