作用域与闭包
作用域
编译原理
传统的编译语言中,程序的源代码在执行之前必须经历三个步骤,统称为编译。
- 分词/词法分析(Tokenizing/Lexing)
这个过程将字符串分解成有意义的代码块,成为词法单元(token)。如:var a = 2; 会被分解成 var,a,=,2,;。 - 解析,语法分析(parsing)
这个过程将词法单元流(数组)转换成一个由元素逐级嵌套代表程序语法的树,成为抽象语法树。(AST,Abstract Syntax Tree)。如:var a = 2;的抽象语法树可能有一个叫做VariableDeclaration的顶级节点,然后有一个Identifier(值为a)的子节点以及一个AssignmentExpression的子节点。AssignmentExpression里有包含NumericalLiteral(值为2)的子节点。 - 代码生成
这个过程将AST转化成可执行代码的过程(机器语言)。
作用域
JavaScript编译到运行需要以下程序进行:
- 引擎:从头到尾负责整个JavaScript的编译与运行
- 编译器:负责词法分析与代码生成等工作
- 作用域:负责收集并维护所有声明的标识符(变量)所进行的一系列查询
对于var a = 2;
- 首先编译器会询问作用域是否存在名叫a的变量存在于当前作用域,如果是,则忽略声明,继续编译,如果不是,则要求作用域在当前作用域声明一个名叫a的变量。
- 然后编译器会生成为引擎运行需要的代码,这些代码会用来执行
a = 2
的赋值操作。引擎在执行代码的时候,首先会询问当前作用域是否存在名为a的变量,如果存在,则对此变量进行操作,如果不是,就继续往上一级作用域查找,直到全局作用域。
如果在全局作用域也没有找到,(非严格模式下RHS方式会抛出会抛出一个Reference异常,LHS方式会在全局作用域返回一个新的名叫a的变量;严格模式下,二者都会返回一个Reference异常)
总结:变量赋值执行两个操作,首先编译器会在当前作用域声明一个新的变量,然后引擎在执行的时候查找该变量。如果能够找到就对它进行赋值。
LHS 与 RHS
引擎查找变量有两种方式,一种为:LHS,另一种为RHS
LHS与RHS的含义为赋值操作符的左侧与右侧,但是其真正含义为赋值操作的目标(LHS)与赋值操作的源头(value)(RHS)
二者区别
如果变量还没有声明(在所有作用域都无法查找到该变量),这两种行为是不一样的。
如下代码:
1 | function foo(a) { |
当执行到a + b
时,第一次对b进行RHS查询是无法查询到该变量的,就是说这是一个未声明的变量,所以引擎会抛出ReferenceError异常。
相较之下,c = a
当引擎通过LHS查询c的时候,在顶层作用域(全局作用域)无法找到该变量,全局作用域就会创建一个名为c的变量并返回引擎。
在严格模式下,通过LHS查询失败并不会返回一个新的变量,而是抛出ReferenceError异常。
词法作用域
词法作用域就是定义在词法阶段的作用域,是由书写时函数声明的位置决定的。
值得注意的是:JavaScript只有词法作用域,并没有动态作用域。这以为着下面的代码运行结果是3而不是2
1 | let a = 3; |
函数作用域
块作用域
提升
包括变量和函数在内的所有声明都会在任何代码执行之前被首先处理。
提升是提升到当前作用域的上方,而不是全局作用域的上方。
函数声明会被提升,但是函数表达式不会被提升。
对于以下代码:
1 | foo(); //TypeError 而不是ReferenceError |
函数优先
函数声明和变量声明都会被提升,但是函数声明会被优先提升。(即如果同时存在函数声明和变量声明,函数声明会覆盖变量声明)
作用域闭包
当函数记住并访问所在词法作用域时,便产生了闭包,即使函数在当前词法作用域之外执行。
简单的闭包:
1 | function foo() { |
- 回调函数
- 模块