引言
在我们开始讨论JavaScript的作用域之前,先来看一句代码:
var a = 1;
这是一句非常简单的代码,但我想问的是,你是如何理解这句话的?声明了一个值为1的全局变量a?
我们今天将以这句话开始,一步步带你了解JavaScript的执行机制以及作用域。
引擎、编译器与作用域
为了解释var a = 1;
这句话,我们先来了解一下引擎、编译器与作用域,JS的执行正是依赖于这三者之间默契的协调配合。让我们用公司成员来比喻这三者之间的关系。
CEO (首席执行官) - 引擎
CEO 负责公司的整体运营,确保公司的目标能够达成。同样地, 引擎作为整个 JavaScript 执行环境的核心,它的主要职责是确保所有 JS 代码能够正确无误地被执行。这包括解析代码、管理内存、处理异常等。引擎需要高效地处理各种复杂的逻辑和数据,负责整体工作。
CTO (首席技术官) - 编译器
CTO 负责公司的技术创新和技术方向,确保公司采用最前沿的技术来保持竞争力。编译器就扮演着类似的角色。它负责将高级语言(如 JS 源代码)转化为更底层的语言(如机器码),使计算机能够理解和执行。
COO (首席运营官) - 作用域
COO 负责公司的日常运营,确保各个部门之间协调工作,资源得到有效利用。在 JS 中,作用域系统就像是公司的运营体系,它管理着变量和函数的生命周期,决定了它们何时何地可以被访问。
好了,现在我们能解释JS眼中的var a = 1;
了
在JS中,代码的执行过程可以分为两个主要阶段:编译阶段
和执行阶段
。
- 编译阶段(
var a
)
引擎先把 var a = 1;
分解成词法单元,v a r a = 1 ;
(词法分析)。
编译器将词法单元组合成抽象语法树(AST)(语法分析)。
编译器会将 var a
提升到当前作用域的顶部。这意味着在执行阶段,变量 a
会被初始化为 undefined
。()
var a; // 变量提升 初始化为undefined
- 执行阶段(
a=1
)
引擎为当前作用域创建一个执行上下文。执行上下文包含变量对象、作用域链和 this 绑定等信息。在执行上下文中,变量 a
被初始化为 undefined
。引擎根据 AST 逐行执行代码:
-
查找变量
a
是否已存在于当前执行上下文的变量对象中。如果是,跳过声明部分;如果不是,创建变量a
并初始化为undefined
。 -
将值
1
赋给变量a
。
a = 1; // 赋值
- 对于作用域
变量不会单独存在,属于某个特定的作用域(编译阶段)
作用域是变量的查找规则,查找变量时,在当前作用域查找,找不到,去外层作用域查找,一直冒泡,直到全局作用域。如果还找不到,未定义。这个查找路径就是作用域链
(执行阶段)。
下面有一个关于作用域链的例子:
var a = 1;
var b = 4;
function foo() {
var a = 5;
var a = 2; //var a 会被忽略,但值被覆盖为2
function bar() {
var a = 3;
return a + b;
}
console.log(a, b); //2 4
}
foo();
// 在console.log()的当前作用域下去找 a 和 b,找到 a=2,找不到b,去外层作用域找,找到 b=4
变量查找机制(LHS和RHS)
JS代码在执行过程中,引擎如何查找和处理变量有两种不同方式,左查询(LHS)和右查询(RHS)。这两种方式与变量的读取和写入有关。下面我们来解释左查询和右查询。
//LHS 查询是在赋值操作中,查找变量的存储位置,以便将值写入该位置。
//RHS 查询是在读取操作中,查找变量的值,以便使用该值。
var a = 1; // a 是LHS,引擎需要通过作用域找到a来给a赋值
console.log(a); // a是RHS,引擎需要找到a的值赋给log函数
所以
LHS:用于查找变量的存储位置,以便进行赋值
操作。
RHS:用于查找变量的值,以便进行读取
操作。
这里有几个特别的例子
function foo1(a) {
var b = a;
return a + b;
}
var c = foo1(2);
// 中 LHS 和 RHS 引用的分析
// LHS 引用(共 3 处):
// c = ...: 在 var c = foo(2); 这行代码中,c 出现在赋值操作符 = 的左侧,因此它是一个 LHS 引用。 我们想要找到 c 的容器并将其赋值为 foo(2) 的返回值。[1]
// a = 2(隐式变量分配): 当调用 foo(2) 时,会隐式地将参数 2 赋给函数的参数 a。 这个赋值操作也需要进行 LHS 查询来找到 a 的容器。[2]
// b = ...: 在函数 foo(a) 内部,var b = a; 这行代码中,b 出现在赋值操作符 = 的左侧,因此它是一个 LHS 引用。 我们想要找到 b 的容器并将其赋值为 a 的值。[3]
// RHS 引用(共 4 处):
// foo(2 ...): 在 var c = foo(2); 这行代码中,我们需要获取 foo 的值(也就是函数本身)并执行它。 因此,foo 是一个 RHS 引用。[1]
// = a: 在 var b = a; 这行代码中,我们需要获取 a 的值并将其赋给 b。 因此,a 是一个 RHS 引用。[2]
// a ...: 在 return a + b; 这行代码中,我们需要获取 a 的值并将其与 b 的值相加。 因此,a 是一个 RHS 引用。[3]
// ...b: 在 return a + b; 这行代码中,我们需要获取 b 的值并将其与 a 的值相加。 因此,b 是一个 RHS 引用。[4]
function foo2() {
b = 2; // LHS 查询 默认声明变量
}
foo();
// LHS b 变成了全局变量
console.log(b);
//如果在作用域链上找不到b,在非严格模式下,引擎会在全局对象上创建变量b,并将其初始化为undefined,在函数中赋值为2,b成了一个全局变量
//严格模式时,ReferenceError: b is not defined
//不会在全局对象上创建变量
"use strict"
function foo3() {
b = 2;
}
foo3();
结论
通过对var a = 1;
这行代码的深入分析,我们不仅了解了JavaScript中的引擎、编译器、作用域,还有作用域链,变量查找机制等复杂概念。随着语言特性的不断演进,持续学习和实践将是不断提升技能的关键。