作用域的概念
存储变量中的值,并能对其进行访问和修改,是几乎所有的编程语言最基本的功能之一。而正是这种功能是程序具有了状态。那些随之而来就有很多问题了,这些变量如何存储,程序如何访问和修改。如此一来,就需要一系列的规则来指明如何存储变量,如何更方便的寻找访问变量。我们可以将这一系列的规则称为作用域。然而,在我看来,我们通常所说的一个个作用域,可以认为是包含了变量的存储访问等一系列规则的变量的集合。
JavaScript编译原理
传统的编译语言的流程中,一般可以分为三个步骤:分词/词法分析(分解为词法单元)、解析/语法分析(构建抽象语法树)、代码生成(生成可执行代码)。与传统的编译语言不同,JavaScript的编译过程不发生在构建之前,而是通常发生在代码执行前的几微妙时间内(也可称为预编译)。故,JavaScript常称为动态解释性语言。
JavaScript的处理
在处理过程中,涉及到的有引擎、编译器和作用域。
- 引擎:负责整个JS程序的编译和执行过程
- 编译器:负责语法分析及代码生成等dirty work
- 作用域:负责收集和维护变量组成的一系列查询(值查询RHS、位置查询LHS),并通过一套严格的规则,确定当前执行的代码对这些变量的访问权限。
JavaScript处理实例
1 | function foo(a) { |
在上述代码中,编译器先将函数foo的定义放入作用域中;在执行foo(2)时,通过RHS查询foo变量的值,并执行;然后,对参数a进行LHS查询其位置,并将2赋值给a;通过RHS查询console变量,及其子变量log;通过RHS查询a的值,并运行。
- 在JS执行前会先进行预编译,此时会提取var定义的变量(不进行赋值操作,故都为undefined)和函数声明,将其加到作用域。然后进行赋值操作、函数执行、条件判断等。
- LHS为左查询,获取其位置,进行赋值操作。RHS为右查询,获取变量的值。
- 在作用域中,定义一个变量var a时,会先在当前作用域中查找是否存在,存在则忽略,否则新建。
- 在进行LHS时,如果在全局作用域中仍然无法找到:“严格模式”下,抛出ReferenceError,“非严格模式”下,在全局作用域中新建这个全局变量。
- 在进行RHS时,当前作用域中找不到,则去上一层作用域找,直到在全局作用域中也找不到时,抛出ReferenceError。
- ReferenceError同作用域判断失败有关。如果查到到相应的值,但是在其值上进行了不合理的操作,如引用null类型的值的属性,会抛出TypeError错误。
词法作用域
作用域有两种主要的工作模式,第一种为比较普遍的词法作用域,另一种为动态作用域(如bash脚本等)。JavaScript采用此法作用域。词法作用域意味着作用域由书写代码时函数声明的位置来决定,即JavaScript的嵌套作用域机制。
词法作用域是由写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。然而使用eval和with可以欺骗词法(但这两种方法都对性能不好,轻易不要使用)。如:
1 | function foo(str, a) { |
函数作用域与块作用域
在JavaScript中,函数是最常见的作用域单元。从ES3开始,try/catch结构在catch语句中具有块级作用域。另外,with也是块级作用域的一个例子。但是目前在for选好语句、if/else语句、while语句等控制语句中,任然不存在块级作用域,但是可以使用let声明在这些控制语句中声明块级变量。因为let在ES6以上版本中才可以使用,所以要使用let,就要对代码进行转换,从而使其可以转换为与let效果相同的可以在ES5中运行的代码(使用try/catch)。
闭包
1 | for (var i=1; i<=5; i++) { |
上面的例子为经常出现的问题。ES6的let可以更好的解决这个问题。在上面的例子中,timer()依然持有对该作用域的引用,而这个引用就称为闭包,如果作用域中还有这种引用,那么这个作用域包含其外层作用域就不会被回收。所以,在闭包使用完成后,要对闭包进行回收,就要将其设置为null引用。
根据《你不知道的JS》一书的定义,Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
用任何形式的函数类型的值进行传递,且该函数在别处调用时就会产生闭包。另外,在定时器、事件监听器、AJAX请求等任务中,只要使用了回调函数,其实就是在使用闭包。
模块
1 | //现代的模块机制 |