上篇文章 在介绍 Javascript 闭包的时候提到了“闭包创建时所处的环境信息”,但是并没有说明这些信息到底是什么。
也多亏了读者的提醒,我对于 Js 闭包的理解还是太肤浅了。这篇文章除了介绍 Js 的作用域和作用域链外,我还会讨论变量提升(var hositing) 这个问题。
在 Javascript 中,只有局部作用域和全局作用域。而只有函数可以创建局部作用域,像 if,for 或者 while 这种块语句是没办法创建作用域的。 (当然 ES6 提供了 let 关键字可以创建块作用域。)
Javascript 的这种特性导致 for 循环里面创建闭包时会产生让人意想不到的结果。比如下面这个例子:
var i = 20; var makeLogger = function() { var funcs = []; for(var i = 0; i < 10; ++i){ funcs[i] = function() { console.log(i); } } return funcs; } var loggers = makeLogger(); for(var i = 0; i < 10; ++i){ loggers[i](); }
RESULTS:
上面的输出结果,大致原因就是 for 循环里面的变量的作用域是整个函数的,循环内部创建的一系列闭包引用的是同一个变量 i,而在 for 循环结束后,这个 i 的值变成了 10。所以当我们调用这些内部函数的时候,就会输出 10 了。
现在这样讲可能还是不够清楚,在我们了解作用域链和 Javascript 的执行原理后,就更容易理解了。
让我们来看几个具体的例子:
var name = 'zilongshanren'; function echo() { console.log(name); var name = 'hello'; console.log(name); } echo();
RESULTS:
undefined hello
要理解上面的代码的输出结果,我们可以按照上面提到的 4 点来解释:
[[scope chain]] = [ { global Object: { name: 'zilongshanren' ... } } ]
echo 函数的作用域属性指向此 scope chain 对象。
[[scope chain]] = [ { Active Object { name: undefined, arguments: ... ... }, global Object: { name: 'zilongshanren' ... } } ]
这个例子可能比较简单,因为它没有使用闭包。
我们接下来分解一下本文开头的例子。
[[scope chain]] = [ { global Object: { i: 20, ... } } ]
[[scope chain]] = [ { makeLogger local scope object : { i: undefined, funcs: [], }, global Object: { i: 20, ... } } ]
并且此时 funcs…funcs的 scope 都指向该 scope chain。
[[scope chain]] = [ { makeLogger active object: { funcs: undefined, i: undefined, arguments: ... }, global Object: { i: 20, ... } } ]
[[scope chain]] = [ { makeLogger local scope object : { i: undefined, funcs: [function object ...], }, global Object: { i: 20, ... } } ]
这里的 funcs 函数还会生成闭包对象,它包含了 makeLogger 局部作用域的变量的值,即 i=10.
下图是 V8 引擎中 funcs 函数及其闭包的截图:
[[scope chain]] = [ { loggers function active object : { arguments: ... }, makeLogger local scope object : { i: 10, funcs: [function object ...], }, global Object: { i: 20, ... } } ]
当执行 loggers 函数的 console.log(i)的时候,它会沿着此时的作用域链进行变量查找,于是找到了 i=10. 所以我们输出的结果就是 10.
我们看一个例子:
var name = 'zilongshanren'; function echo() { name = "hello"; console.log(name); var name; console.log(name); } console.log(name); echo();
+RESULTS:
zilongshanren hello hello undefined
调用 echo 函数的第一行 name = "hello"时并不是对全局变量 name 进行重新赋值,而是对函数内部声明的变量 name 进行赋值。所以,在 echo 函数声明之后,调用 console.log(name)输出的还是 zilongshanren。
echo 函数内部的 name 变量“使用在前,而声明在后”,这就是所谓的变量提升。
如果从我们前面提到的变量作用域和作用域链来解释这个行为肯定是更容易理解的。
正因为函数内部的变量声明会发生“提升”副作用,所以,最好的做法就是把函数需要用到的局部变量都放在函数开头进行声明,避免产生不必要的混淆。
JavaScript 中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。理解作用域和作用域链对于理解闭包和变量提升这种奇葩特性非常有帮助。 本文可能有些地方讲的还不是非常清楚,读者可以读一读后面的参考链接,相信会有助于理解。