在上一篇“javascript 难点剖析一:prototy(原型)”后, 我们紧接着来理解什么是闭包。闭包虽不是 javascript 的特色功能, 但要理解还真要费那么点工夫。
在理解闭包之前, 首先要清楚 javascript 中的作用域只有2种: 全局作用域 和 方法作用域。全局作用域很好理解了, 方法作用域就是指一个 function 形成一个独立的作用域, 而且方法作用域还能够嵌套。
与别的语言不同的是: 花括号 “{}” 不能形成一个独立的作用域, 例如 Java 中的作用域
作用域
1 | var global = 0; |
闭包
我们以 for 循环开始
1 | for (var i = 0; i < 10; i++) { |
思考:这段代码会输出什么呢?
仔仔细细看了 30 秒,战战兢兢地说:每隔 1 秒后按顺序输出 0-9 吗?
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
我们将这段代码放到浏览器中执行一下得出结果:
在 1 秒后几乎同时输出了 10 个 10(注意不是每隔 1 秒输出一个 10)
为什么?
原因
因为 setTimeout 是异步执行!!
原理
这是由于 javascript 的 “消息队列” 和 “异步函数” 机制导致的结果。
知识普及:
栈:
JavaScript 是单线程语言,主线程执行同步代码。
函数调用时,便会在内存形成了一个“调用记录”,又称“调用帧”,保存调用位置和内部变量等信息。如果函数内部还调用了其他函数,那么在调用记录上方又会形成一个调用记录,所有的调用记录就形成一个“调用栈”。堆:
对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。消息队列与事件循环Event Loop:
一个 JavaScript 运行时包含了一个待处理的消息队列(异步任务),(内部是不进入主线程,而进入”任务队列”(task queue)的任务。比如UI事件、ajax网络请求、定时器setTimeout和setInterval等。
每一个消息都与一个函数(回调函数callback)相关联。当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。
这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。javascript 是单线程,主线程执行同步代码,事件、 I/O 操作等异步任务,将会进入任务队列执行,异步执行有结果之后,就会变为等待状态,形成一个先进先出的执行栈,主线程的同步代码执行完之后,再从”任务队列”中读取事件,执行事件异步任务的回调。
这就是为什么执行顺序是, 同步 > 异步 > 回调
更简单的说:只要主线程空了(同步),就会去读取”任务队列”(异步),这就是 JavaScript 的运行机制。
举个野栗子便于理解
因为 setTimeout 是异步执行,所以我们可以将这个 for 循环拆成 2 个部分:
第一个部分专门处理 i 值的变化
第二个部分专门执行 setTimeout
因此我们可以得到如下代码
1 | var i = 0; |
当执行到第一个 setTimeout 时,javascript 内核检测到这是一个异步函数,所以将这个异步函数移入了消息队列,等待所有主线同步任务的完成后再继续执行。
接着第二个、第三个 setTimeout 异步函数依次执行相同操作。
直到将第 10 个 setTimeout 异步函数加入消息队列后,同步任务执行完成,然后才开始以先进先出的原则调用“消息队列”里面的异步函数。
提升
以上的疑惑解决了之后,我们再来看一个例子:
1 | for (var i = 0; i < 10; i++) { |
这段代码与上面的不同之处是将 1000 的延迟改为了 0!
思考:这段代码会输出什么呢?
随便想了想,上面的有 1000 毫秒的延迟,变量 i 才会瞬间执行到 10,而这里为 0,说明没有延迟,也就是说没有给变量 i 变化的时间,所以答案肯定是
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
我们将这段代码放到浏览器中执行一下得出结果:
几乎同时输出了 10 个 10
有的童鞋可能又要疑惑了,为什么?????
有这样疑问的童鞋建议把原理认认真真再看一遍,还要多吃点上面的‘野栗子’,有助于理解。
为什么 setTimeout 中匿名函数没有形成闭包呢?
原因
因为 setTimeout 中的匿名函数没有将 i 作为参数传入来固定这个变量的值, 让其保留下来, 而是直接引用了外部作用域中的 i, 根据上面的 javascript 机制(先同步,再异步),因此 i 变化时, 也影响到了匿名函数。
因此如果我们定义一个外部函数, 让 i 作为参数传入,即可“闭包”我们要的变量了!!
1 | for (var i = 0; i < 10; i++) { |
提升
1 | for (var i = 0; i < 10; i++) { |
写在最后:真正理解了作用域也就理解了闭包