作用域
用来存储变量并且限制访问变量的一套规则。
编译原理
传统编译语言编译过程中的三个步骤
- 分词/词法分析
例如:var a = 2; 会被分解成词法单元:var、a、=、2、;
- 解析/语法分析
生成抽象语法树(AST),var 是顶节点
- 代码生成
AST 转换为可执行代码
JS 引擎会比这个过程更加复杂,例如,词法分析和代码生成阶段会对运行性能进行优化,包括对冗余元素进行优化。
JS引擎在代码执行前的编译过程
例如:var a = 2,会被分解成两个独立的步骤
var a,在其作用域中声明新变量,这是在最开始的阶段,也可以说是“预编译”阶段。
a = 2,查询(LHS查询)并对 a 进行赋值。
LHS 查询,查询的目的是对变量进行赋值,也可以说是赋值操作的目标。比如 "=" 或者调用函数时传入的参数。RHS 查询,目的是获取变量的值,也可以说是赋值操作的源头。
a = 2; // 对 a 的引用就是 LHS 引用
console.log(a) // RHS 引用
区分 LHS 和 RHS 有什么用?
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 ); // Uncaught ReferenceError: b is not defined
RHS 查询在其作用域链中找不到所需变量,就会抛出 ReferenceError 异常,这个异常同作用域判别失败有关。
function foo(a) {
b = a;
console.log( a + b );
}
foo( 2 ); // 4
LHS 查询,如果在全局作用域也找不到目标变量,会创建一个具有该名称的变量,并返还给引擎(非严格模式)
作用域链
作用域链也可以叫作用域的嵌套。当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。
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、bar、b;
函数 bar 的作用域:有一个标识符 —— c;
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。
作用域查找会在找到第一个匹配的标识符时停止,也就是说,可以允许不同层级作用域的标识符定义成同名的,但只会查找距离自己最近的,这叫“遮蔽效应”。
可以直接访问 window 对象来查找被遮蔽的全局变量,但是如果非全局的变量被同名变量遮蔽,就无法访问到。
作用域分类
全局作用域、函数作用域、块作用域
全局作用域
任何变量,如果变量未经声明就赋值,此变量就为全局对象(window)所有。一切声明过的全局变量也全是 window 的属性。
a = 1;
var b = 2;
console.log(window.a, window.b); // 1,2
函数作用域
属于这个函数的全部变量都可以在整个函数的范围内使用(事实上在嵌套的作用域中也可以使用)。
function foo(a) {
var b = 2;
console.log(a,b,c) // c is not defined
function bar() {
var c = 3;
}
bar()
}
foo(1)
这种设计方案能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性。还可以私有化某些变量或者函数,防止全局作用域暴露过多。
但是,这会导致一些额外的问题。首先, 必须声明一个具名函数 foo(),意味着 foo 这个名称本身“污染”了所在作用域(在这个 例子中是全局作用域)。其次,必须显式地通过函数名(foo())调用这个函数才能运行其 中的代码。
var a = 2;
(function foo(){
var a = 3;
console.log( a ); // 3
})();
比较一下上面两个代码片段。第一个片段中 foo 被绑定在所在作用域中,可以直接通过 foo() 来调用它。第二个片段中 foo 被绑定在函数表达式自身的函数中而不是所在作用域中。
块作用域
一个比较典型的例子
for (var i=0; i<10; i++) {
console.log( i );
}
在 for 循环的头部直接定义了变量 i,通常是因为只想在 for 循环内部的上下文中使 用 i,而忽略了 i 会被绑定在外部作用域(函数或全局)中的事实。这就是没有块作用域的坏处。
1、with
with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。
尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作 用域中。
2、try/catch
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
err 仅存在 catch 分句内部,当试图从别处引用它时会抛出错误。
尽管这个行为已经被标准化,但是当同一个作用域中的两个或多个 catch 分句 用同样的标识符名称声明错误变量时,很多静态检查工具还是会发出警告。
3、let/const
let 和 const 关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环 的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。