《JavaScript权威指南》中讲到:一个变量的做用域是程序源代码中定义这个变量的区域。数据结构
而实际上来讲,做用域就是一套存储变量的规则,用于肯定在何处以及如何查找变量。闭包
尽管一般将JavaScript称做为 动态 或 解释型 语言,但实际上JavaScript是有编译过程的。模块化
当看到 var a = 2
,这段程序时,咱们来看看编译器和引擎都作了什么?函数
首先编译器会在当前做用域中查询是否已经有一个名为a的变零存在于同一个做用域的集合中,若是有,则忽略该声明,继续进行编译,若是没有,则会要求做用域在当前的做用域集合中声明一个变量a。ui
编译器将 var a = 2
这个代码片断编译成用于执行的机器指令spa
引擎在运行时会首先在当前做用域中查找是否存在变量a,若是不存在则继续像上一级查找,若是存在,则引擎会使用这个变量设计
若是引擎最终找到了这个变量a,就会将2赋值给它,若是没有找到则会抛出一个异常。3d
简单地说,词法做用域就是定义在词法阶段的做用域,也就是说,词法做用域是你在书写代码时就已经决定了的。code
看一下如下代码cdn
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
复制代码
在这个例子中有三个嵌套的做用域:
① 包含着整个全局做用域,只有一个标识符:foo ② 包含着 foo 函数所建立的做用域,有三个标识符:a、b、bar ③ 包含着 bar 函数所建立的做用域,有一个标识符:c
做用域气泡由其对应的做用域块代码写在哪里决定,它们是逐级包含的。
做用域的分类
先看一段代码:
var a;
a = 2;
console.log( a );
复制代码
执行结果是 2。
实际上JavaScript代码在执行时候并非一行一行的执行的,引擎在执行代码以前会首先对其进行编译,包括变量和函数在内的全部声明都会在任何代码被执行前被‘移动’到最上面,这就叫作变量提高。
再来看一个例子:
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
复制代码
值得注意的是,每一个做用域都会进行提高操做。只有声明自己会被提高,而赋值或其余运行逻辑仍会留在原地。
所以上面那段代码能够被理解为下面的形式:
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
复制代码
具体的提高规则以下:
当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套。所以引擎在查找变量时,会先从当前的执行做用域开始查找,若是找不到,就向上一级继续查找,当到达最外层的全局做用域时,不管找到仍是没找到,查找过程就会中止。像这样由多个执行上下文的变量对象构成的链式结构就叫作做用域链。
闭包的定义有不少种,可是通常能够分为两类
一种说法是闭包是符合必定条件的函数。好比《JavaScript⾼级程序设计》是这样定义的:闭包是指有权访问另外一个函数做用域中的变量的函数。
另外一种说法是闭包是由函数以及和它相关的引用环境组合而成的实体。好比MDN中是这样定义的:闭包是函数和声明该函数的词法环境的组合。
这两种说法在某种意义上实际上是对立的,一个认为闭包是函数,另外一个认为闭包是函数和引用环境组成的总体。但其实第二种的表达看起来更确切点,MDN稍早以前给出的闭包定义是: 闭包是那些可以访问自由变量(自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。)的函数,这种说法是比较结交接近于第一种说法的,可是后来的MDN定义却改为了上面的第二种说法。
函数只是一些可执行的代码,而闭包的本质来源于两点:
即便函数已经执行完毕,在执行期间建立的变量也不会被销毁,所以每运行一次函数就会在内存中留下一组变量。(js固然会有垃圾回收机制,不过若是它发现你正在使用闭包,则不会清理可能会用到的变量)
因此咱们概括一下,就是关于一个函数要成为一个闭包到底须要满意几个条件:
下面代码就是一个典型的闭包
function foo(){
var a = 2;
function bar(){
console.log(a);//2
}
return bar;
}
var baz = foo();
baz();
baz = null;
复制代码
因为闭包占用内存空间,因此要谨慎使用闭包。尽可能在使用完闭包后,及时解除引用,以便更早释放内存。