什么是作用域
编译原理
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
-
分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序 var a = 2;。这段程序通常会被分解成为下面这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。
分词(tokenizing)和词法分析(Lexing)之间的区别是非常微妙、晦涩的,主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。简单来说,如果词法单元生成器在判断 a 是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就被称为词法分析。
-
解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下来是一个叫作 Identifier(它的值是 a)的子节点,以及一个叫作 AssignmentExpression的子节点。AssignmentExpression 节点有一个叫作 NumericLiteral(它的值是 2)的子节点。
-
代码生成
将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。抛开具体细节,简单来说就是有某种方法可以将 var a = 2;的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
这段解释来源于《你不知道的JavaScript》。
了解这三个步骤对于我们理解接下来的作用域有很大的帮助。
什么是作用域
定义:
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限----《你不知道的JavaScript》。
- 引擎
从头到尾负责整个 JavaScript 程序的编译及执行过程。 - 编译器
负责语法分析及代码生成等脏活累活。 - 作用域
负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限(《你不知道的JavaScript》)。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值.
而 JavaScript高级程序设计 一书中对于作用域(执行环境)的定义如下: 执行环境: 执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。《JavaScript高级程序设计》
全局执行环境:全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也不一样。在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。
函数执行环境:每个函数都有自己的执行环境。当执行流进入到函数时,会把这个函数的执行环境压入执行栈中,等到执行结束后,再从栈中弹出,此时再把执行权交给外层的执行环境。
作用域链: 当代码在一个执行环境中执行时,会创建一个变量的作用域链,来保证执行环境对所有变量和函数的有序访问。作用域的前端,始终是当前执行环境的变量对象,作用域的后端,始终是全局变量对象(作用域嵌套意思一样)。
var color = "blue";
function changeColor () {
var anotherColor = "red";
function swapColors () {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor; // 这里可以访问 color、anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor,但不能访问 tempColor
swapColors();
}
// 这里只能访问 color
changeColor();
分析:以上共存在三个执行环境:
全局执行环境: 变量color、函数changeColor()
changeColor局部环境:变量anotherColor、函数swapColors(),可以访问的是变量color、anotherColor
swapColors局部环境: 变量tempColor,可以访问的是变量color、anotherColor、tempColor
函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。
词法作用域
定义:
词法作用域:词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar(); // 2
分析:bar调用,bar里面foo被调用,foo函数需要查找变量a.
由于javascript采用词法作用域,foo被解析的时候是在全局作用域,所以a是全局作用域中的2,而非bar里面的a。假设js采用的是动态作用域,foo是在bar中被调用的,所以a查找到了bar作用域里的3。
欺骗词法
with
with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作用域中。
function buildUrl () {
var qs = "? debug = true";
with (location) {
var url = href + qs;
}
return url;
}
eval()
JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。个人觉得,相当于在eval()的位置又重新把这个字符串变量声明了,所以原本属于父级执行环境的变量在当前执行环境中也可以找到。
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
块级作用域
块级作用域:从ES6开始,JavaScript具有了块级作用域。 为什么要有块级作用域?1、防止内层变量修改外层变量。2、用来计数的循环变量泄漏为全局变量。
var a = 1;
function f() {
console.log(a);
if (false) {
var a = 2;
}
}
f(); // undefined
分析:这段代码的本意是if外使用全局变量,内部使用内层的a变量。但是由于变量提升,最后输出了undefined。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
分析:变量i只是为了控制循环,但是泄漏成了全局变量,如果另外一个for循环继续使用i变量控制循环,就会出错。
代码块:在ES6标准中,用{}包裹的代码。块级作用域是可以嵌套的。
- with:with语句也会常见一个代码块。
- try/catch():catch分句会创建一个块级作用域。
- let、const:ES6中规定,let、const声明的变量在未定义前都不能使用。
作用域提升
- 变量和函数都可以提升。
console.log(a); // undefined
var a = 1;
function foo () {
console.log(b); // undefined
var b = 2;
return a + b; // 3
}
foo()
- 函数声明的函数体可以提升,但是函数表达式不能提升。
console.log(a); // undefined
console.log(foo);// foo(){...}
console.log(bar); // undefined
var a = 1;
function foo () {
console.log(b); // undefined
var b = 2;
return a + b; // 3
}
var bar = function bar () {
console.log(d); // undefined
var d = 2;
return a + d; // 3
}
foo()
bar()
- 函数的提升要先于变量的提升。
foo(); // 1
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 1 );
}
- 用let、const声明的变量不会被提升,let和const声明的变量存在“暂时性死区”:在变量未被声明之前,不能使用该变量。