深入了解JS执行过程

深入了解JS执行过程

文章翻译至: Exploring the Magic of JavaScript: A Deep Dive into Its Execution Process

目录

JavaScript 是 Web 开发中使用的最流行的编程语言之一。它是一种高级编程语言,可以解释和动态类型。JavaScript 的主要特性之一是其事件驱动的架构,它允许它无缝地处理异步代码执行。在本文中,我们将讨论 JavaScript 如何在幕后工作

我们用高级语言编写代码。但我们的浏览器只能理解二进制/机器代码。我们必须以某种方式将这种高级语言转换为机器可理解的语言。

在读取 HTML 代码时,当浏览器遇到任何<script>标签或任何包含任何事件(如 onmouseoveronClick)的属性时,它会将其发送到其 javascript 引擎(V8等......)

然后,Javascript 引擎创建一个名为 Execution context 的特定环境来执行代码。在执行上下文运行时,解析器解析特定代码,变量和函数存储在内存中,最后生成可执行字节码,最终由 javascript 引擎执行。

主要有两种类型的执行上下文。

1. 全局执行上下文(GEC)

每当 javascript 引擎收到<script>文件时,它首先会创建一个默认的执行上下文,称为全局执行上下文。它是默认的执行上下文,所有不在函数中的 javascript 代码都会在这里被执行。

2. 函数执行上下文(FEC)

当调用函数时,js 引擎会在全局执行上下文中创建不同类型的执行上下文,称为函数执行上下文,以求值和执行函数中的代码。

每个函数调用都有自己的函数执行上下文,因此会有多个函数执行上下文。

执行上下文是如何创建和工作的?

JS 中的一切都发生在执行上下文中。想象一下一个密封的容器,JS在其中运行。它是一个抽象的概念,包含有关环境的信息。在当前代码中执行。

在容器中,第一个组件是内存组件(内存创建阶段),第二个组件是代码组件(代码执行阶段)。内存组件具有键值对中的所有变量和函数。它也称为可变环境。

代码组件是一次执行一行代码的地方。它也被称为执行线程。

1. 内存创建阶段

在此阶段,执行上下文首先与执行上下文对象相关联。此对象存储了大量重要数据,这些数据将在代码执行阶段使用。

此阶段分为三个阶段:

  1. 创建变量对象( Variable Object - VO)

它是一种执行上下文中创建的类似对象的容器。它存储执行上下文中定义的变量和函数声明。

在全局执行上下文中,对于使用 var 关键字声明的变量,将向 VO 添加一个指向该变量并设置为undefined的属性。

对于函数,将一个属性添加到 VO 中,指向该函数,并且该属性存储在内存中。这意味着所有函数声明都将在 VO 中存储和访问,甚至在代码开始运行之前。

另一方面,函数执行上下文不构造 VO,而是生成一个称为argument对象的类似数组的对象,其中包括提供给函数的所有参数。

在执行代码之前将变量和函数声明存储在内存中的这种过程称为Hoisting(提升)

  1. 创建作用域链

每个函数执行上下文都会创建其作用域,可以通过作用域的过程来访问其定义的变量和函数的空间。

当一个函数在另一个函数中定义时,内部函数可以访问外部函数中定义的代码,反之亦然。此行为称为词法范围。

这个范围的概念在 JavaScript 中带来了一个相关的现象,称为closures(闭包)。内部函数可以访问其外部函数的变量,即使外部函数执行完毕后也能继续访问。

js引擎在 最内层函数 > 内部函数 > 全局执行上下文的范围,以解决其中调用的变量和函数的概念被称为: Scope Chain(作用域链)

当 JavaScript 引擎无法解析作用域链中的变量时,它才会停止执行并引发错误。

不过,解析不能向后。也就是说,全局作用域/外部作用域永远无法访问内部函数的变量,除非它们从函数返回。

作用域链就像一个单向玻璃。你可以看到外面,但外面的人看不到你。

  1. 设置“this”的值

此关键字是指执行上下文所属的范围。

在全局执行上下文中,在任何函数之外,这都是指全局对象,即window对象。

使用 var 关键字初始化的函数声明和变量被指定为全局对象(窗口对象)的属性和方法。

var name = “Bishnu”window.name = "Bishnu" 严格相等

在函数执行上下文的情况下,它不会创建此对象。相反,它可以访问它所定义的环境。

在对象中,this关键字不指向全局执行上下文,而是指向对象本身。

2. 代码执行阶段

在此之前,VO 包含值为undefined的变量。

在此阶段,js 引擎再次读取当前执行上下文中的代码,然后使用这些变量的实际值来更新 VO。然后,代码由解析器解析,转换为可执行字节码,最后执行。

