你不知道的JS读书笔记1—闭包&作用域

编译原理

编译原理,实际上JavaScript是编译语言,只是它在执行前的短暂时间快速的编译。

一般的编译步骤包括:

  • 词法分析:简单地说就是划分单词
  • 语法分析:将这些单词进行分析,转化为“抽象语法树”(AST)
  • 代码生成:将“抽象语法树”变成可执行的代码

在JavaScript执行代码的过程中有三个人的参与:引擎,编译器,作用域。简单地说,

  • 引擎: 负责整个代码的编译与执行过程。
  • 编译器:负责具体的编译工作,包括词法分析,语法分析,代码生词。
  • 作用域:存放变量,负责变量的存放和查找
    比如,var a = 2; 看似很简单的一段代码,执行过程如下。
1. 编译器遇到var a,会在作用域中查找当前作用域是否已经声明了a,如果已经声明就忽略,否则在当前作用域下声明a变量。
2. 编译器为引擎生成好代码 a = 2。
3. 引擎运行时,会查找当前作用域看a是否被声明,如果已经声明就直接执行,否则一直查找直到最外层(作用域的嵌套),如果没有引擎就报错。

查找分为两种,LHS和RHS。LHS查找的是变量的本身,看它是否存在,如果找不到会自动创建一个。RHS找到的是变量的值,如果找不到会跑出ReferenceError的错误。所以在编译环节,分清楚是哪种查找是重要的。

变量提升

由于JavaScript的执行过程,可以看出在引擎执行代码之后,编译器已经把所有存在的变量放入对应的作用域。这也就是所谓的变量提升。需要注意的是,函数提升比变量提升优先。而且,函数声明会整体提升,而函数表达式而只会把前面的变量提升。函数重复声明的处理是覆盖,变量重复声明的处理是忽略。

词法作用域

作用域就是记录和存放变量的一个地方,它严格控制着程序对变量的访问。并且有自己的一套查找规则,向上查找,因为作用域是嵌套的。子作用域可以访问父作用域的变量,反之不可。这就达到了作用域的隔离。

作用域分成动态作用域和词法作用域(静态作用域)。词法作用域在写好代码的时候就确定了变量的值。

作用域的查找是逐步向上的,直到找到第一个匹配的变量。所以变量会有覆盖问题。但对于全局变量,可以直接用window.a 来引用全局作用域下的a变量。

eval和with在运行时会修改作用域,尽量避免使用。使用了它们,也会导致性能问题,因为JavaScript引擎在执行前无从优化。

JavaScript不仅有函数作用域,ES3-5中还有块状作用域。with,try/catch就是块状作用域。

闭包

函数在定义时的词法作用域外进行调用,使得它的父函数即使执行完之后作用域仍然不会被回收,因为需要被这个函数访问到。这样的状态我们称之为闭包。

第一类型函数是指函数可以被当成一般的变量一样,作为一个参数,或者返回值,或者赋值对象。

只要使用了回调函数,实质上就是在使用闭包。

模块模式是经典的闭包使用例子。它满足两个条件,外部函数至少被调用一次,该函数至少返回一个内部函数。

有关闭包的解读可以参考我的另一篇博客。