大厂的必经之路 了解JS的执行机制!!

199 阅读7分钟

通过一句 var a= 1; 了解JS的执行机制

JavaScript 是一种广泛使用的编程语言,其作用域和变量查找机制是理解代码执行流程的关键。接下来将分享我的一些关于 JavaScript 中的变量声明、赋值、作用域以及变量查找过程的见解,并给出一些相关的例子来说明这些概念,方便大家理解。

1. 变量声明与赋值 ,var a = 1;如何解释?

在 JS 中,变量可以通过 var 关键字声明。例如:

var a = 1;

看似很简单,其实确实不难。这行代码往细了讲,可以分为两个步骤解释:

  1. 声明var a; —— 变量 a 被声明。
  2. 赋值a = 1; —— 变量 a 被赋予值 1。

2. 要实现 var a = 1; 哪些东西要为它工作?

在 JS 中,这句 var a = 1; 语句背后涉及多个组件的协同工作,接下来我们将探讨这一过程,了解引擎、编译器和作用域管理如何共同完成这一任务,实现代码的执行的。

1.引擎:CEO

引擎可以类比为一个公司的 CEO,负责整体的工作。它的主要职责包括:代码解析、性能优化以及内存管理。

2. 编译器:CTO

编译器扮演着 CTO 的角色,负责将源代码转换为可执行的机器码。具体来说,编译器的工作可以分为以下几个步骤:

2.1 分词

分词是将源代码分解成一个个有意义的单元(token)。例如,对于 var a = 1; 这行代码,编译器会将其分解为:

  • var:关键字
  • a:标识符
  • =:赋值操作符
  • 1:数字字面量
  • ;:语句结束符
2.2 语法分析

在分词之后,编译器会进行语法分析,将 token 序列转换为抽象语法树(AST)。AST 是一种树状结构,表示代码的逻辑结构。

2.3 代码生成

最后,编译器会根据 AST 生成可执行的机器码。这个过程包括优化和翻译,确保生成的代码高效运行。

3. 作用域:COO

作用域扮演着 COO(首席运营官)的角色,它决定了变量在代码中的可见性和生命周期。在 JavaScript 中,变量不会单独存在,而是归属于某个特定的作用域。

3.1 作用域的分类

JavaScript 中主要有两种作用域:

  • 全局作用域:在全局范围内声明的变量。
  • 局部作用域:在函数或块级范围内声明的变量。
3.2 作用域嵌套与作用域链

作用域嵌套是指在一个作用域内嵌套另一个作用域。作用域链是描述这种嵌套关系的链表结构。例如:

var a = 1;
function foo() {
  var b = 2;
  function bar() {
    var c = 3;
    console.log(c); // 输出: 3
    console.log(b); // 输出: 2
    console.log(a); // 输出: 1
  }
  bar();
}
foo();

在这个例子中,bar 可以访问 cba,因为它们分别位于当前作用域、父级作用域和全局作用域中。

3. 变量查找, LHS 查找和 RHS 查找

变量查找过程可以分为两个阶段:LHS 查找和 RHS 查找。

  • LHS 查找(Left-Hand Side) :在赋值操作中查找变量的容器。例如:a = 1;
  • RHS 查找(Right-Hand Side) :在取值操作中查找变量的值。例如:var b = a + 2;

3.1.简单案例 方便理解

