JavaScript 作用域与作用域链

与C、Java等语言不同,JavaScript的作用域不是以花括号包围的块级作用域,而是函数决定的块级作用域。

预编译

JS是一种脚本语言, JS的执行过程, 是一种翻译执行的过程。

但是,在JS中是有预编译的过程的。JS 在执行每一段代码之前,都会首先处理 var 关键字和 function 定义式。在调用函数执行之前,会首先创建一个活动对象,然后搜寻这个函数中的局部变量定义和函数定义。将变量名和函数名都做为这个活动对象的同名属性。对于局部变量定义,变量的值会在真正执行的时候才计算,此时只是简单的赋为 undefined

对于函数的定义,有一个要注意的地方:对于函数定义式,会将函数定义提前,而函数表达式,会在执行过程中才计算。

1
2
3
4
5
6
7
8
9
console.log(typeof a); // function
console.log(typeof b); // undefined
function a() { // 函数定义式
console.log('hello');
};
var b = function() { // 函数表达式
...
}
console.log(typeof a); // function

作用域链

任何执行上下文时刻的作用域,都是由作用域链来实现。

在一个函数被定义的时候,会将它定义时刻的 scope chain 链接到这个函数对象的 [[scope]] 属性。

在一个函数对象被调用的时候,会创建一个活动对象, 然后对于每一个函数的形参,都命名为该活动对象的同名属性。 然后将这个活动对象做为此时的作用域链最前端。具体可参见 理解 JavaScript 作用域和作用域链 一文中的图解。

有了上面的作用域链,在发生标识符解析的时候,就会从链表头依次查询当前 scope chain 列表的每一个活动对象的属性,如果找到同名的就返回。找不到,那就是这个标识符没有被定义。

看下面这个栗子:

1
2
3
4
5
6
7
var x = "globol value";
var test = function(){
alert(x); //弹出"undefined"
var x = "local value";
alert(x); //弹出"local value";
}
test();

解释:执行 test() 时,形成的作用域链为 调用对象->全局对象
执行 alert(x) 时,首先查找调用对象是否有 x 的属性,如果有则使用 调用对象 的 x ,如果没有,就接着查找 全局对象 是否有 x 的属性。在本例中,调用对象有 x,但因为 js预编译 的变量提升机制,会将局部变量 x 的声明提前,因此第一个 alert(x)undefined

从作用域链的结构可以看出,全局变量永远处于作用域链的最末端,因此它的查找速度最慢,应尽可能使用局部变量。

作用域的嵌套

JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。

1
2
3
4
5
6
7
8
9
var scope = 'global';
var f1 = function() {
console.log(scope);
}
var f2 = function() {
var scope = 'f2'; // 局部变量
f1();
}
f2(); // 输出 global

解释:函数作用域的嵌套关系是在定义时决定的,而不是在调用时决定的。通过 f2 调用的 f1 在搜索 scope 时,找到的是 f1 函数的父作用域中定义的 scope 变量,而不是 f2 中定义的 scope 变量。

不过,如果 f2 中的 scope 变量前不加 var ,就会是另一种结果了(输出 f2 ),因为此时 scope 是全局变量。

参考

  1. Javascript作用域原理
  2. JavaScript 开发进阶:理解 JavaScript 作用域和作用域链
  3. JavaScript作用域
  4. javascript基础拾遗——词法作用域
  5. Js作用域与作用域链详解
坚持原创技术分享,您的支持将鼓励我继续创作!