想当大佬必须知道的JS执行机制和作用域

255 阅读6分钟

引言

在我们开始讨论JavaScript的作用域之前,先来看一句代码:

var a = 1;

这是一句非常简单的代码,但我想问的是,你是如何理解这句话的?声明了一个值为1的全局变量a?

我们今天将以这句话开始,一步步带你了解JavaScript的执行机制以及作用域。 IP-C.jpg

引擎、编译器与作用域

为了解释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 逐行执行代码:

  1. 查找变量 a 是否已存在于当前执行上下文的变量对象中。如果是,跳过声明部分;如果不是,创建变量 a 并初始化为 undefined

  2. 将值 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中的引擎、编译器、作用域,还有作用域链,变量查找机制等复杂概念。随着语言特性的不断演进,持续学习和实践将是不断提升技能的关键。