对代码中的 LHS 和 RHS 引用进行分析:

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo(2); 
  • LHS 引用(共 3 处):

    • c = ...: 在 var c = foo(2); 这行代码中,c 出现在赋值操作符 = 的左侧,因此它是一个 LHS 引用。 我们想要找到 c 的容器并将其赋值为 foo(2) 的返回值。
    • a = 2(隐式变量分配): 当调用 foo(2) 时,会隐式地将参数 2 赋给函数的参数 a。 这个赋值操作也需要进行 LHS 查询来找到 a 的容器。
    • b = ...: 在函数 foo(a) 内部,var b = a; 这行代码中,b 出现在赋值操作符 = 的左侧,因此它是一个 LHS 引用。 我们想要找到 b 的容器并将其赋值为 a 的值。
  • RHS 引用(共 4 处):

    • foo(2 ...): 在 var c = foo(2); 这行代码中,我们需要获取 foo 的值(也就是函数本身)并执行它。 因此,foo 是一个 RHS 引用。
    • = a: 在 var b = a; 这行代码中,我们需要获取 a 的值并将其赋给 b。 因此,a 是一个 RHS 引用。[3]
    • a ...: 在 return a + b; 这行代码中,我们需要获取 a 的值并将其与 b 的值相加。 因此,a 是一个 RHS 引用。
    • ...b: 在 return a + b; 这行代码中,我们需要获取 b 的值并将其与 a 的值相加。 因此,b 是一个 RHS 引用。

4 引擎和作用域的对话 方便理解的小剧场

function foo(a) {
    console.log(a ); //2
}
foo(2);

让我们把上面这段代码的处理过程想象成一段对话,这段对话可能是下面这样的。 image.png

5. LHS与RHS运用起来有何不同?

5.1 RHS 查找失败与 ReferenceError

考虑以下代码:

function foo(a) {
    console.log(a + b);
    b = a;
}

foo(2);

在这段代码中,foo 函数接受一个参数 a,并在控制台输出 a + b 的结果,然后将 a 的值赋给 b。当我们调用 foo(2) 时,会发生什么?

1. 代码结果:

image.png

2. 进行变量查找分析:
  • RHS 查找:在 console.log(a + b) 中,b 是 RHS 查找的目标,引擎需要找到 b 的值。如果 b 未定义,引擎会抛出 ReferenceError: b is not defined
  • LHS 查找:在 b = a 中,b 是 LHS 查找的目标,引擎需要找到 b 的容器。即使 b 未定义,JavaScript 引擎也会在当前作用域中创建一个变量 b 并赋予值 2。

5.2 LHS 查找与隐式全局变量

考虑以下代码:

function foo() {
    b = 2;  // LHS 查询,默认声明变量
}

foo();
// LHS b 全局
console.log(b);  // 输出: 2

在这段代码中,foo 函数内部有一个赋值操作 b = 2。当我们调用 foo() 时,会发生什么?

1. 代码结果:

image.png

2. 进行变量查找分析:

  • LHS 查找:在 b = 2 中,b 是 LHS 查找的目标。即使 b 在当前作用域中未定义,JavaScript 引擎也会在当前作用域的父级作用域中继续查找。
  • 全局作用域:如果在所有作用域中都没有找到变量 b,JavaScript 引擎会在全局作用域中创建一个变量 b 并赋予值 2。

注意:"use strict";//严格模式下会抛出 ReferenceError错误,在严格模式下,JavaScript 引擎禁止隐式创建全局变量。如果在 LHS 查找过程中没有找到变量 b,引擎会立即抛出 ReferenceError 错误,而不是在全局作用域中创建一个新变量。

6. 变量声明、赋值与调用错误报错的类型?

给出以下代码:

var a;  // 声明变量 a,未赋值
a = 2;  // LHS 赋值,将 2 赋给变量 a
a();  // 调用 a,引发 TypeError

让我们逐步分析这个过程:

    1. var a; : 变量 a 被声明,但没有赋值,因此它的值为 undefined
    1. a = 2; : 变量 a 被赋予值 2。此时,a 的值为 2,类型为 number
    1. a(); : 尝试调用变量 a。由于 a 的值是 2,而 2 不是一个函数,因此会引发 TypeError

7. 结语

在这里,我们探讨了 JavaScript 中的变量声明、赋值、作用域以及变量查找机制的相关概念,理解这些概念对于我们编写高效、无误的 JavaScript 代码至关重要,希望本文能帮助你更好地掌握 JavaScript 的核心机制,理解LHS与RHS的区别,从而更准确的编写和调试代码,希望小伙伴们能够收获到自己想要了解的知识,在前端开发的道路上更进一步,成为一名更牛的前端开发者。

download.jpg