前言
相信很多JavaScript开发者都使用过闭包这一技术,也相信很多开发者都使用的糊里糊涂的。
下面说一下我的理解。
作用域
当JavaScript运行的时候,它需要一套规则来管理如何存储变量,并且能在之后如何访问到这些变量。这套规则就是作用域。(在JavaScript代码是怎么执行的? (opens new window)中讲过一点,在这就不复述了,详情请看文章)
作用域主要有两种工作模式:
- 词法作用域
- 动态作用域
JavaScript所采用的是词法作用域。
词法作用域
顾名思义:就是定义在词法阶段的作用域。简单来说,词法作用域是由你将这些变量和块作用域写在哪所决定的。
请看下面例子:
function foo (b) {
function bar (c) {
console.log(b + c);
}
bar (2)
}
foo(1)
2
3
4
5
6
7
在这个例子中有三层嵌套的作用域:
- 第一层包含着全局作用域,只有foo
- 第二层包含着foo所创建的作用域,有b、bar
- 第三层包含着bar所创建的作用域,有c
作用域由其对应的代码写在哪所决定的,逐级包含。
作用域链
引擎主要是利用作用域来查找变量的位置。在上面例子中,当执行到bar函数中的console.log(b+c)时,要找到变量b和c。
首先从bar的作用域找起,在当前作用域中无法找到b时,就到上一级作用域去找即foo的作用域。如此推理,作用域查找会逐级向外查找直到找到第一个匹配或者再没有父作用域才停止。我们将这个查找变量的过程中所经过的作用域称为作用域链(Scope chain)
作用域最外层就是全局作用域,在全局作用域中都没找到就抛出一个错误。幸好,b在foo中找到了,c在bar中找到了。
具体到底层中,执行上下文被创建时会有一个内部属性[[scope]]。[[scope]]所指向的就是定义该函数所在的作用域中。
比如说:在上面例子中执行到foo(1),执行上下文关系是这样的:
当foo的执行上下文被创建时,其中内部属性[[scope]]指向的是foo函数所定义的执行上下文中--也就是全局执行上下文。
现在就拥有一个作用域链。当试图在foo中访问某些变量时,JavaScript引擎会先在其作用域(foo())中查找这个属性。如果找不到,那么就在它的父作用域中查找(全局作用域)。在全局作用中都没找到的话,那就抛出一个ReferenceError。
只要这些作用域被引用,它们就不会被垃圾回收器销毁,就能一直访问它们。没有引用这些作用域时,就说明它们可以被回收了。
闭包
闭包是什么?闭包在MDN是这样定义的:
函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。
闭包的产生
而闭包的产生在《你不知道的JavaScript》中也有详细介绍:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数时在当前词法作用域之外执行。
举书的例子介绍
function foo() {
var a = 2;
function bar () {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 这就是闭包
2
3
4
5
6
7
8
9
函数bar()的词法作用域能够访问foo()的内部作用域。然后将bar()函数本身当作一个值类型进行传递。
在foo()执行后,其返回值(也就是内部的bar())赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()
bar()显然可以被正常执行。它在定义的词法作用域以外的地方执行。
foo()返回后并没有被回收,因为bar()依然持有对该作用域的引用,而这个引用就是闭包。
深入到底层原理中,当执行到foo()时,执行上下文的关系是这样的:
注意bar函数存在有指向foo()的引用。如果foo()没有任何返回值,那么foo()不再被引用,于是就可以被垃圾回收器回收。但是foo()实际上是有返回值的,并且返回值被存储在baz()中,所以引用关系变成了如下图所示:
baz()的调用创建了一个执行上下文,当这个函数尝试访问a时,JavaScript引擎无法在当前作用域找到它时,于是就会在其父作用域foo()中查找。
可以注意到,在foo函数之外,除了被返回的bar,没有其他地方可以访问到a这个变量了。这就是用闭包实现“私有变量”的方法。
需要注意的是作用域链是不会被复制的。每次函数调用只会往作用域链下面新增一个作用域。所以,如果在函数调用的过程中对作用域链中的任何一个作用域的变量进行修改的话,那么同时作用域链中也拥有该作用域的函数也是能够访问到这个变化后的变量的
这也就是为什么下面这个大家都很熟悉的例子会不能产生我们想要的结果
for (var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
2
3
4
5