问题
Javascript
到处充满着神(sha)奇(bi)的地方。
面试中经常被问到的一个题目:
1 | for (a = 0; a < 5; a++) { |
此处,变量 a
未声明即使用,在 js 中不会出现语法错误。详细的解释可以参考 你听说过「变量提升」吗? 。
js 中的丰富的异步编程,使其简单优雅而高效。但也不免带来了理解上的成本。这个示例中,setTimeout
便是一个常见的异步延迟方法,即使延迟时间是 0。
然我们从头来看一遍这个代码,主脚本程序执行 for
循环,由于 javascript解释器
的 event loop
机制。setTimeout
作为 消息
被添加到消息队列,待当前任务(即主脚本程序)执行完成后,才会处理消息队列中的其他任务。详细内容可以查看 并发模型与事件循环。
所以,等到 setTimeout
要执行时,当前函数帧的变量 a
已经被赋值为 5
, 故会输出 5。
如果我们想要实现输出 0 1 2 3 4
,有下面几种方法:
- 使用 ES6 中的
let
。
1 | for (let a = 0; a < 5; a++) { |
- 使用闭包
这么半天终于遇到闭包了。闭包是有权访问另一个函数作用域中变量的函数。,修改代码如下:
1 | for (a = 0; a < 5; a++) { |
这里,先构建了一个匿名函数,将 a
作为参数传入。setTimeout
的回调函数就形成了闭包,其可以访问匿名函数作用域中的 b
。每次执行 for
循环,即形成一个匿名函数作用域,而这个作用域中的变量 b
则是根据传入的形参 a
所确定。所以就正常的返回了 0 1 2 3 4
。
闭包
要理解闭包,首先我们要了解 js 的作用域链。js 中,当一个函数被调用,会创建一个执行环境及相应的作用域链,然后使用 arguments
和其他命名参数值的来初始化函数的活动对象。正常情况下,我们在函数中访问一个变量时,就会从该函数的作用域链中搜索具有相应名字的变量,函数执行完毕后,局部变量会被销毁,内存中仅保留全局作用域。但是,闭包的情况又有所不同。
闭包的作用域链不仅包含函数作用域和全局作用域,还包括所能访问的父函数的函数作用域。这时候,父函数执行完毕,并不会销毁其函数作用域,因为还被闭包所引用。除非等到闭包被销毁后,父函数作用域也会被销毁。
这其实也不难理解,js 的函数式编程运行函数执行后,返回另一个函数。这在很多框架中被广泛应用。
1 | function a() { |
如上代码,函数 a 返回一个匿名函数,当 a()
执行后,其实 a
是将其作用域传递给匿名函数。匿名函数当然也就可以返回 value
。待 b = null;
被销毁后,a 也就被销毁。
闭包的优缺点
保护函数内的变量安全,加强了封装性。但是闭包会增加内存的占用,有内存泄漏的风险。