前言
var a = 1;在看到这行代码你首先会想到什么,声明一个名为‘a’的变量给它赋值为1?这么想也没错,但是格局小了,
当我们执行 var a = 1; 这行代码时,JavaScript引擎内部会发生一系列的操作。这些操作可以大致分为编译阶段和执行阶段。让我们一起往代码底层探索,在解释这两个阶段之前,我们得先搞清楚一个变量是如何被查找的。
内存中的存储与作用域链
当执行 var a = 1; 时,变量 a 被创建,并在内存中分配了一块空间来存储数字 1。这块内存空间的位置取决于变量的作用域。如果是在函数内部声明的,那么它会被存储在函数的局部作用域中;如果是全局声明的,则会被存储在全局对象(如浏览器环境下的 window 对象)中。
- 当查找变量的时候都发生了什么?
会先从当前上下文的变量对象中查找; 如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找; 一直找到全局上下文的变量对象,也就是全局对象; 作用域链的顶端就是全局对象; 这样由多个执行上下文的变量对象构成的链表就叫做作用域链,从某种意义上很类似原型和原型链。
- 作用域链和原型继承查找时的区别:
查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined 查找的属性在作用域链中不存在的话就会抛出ReferenceError。
- 作用域嵌套:
既然每一个函数就可以形成一个作用域(词法作用域 || 块级作用域),那么当然也会存在多个作用域嵌套的情况,他们遵循这样的查询规则:
内部作用域有权访问外部作用域;
外部作用域无法访问内部作用域;
兄弟作用域不可互相访问;
为了方便理解,我们可以把想象成这栋高大的建筑:
LHS 和 RHS 查询
在JavaScript中,查找变量的过程可以分为两种类型:LHS查询和RHS查询。
- LHS查询(Left Hand Side):当你在等号的左侧看到一个变量名时,这通常意味着你正在进行LHS查询。这种查询的目的是找到一个可以赋值的地方。如果查找的目的是对变量进行赋值,就会使用LHS。例如,在
a = 1;中,a是LHS查询的目标。 - RHS查询(Right Hand Side):当你在等号的右侧看到一个变量名时,这通常意味着你正在进行RHS查询。这种查询的目的是获取变量的值,你可以将RHS理解成retrive his source value(取到它的源值)。如果目的是获取变量,就会使用RHS。例如,在
b = a;中,a是RHS查询的目标。 - 另外需要提醒的是,在js中函数同样应该被算作一个对象。
提问:以下代码分别执行了几次LHS和RHS?
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
答案是LHS查询有三处,RHS有四处。
分别是
-
LHS
- c= ...
- a=2;(foo(a),foo(2)) 这一个LHS其实是一个隐式查询
- b=...
-
RHS
- foo(2)
- = a;
- return a;
- return b;
你对了吗?
值得一提的是不成功的RHS引用会导致程序抛出ReferenceError异常。不成功的LHS应用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)。
编译阶段
在编译阶段,JavaScript引擎会进行词法分析和语法分析。这意味着代码首先会被分解成一个个有意义的符号(或称为标记),然后这些标记会被组合成抽象语法树(AST)。对于 var a = 1; 这个表达式来说,编译器会做如下处理:
- 分词:将代码分解成独立的词汇单元。例如,
var是一个关键字,a是一个标识符,=是一个赋值运算符,而1则是一个数值字面量。 - 解析作用域:编译器会识别出
var a是一个变量声明,并确定这个变量应该属于哪个作用域。如果这个声明是在一个函数内部,那么a将属于该函数的局部作用域。如果是在全局上下文中,那么a将成为全局变量。 - 初始化:在编译阶段,所有使用
var声明的变量都会被初始化为undefined。这意味着即使在赋值之前,你也可以访问这些变量,它们的值将是undefined。这是由于变量提升(Hoisting)的原因。
执行阶段
一旦编译完成,就进入了执行阶段。在这个阶段,JavaScript引擎会按照AST执行实际的代码逻辑。对于 var a = 1; 来说,执行阶段涉及以下步骤:
- 查找变量:当执行
a = 1;时,引擎需要先找到变量a的位置。这涉及到作用域链的查找。如果在整个作用域链中都没有找到变量a,则会在当前作用域中创建一个新的变量a。 - 赋值操作:一旦找到了变量
a(或者在当前作用域中创建了新的变量a),就会执行赋值操作,将1赋值给a。这里,a是赋值操作的目标,我们称之为LHS(Left Hand Side)查询。而1是一个值,不需要查找,直接用于赋值。
总结
通过上述分析,我们可以看到,即使是一行简单的 var a = 1; 代码,背后也涉及到了编译阶段的词法分析、作用域的解析以及执行阶段的作用域链查找和赋值操作。这些底层机制共同协作,确保了代码的正确执行。