我所知道的JavaScript作用域

说到作用域我们首先要讨论一下执行环境。执行环境(ExecutionContext)定义了变量或函数有权访问的其他数据,决定了它们的行为。 与之相关的变量对象(Variable Object),环境中定义的所有变量和函数都保存在这个对象中,虽然我们无法访问这个对象,但解析器在处理数据时会在后台使用它。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是大年执行的代码所在环境的变量,如果执行环境是函数的话,那么它的活动对象就是变量对象,活动对象(activeObject)在最开始只包含一个对象-arguments对象(该对象在全局环境中不存在的 ),而下一个变量对象来自外部环境,再下一个变量对象来自下一个外部环境,这样一直延续到全局执行环境,全局执行环境的变量对象始终都只是作用域链中的最后一个对象。

JavaScript的作用域接近于词法作用域(因为有eval和with,可以欺骗作用域),词法作用域就是定义在词法阶段的作用域,换句话来说,词法作用域是由你写代码将变量和块作用域写在哪里决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var global = 1;
function fn(param1){
var local1 = 'local1';
var local2 = 'local2';
function fn1(param2){
var local2 = 'hello local2';
console.log(local1); //local1
console.log(local2); //hello local2
};

function fn2(){
var local2 = 'my local2';
fn2(local2); //my local2
};
console.log(local2); //local2
console.log(global); //1
};

当浏览器拿到一段代码的时候不会直接去执行它。而是会将这段代码抽象称语法树,在确定没有问题的情况下再去执行它。具体为以下两个步骤:

  1. javascript预编译:就是通过语法分析和预解析构造合法的语法分析树,读取变量和函数的声明,并确定其作用域即生效范围。
  2. javascript执行:执行具体的代码,JavaScript引擎在执行每个函数实例时,都会创建一个执行环境和活动对象(它们属于宿主对象,与函数实例的生命周期保持一致)

而通过上面一段代码的执行,我们不难发现关于作用域的这几个规则:

  1. 在对变量进行查找时,是由作用域的最内部开始逐级而上进行查询,这个过程会一直到找到标识符为止,如果找不到标识符,通常会导致错误发生。
  2. 内部作用域的变量对外层的作用域有遮蔽效应,即函数内部的同名变量或参数其优先级高于全局同名变量。(如local2)
  3. 局部变量的作用域仅在于函数内部,外部不能访问函数内部的变量。

要注意的是某个执行环境中的所有代码都执行完毕的时候,该环境会被销毁,保存其中的所有变量和函数也会被销毁(全局执行环境会在应用程序关闭后再销毁)

块级作用域

在早期的ECMAScript中,并没有块级作用域,但ES6开始引进了块级作用域,关于块级作用域的最主要的用法便是下文的let和const。具体表现如下:

1
2
3
4
5
6
7
8
if (true) {
let a = 2;
const b = 3;
a = 3;
b = 4;// TypeError: Assignment to constant variable
}
console.log( a ); // ReferenceError: a is not defined
console.log( b ); // ReferenceError: b is not defined

如上所示,在块级作用域内使用let和const声明的变量在块级作用域外不能被访问,且const定义的变量是常量,不能被改变。