JavaScript深刻之做用域和闭包

js做用域

做用域定义

《JavaScript权威指南》中讲到:一个变量的做用域是程序源代码中定义这个变量的区域。数据结构

而实际上来讲,做用域就是一套存储变量的规则,用于肯定在何处以及如何查找变量。闭包

编译原理

尽管一般将JavaScript称做为 动态解释型 语言,但实际上JavaScript是有编译过程的。模块化

  • 词法分析(lexical analysics):将代码分解成词法单元
  • 语法分析(parsing):将代码整理成 抽象语法树(AST)
  • 代码生成:将AST转化成可执行代码

理解做用域

做用域相关的核心组件

  • 引擎:负责整个JavaScript程序的编译和执行过程
  • 编译器:负责整个编译过程
  • 做用域
    • 负责收集变量声明
    • 提供变量声明查询
    • 控制代码对变量的访问权限

当看到 var a = 2,这段程序时,咱们来看看编译器和引擎都作了什么?函数

  1. 首先编译器会在当前做用域中查询是否已经有一个名为a的变零存在于同一个做用域的集合中,若是有,则忽略该声明,继续进行编译,若是没有,则会要求做用域在当前的做用域集合中声明一个变量a。ui

  2. 编译器将 var a = 2 这个代码片断编译成用于执行的机器指令spa

  3. 引擎在运行时会首先在当前做用域中查找是否存在变量a,若是不存在则继续像上一级查找,若是存在,则引擎会使用这个变量设计

  4. 若是引擎最终找到了这个变量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();
复制代码

具体的提高规则以下:

  • 函数声明会被优先提高
  • 重复的var声明会被忽略掉
  • 后声明的函数会覆盖掉先声明的函数
  • 函数的声明会被提高到当前做用域顶部,不收代码逻辑控制

做用域链

当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套。所以引擎在查找变量时,会先从当前的执行做用域开始查找,若是找不到,就向上一级继续查找,当到达最外层的全局做用域时,不管找到仍是没找到,查找过程就会中止。像这样由多个执行上下文的变量对象构成的链式结构就叫作做用域链。

闭包

闭包的定义

闭包的定义有不少种,可是通常能够分为两类

  • 一种说法是闭包是符合必定条件的函数。好比《JavaScript⾼级程序设计》是这样定义的:闭包是指有权访问另外一个函数做用域中的变量的函数。

  • 另外一种说法是闭包是由函数以及和它相关的引用环境组合而成的实体。好比MDN中是这样定义的:闭包是函数和声明该函数的词法环境的组合。

这两种说法在某种意义上实际上是对立的,一个认为闭包是函数,另外一个认为闭包是函数和引用环境组成的总体。但其实第二种的表达看起来更确切点,MDN稍早以前给出的闭包定义是: 闭包是那些可以访问自由变量(自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。)的函数,这种说法是比较结交接近于第一种说法的,可是后来的MDN定义却改为了上面的第二种说法。

函数只是一些可执行的代码,而闭包的本质来源于两点:

  • 词法做用域:函数外部的代码没法访问函数体内部的变量,而函数体内部的代码能够访问函数外部的变量。
  • 函数当作值传递:即所谓的函数是一等公民(First-class value)。函数能够做为做为另外一个函数的返回值和参数。原本执行过程和词法做用域是封闭的,这种返回的函数就比如是一个通道,这个通道能够访问这个函数词法做用域中的变量,即函数所须要的数据结构保存了下来,这‘通道’说的就是内部函数对词法做用域的引用。

即便函数已经执行完毕,在执行期间建立的变量也不会被销毁,所以每运行一次函数就会在内存中留下一组变量。(js固然会有垃圾回收机制,不过若是它发现你正在使用闭包,则不会清理可能会用到的变量)

因此咱们概括一下,就是关于一个函数要成为一个闭包到底须要满意几个条件:

  • 函数嵌套,即基于词法做用域的查找规则
  • 将内部函数做为值返回
  • 在所在做用域外被调用

下面代码就是一个典型的闭包

function foo(){
    var a = 2;
    function bar(){
        console.log(a);//2
    }
    return bar;
}
var baz = foo();
baz(); 
baz = null;
复制代码

因为闭包占用内存空间,因此要谨慎使用闭包。尽可能在使用完闭包后,及时解除引用,以便更早释放内存。

闭包的做用

  • 模块化
  • 保存私有变量
相关文章
相关标签/搜索