for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i) }, i * 1000) }
上面这个内容会打印什么?
看过这题的都会知道答案,每隔一秒打印一个5,打印5次。如果我想将每一轮循环的i打印出来呢,很简单,将var替换成let;
这道题真的是考察闭包吗?
为什么要有闭包?
因为在JavaScript中,没有办法在函数外部访问到函数内部的变量对象。那么反之,有了闭包,我们可以在函数以外的任何地方访问到函数内部的变量对象。
(注意,我这里用的是变量对象,而不是某个变量,因为它是一个合集,准确的说,是包含了整个函数作用域。)
如何写闭包?
常见的闭包方式是:
function fn1() { var a = 1, b = 2; return function() { return a } } var fn2 = fn1(); fn2(); // 1
这里fn1执行完成后,按理说,内部的a、b所在的作用域应该会销毁,但是因为闭包的存在,返回的匿名函数保留了对当前作用域的引用,因此我们可以在fn1执行完成之后,依然可以访问到fn1内部的变量a,这就是闭包的使用。
(注意,这里虽然只是return了a,但是变量b也在内存中,也没有销毁,因为闭包保存的不是某个变量,而是整个变量对象)
再来看一些其它闭包例子
function fn1() { var a = 1; setTimeout(function() { console.log(a) }, 1000 ) } fn1(); // 1
当fn1执行完成后,内部作用域并没有销毁,而是被setTimeout保留下来了,因此这也是闭包!
var a = 1, b = 2; function () {} ..... var btn = document.getElementById('btn'); btn.addEventListener('click', function() {}, false);
没错,这也是闭包!我用DOM2级方式给btn这个dom节点添加事件,尽管里面什么变量都没有引入,但依然保留着外界的变量对象,这也是闭包!
除了上面这些,还有吗?当然有了,比如每一个带callback回调函数的,都是用了闭包,再比如每一个模块导出的时候,一定会有闭包来访问一些内部的函数或者变量,这也是闭包!
好了,现在我懂了
那我们再来回看最初提的那个问题,思考一下
为什么原题中的代码没有达到我们期待的效果?
我们所期待的是,每一次for循环,我们都能保存一个i的副本,将它保留下来并传给setTimeout,我们每次循环都会重新定义这个函数,也就是说第一次循环和第二次循环中的setTimeout是不一样的(也就是说循环结束的时候,是有5个函数)。题中的代码也就等同于下面的代码:
for (var i = 0; i < 5; i++) { { setTimeout(function() { console.log(i) }, i * 1000) } }
setTimeout本身就是一个闭包,而且大括号提供了一个块级作用域,所以我们理想情况下很容易做到,但是却失败了,原因是什么?并不是闭包的问题,而是我们保存的这个i的副本,出了问题。它们都被封闭在一个共享的全局作用域中,实际上只有一个i,看似有了块级作用域,但是没起作用,因为是var声明的变量不存在块级作用域,因此循环结束的时候,“所有”的i,其实也就是一个i,就是5。
这道题的解题思路是什么?
其实就是让var声明的变量i保留在块级作用域内。
那么我们再来看,为什么用let能解决这个问题,很简单,let声明的变量有块级作用域,因此i有了5个副本,并且毫不相关,再配合setTimeout的闭包,我们成功了!
上面那个方法也等于下面这个
for (var i = 0; i < 5; i++) { {
let j = i; setTimeout(function() { console.log(j) }, j * 1000) } }
还有没有别的方法了,如果不改变var,如何制造块级作用域?es5里虽然没有块级作用域,但是我们有模拟块级作用域的方法:函数作用域!
for (var i = 0; i < 5; i++) { var a = function(j) { setTimeout(function() { console.log(j) }, j * 1000) }; a(i); a = null; }
这里为了避免变量a污染全局,最后将a赋值为null,当然了,也可以let a ;
但是这样写又有些繁琐,因为还要创建一个函数a,然后再销毁,那能否不这样呢?
IIFE!也就是立即执行函数。
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j) }, j * 1000) })(i) }
综合来看,这道题与其说是考闭包,不如说是考块级作用域的概念,如果硬要考闭包,不如不给代码,把需求告诉他,让他手写一个,这样才行吧。
对了,这里再补充一点之前提过的,当我用let替换var的时候,既然每次循环都是一个块级作用域,互相不干扰,那为什么i会一直自动加1呢,它是怎么记得上次循环是多少呢?
因为JavaScript引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
end