阅读 6

JavaScript深入之作用域和闭包

js作用域

作用域定义

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

而实际上来说,作用域就是一套存储变量的规则,用于确定在何处以及如何查找变量。

编译原理

尽管通常将JavaScript称作为 动态解释型 语言,但实际上JavaScript是有编译过程的。

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

理解作用域

作用域相关的核心组件

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

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

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

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

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

  4. 如果引擎最终找到了这个变量a,就会将2赋值给它,如果没有找到则会抛出一个异常。

词法作用域

简单地说,词法作用域就是定义在词法阶段的作用域,也就是说,词法作用域是你在书写代码时就已经决定了的。

看一下以下代码

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;
复制代码

由于闭包占用内存空间,所以要谨慎使用闭包。尽量在使用完闭包后,及时解除引用,以便更早释放内存。

闭包的作用

  • 模块化
  • 保存私有变量
关注下面的标签,发现更多相似文章
评论