JavaScript Execution Stack

执行栈/调用栈跟踪脚本生命周期中创建的所有执行上下文。

JavaScript 是一种单线程语言,这意味着它一次只能执行一个任务。当其他操作、函数和事件发生时,对于每个操作、函数和事件,都会创建单独的执行上下文,并且由于 js 的单线程特性,将创建要执行的栈执行上下文,称为执行栈。

首先,当脚本在浏览器中加载时,会创建一个默认的全局执行上下文,js 引擎在其中开始执行代码并设置到执行栈的底部。

然后,JS 引擎在代码中搜索函数调用。对于每个函数调用,都会为该函数创建一个新的函数执行上下文,并将其设置在当前执行上下文的顶部。

当前位于顶部的执行上下文(EC)成为活动(active)的执行上下文,将首先由js引擎执行。

一旦活动执行上下文中的所有代码都执行完毕,js 引擎就会弹出执行栈的特定函数的EC,移动到它的下面(下一个),依此类推。

以这段代码为例:

var name = "Victor";

function first() {
  var a = "hi";
  second();
  console.log(`${a} ${name}`);
}
function second() {
  var b = "hey!";
  third();
  console.log(`${b} ${name}`);
}
function third() {
  var c = "Hello";
  console.log(`${c} ${name}`);
}
  1. 首先,脚本被加载到 js 引擎中。然后 js 引擎创建 GEC 并将其设置到执行栈的底部。

  2. 变量name因为在函数之外定义,因此它位于 GEC 中并存储在其 VO 中。函数也会发生相同的过程(变量a, b, c使用var创建)。

  3. 当 js 引擎遇到第一个函数first时,会为其创建一个新的 FEC。这个新上下文位于当前上下文(即 GEC)之上。在第一次函数调用的持续时间内,其FEC执行上下文将成为活动上下文(位于栈顶)。

  4. 在第一个函数中,变量 var a = “hi” 存储在其 FEC 中,而不是 GEC 中。接下来,在第一个函数中调用第二个函数。由于 js 的单线程性质,第一个函数的执行将被暂停。它必须等到第二个函数完成。同样,js 引擎为第二个函数创建了一个新的 FEC,并将其放在堆栈的顶部,使其成为活动上下文。

  5. 变量 var b=“hey!” 存储在其 FEC (second所属的FEC)中,接着,第三个函数在第二个函数中调用。FEC被创建并放置在执行栈的顶部。

  6. 在第三个函数中,变量 var c = “Hello” 存储在其 FEC 中(third所属的FEC),然后,消息“Hello Victor”被记录到控制台中。第三个函数已经执行完毕,它的 FEC 从堆栈的顶部被移除,此时第二个函数的 FEC 又变为了活动上下文。

  7. 在第二个函数中,"hey! Victor"被记录到控制台。该函数执行完毕,并且 EC 将从调用栈中弹出。然后第一个函数执行完毕,第一个函数的 EC 堆栈从堆栈中弹出,因此控制返回到代码的 GEC。

  8. 最后,当整个代码的执行完成时,js 引擎会从当前栈中删除 GEC。

异步代码执行

JavaScript 是一种单线程编程语言。这意味着 JavaScript 在单个时间点只能做一件事。

JavaScript 引擎从文件顶部执行脚本,然后向下运行。它在执行阶段创建执行上下文,将函数推入和弹出调用堆栈。如果函数执行时间较长,则在函数执行期间无法与 Web 浏览器交互,因为页面挂起。

需要很长时间才能完成的函数称为阻塞函数。从技术上讲,阻塞函数会阻止网页上的所有交互,例如鼠标单击。

function task(message) {
    // emulate time consuming task
    let n = 10000000000;
    while (n > 0){
        n--;
    }
    console.log(message);
}

console.log('Start script...');
task('Call an API');
console.log('Done!');

/*
Start script...
Call an API.
Done!
*/

回调

为了防止阻塞函数阻塞其他活动,通常将其放在回调函数中。例如:

console.log('Start script...');

setTimeout(() => {
    task('Download a file.');
}, 1000);

console.log('Done!');

/*
Start script...
Done!
Download a file.
*/

在示例中,调用 setTimeout 函数时,JavaScript 引擎将其放在调用栈上,Web API 创建一个计时器,该计时器将在 1 秒后过期并执行回调函数。

然后 JavaScript 引擎将 task() 函数放入称为回调队列或任务队列的队列中:

事件循环: 监视调用栈和回调队列。如果调用栈为空,则事件循环将从队列中获取第一个回调函数,并将其推送到调用栈,从而有效地运行它。

如果调用栈不为空,则事件循环将回调队列中的下一个函数放置到调用栈执行。直到它为空。