一行代码也能玩这么花?带你领略代码底层的魅力

121 阅读5分钟

前言

var a = 1;在看到这行代码你首先会想到什么,声明一个名为‘a’的变量给它赋值为1?这么想也没错,但是格局小了, 当我们执行 var a = 1; 这行代码时,JavaScript引擎内部会发生一系列的操作。这些操作可以大致分为编译阶段和执行阶段。让我们一起往代码底层探索,在解释这两个阶段之前,我们得先搞清楚一个变量是如何被查找的。

内存中的存储与作用域链

当执行 var a = 1; 时,变量 a 被创建,并在内存中分配了一块空间来存储数字 1。这块内存空间的位置取决于变量的作用域。如果是在函数内部声明的,那么它会被存储在函数的局部作用域中;如果是全局声明的,则会被存储在全局对象(如浏览器环境下的 window 对象)中。

  • 当查找变量的时候都发生了什么?

会先从当前上下文的变量对象中查找; 如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找; 一直找到全局上下文的变量对象,也就是全局对象; 作用域链的顶端就是全局对象; 这样由多个执行上下文的变量对象构成的链表就叫做作用域链,从某种意义上很类似原型和原型链。

  • 作用域链和原型继承查找时的区别:

查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined 查找的属性在作用域链中不存在的话就会抛出ReferenceError。

  • 作用域嵌套:

既然每一个函数就可以形成一个作用域(词法作用域 || 块级作用域),那么当然也会存在多个作用域嵌套的情况,他们遵循这样的查询规则:

内部作用域有权访问外部作用域;

外部作用域无法访问内部作用域;

兄弟作用域不可互相访问;

为了方便理解,我们可以把想象成这栋高大的建筑: 0e7efdf4961afeeef96a6319e549808.png

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; 这个表达式来说,编译器会做如下处理:

  1. 分词:将代码分解成独立的词汇单元。例如,var 是一个关键字,a 是一个标识符,= 是一个赋值运算符,而 1 则是一个数值字面量。
  2. 解析作用域:编译器会识别出 var a 是一个变量声明,并确定这个变量应该属于哪个作用域。如果这个声明是在一个函数内部,那么 a 将属于该函数的局部作用域。如果是在全局上下文中,那么 a 将成为全局变量。
  3. 初始化:在编译阶段,所有使用 var 声明的变量都会被初始化为 undefined。这意味着即使在赋值之前,你也可以访问这些变量,它们的值将是 undefined。这是由于变量提升(Hoisting)的原因。

执行阶段

一旦编译完成,就进入了执行阶段。在这个阶段,JavaScript引擎会按照AST执行实际的代码逻辑。对于 var a = 1; 来说,执行阶段涉及以下步骤:

  1. 查找变量:当执行 a = 1; 时,引擎需要先找到变量 a 的位置。这涉及到作用域链的查找。如果在整个作用域链中都没有找到变量 a,则会在当前作用域中创建一个新的变量 a
  2. 赋值操作:一旦找到了变量 a(或者在当前作用域中创建了新的变量 a),就会执行赋值操作,将 1 赋值给 a。这里,a 是赋值操作的目标,我们称之为LHS(Left Hand Side)查询。而 1 是一个值,不需要查找,直接用于赋值。

总结

通过上述分析,我们可以看到,即使是一行简单的 var a = 1; 代码,背后也涉及到了编译阶段的词法分析、作用域的解析以及执行阶段的作用域链查找和赋值操作。这些底层机制共同协作,确保了代码的正确执行。