JavaScript 内部是如何工作的?

这篇文章的目的实际上是以一种简单的方式介绍**JavaScript**的内部工作原理,以便即使是新程序员也能够掌握这个概念,并直观地看到编写 JavaScript 代码时发生的情况。

首先,我希望关注至少 3 个问题,这将有助于克服困难并内化背后的逻辑?

这些问题也是在 Web 开发人员的求职面试中可能会问到的问题,其中 JavaScript 意味着:

**1. JavaScript 如何工作?**

**2. 解释一下同步和异步之间的区别?**

**3. 或者解释一下这句话:JavaScript 是一种可以非阻塞的单线程语言?**

实际上,编写程序时没有必要了解 JavaScript 的内部工作原理,但学习它对于理解背后发生的事情和感受自己正在编写的内容至关重要,因此许多从业多年的开发人员并不关心这一点。

首先让我们了解**什么是程序?**程序只是一组指令,告诉计算机要做什么以及如何执行任务。程序必须**分配内存**,否则,我们将无法在计算机上拥有变量甚至保存文件。程序还应该解析(读取)并执行专用任务,所有这些都发生在内存中。

现在,JavaScript 有一个称为 **JavaScript 引擎** 的引擎,每个浏览器都会实现它。例如,在 Chrome 中它被称为 **V8**,在 Mozilla Firefox 中它被称为 **Spider Monkey**,在 Safari 浏览器中它被称为 **JavaScript Core Webkit**。

**下图展示了谷歌浏览器的V8引擎**

Image description

**JavaScript 引擎内部发生了什么?**

Chrome 中的 V8 等 JavaScript 引擎读取我们编写的 JavaScript 代码并将其转换为浏览器的机器可执行指令。上图显示了 JavaScript 引擎的各个部分,它由两部分组成,即**内存堆**和**调用堆栈**。

还需要注意的是,内存分配发生在**内存堆**中,而解析(读取)和执行发生在**调用堆栈**中。除此之外,内存堆还可以告诉您在程序中的位置。

**让我们用 JS(JavaScript)代码看看内存堆中的内存分配**

const a = 4;  // now we allocated a memory. JS engine is going to remember
// that a has a value of 4. 

const Obj = {a, b, c };  // In memory, variable 'Obj' holds the object {a, b,c}

// The same as on array. the engine will remember values of the array
const Array = [1,2,3,4,5]

**那么,一旦全局声明上述代码,会出现什么问题?**

有一种现象叫做**内存泄漏**。如上所述,变量声明发生在内存堆中,并且分配的大小是有限的。当您继续声明全局变量时,这些全局变量是非常大的数组而不是数字,甚至是未使用的,这会填满内存并导致**内存泄漏**。您会听说全局变量很糟糕,因为当我们忘记清理时,我们会填满这个内存堆,最终浏览器将无法工作。

**调用堆栈怎么样?**

如果我们还记得的话,读取并执行脚本的是调用栈。我们用代码来解释一下。

// Example Call Stak

console.log("x");
console.log("y");
console.log("z");

// Result in browser

// x
// y
// z

上述代码中,调用栈读取第一行 **console.log(“x”)**; 并放入**调用栈**,JavaScript 引擎识别到添加了 **console.log**,则将其弹出到调用栈,运行并输出 x。然后移除第一个运行完毕的 **console.log**,将其放入第二个 **console.log(“y”)**,并加入调用栈,执行 **y**,移除第二个 **console.log**。最后以同样的流程获取 console.log(“z”)。

这是最简单的演示,如果示例稍微复杂一点呢?我们举一个典型的例子:

// Demo Example

const example1 = () => {
  const example2 = () => {
    console.log('7');
}
example2();

// Result:

// 4

现在,根据调用堆栈,上面的代码中发生了什么?让我们看看它将如何运行上面的代码块:

**//调用堆栈**

函数 **example1()** 将首先运行,然后函数 **example2()** 位于调用堆栈顶部并运行,在检查是否还有其他代码要运行后,它会打印出数字 7 作为输出。此后,它将开始按顺序从调用堆栈中删除,首先是 **console.log('7')**、example2()、example1(),然后调用堆栈现在为空。

意味着它**只有一个调用堆栈**。它一次只能执行一件事,需要强调的是**调用堆栈像堆栈一样是先进后出的**。

其他语言可以有多个调用堆栈,即所谓的**多线程**,拥有多个调用堆栈可能更有利,这样我们就不会一直等待任务。

**> 但是,为什么 JavaScript 被设计成单线程的呢?**

要回答这个问题,通常在单线程上运行代码相当容易,因为不会出现多线程环境中出现的复杂情况。你实际上需要关心一件事。在多线程中可能会出现**死锁**之类的问题。有了这个理论,我们很容易知道**同步编程**意味着什么。

同步编程的意思是:执行第一行代码,然后执行第二行,最后执行第三行,等等...

更明确地说,这意味着 **console.log(“y”)** 直到 **console.log(“x”)** 完成才能运行,并且 **console.log(“z”)** 直到前两个完成才能启动,因为它是一个 **调用堆栈**。

程序员很可能会使用 stackoverflow.com 这个网站。这个名字是什么意思呢?好吧。让我们来看看:

Image description

上图显示了内存泄漏是如何发生的,以及 JavaScript 引擎的内存堆是如何溢出的。在这里,调用堆栈接收到许多大于其大小的输入并溢出。

可以借助代码来演示堆栈溢出:

// A recursion function that creates a stackoverflow

function foo(){
  foo();
}
foo();

// This function will keep looping over and over and keeps adding foo() to
// the call stack and ends up to the stackoverflow.

// Need to have fun? run this in the browser to see.

请记住,JavaScript 是单线程的,每次只能执行一条语句。**现在有一个问题:**如果以下代码块中的 **console.log(“y”)** 有一个需要更长时间才能执行的大任务(例如,循环遍历包含数千或数百万个项目的数组)会怎么样?会发生什么?

// Example Call Stak

console.log("x"); 
console.log("y");
console.log("z");

// Result in browser

// x
// y

第一行将执行,并假设第二行有大量任务要执行,因此第三行将等待很长时间才能执行。在上面的例子中,这并没有多大意义,但让我们想象一下执行繁重操作的大型网站,用户将无法执行任何操作。网站将冻结,直到任务完成,用户在那里等待。从性能方面来看,这是一种糟糕的体验。

好吧,对于同步任务,如果我们有一个函数需要花费很多时间,它就会阻塞整个流程。所以,听起来我们需要一些非阻塞的东西。记住我上面提到的陈述:

理想情况下,在 JavaScript 中我们不会等待需要时间的事情。那么,我们如何解决这个问题呢?

为了解决这个问题,出现了**异步编程**。那么,这是什么?

将异步视为一种行为。同步执行很棒,因为它是可预测的。在同步中,我们知道先发生什么,接下来发生什么等等,但它可能会变慢。

当我们必须执行诸如图像处理或通过网络发出 API 调用等请求时,我们使用的不仅仅是同步任务,而是异步任务。

让我们看看如何用代码进行异步编程:

console.log("x"); 
setTimeout(() =>{
  console.log("y");
},3000);

console.log("z");

// Result

      x
      z
      y   // comes after 3 seconds latter

现在,基于上述代码,似乎我们跳过了第二行并执行第三行并等待 3 秒钟来输出结果。这是异步发生的。

Image description

要运行 JavaScript,我们需要的不仅仅是内存堆和调用堆栈。我们需要所谓的 **JavaScript 运行时**,它是浏览器的一部分。它包含在浏览器中。在引擎的顶部,有一些东西和 **事件循环**,如图所示。

现在让我们讨论一下使用 setTimeout 函数的代码。

console.log("x"); 
setTimeout(() =>{
  console.log("y");
},3000);

console.log("z");

**setTimeout 函数**是 Web API 的一部分,而不是 JavaScript 的一部分,而是浏览器提供给我们用来进行异步编程的函数。因此,让我们提供更多细节以便澄清。

**调用堆栈**:console.log(“x”) 进入调用堆栈,运行并将 console.log 输出到浏览器。之后,`setTimeout(() =>{console.log(“y”);},3000);` 进入调用堆栈,因为第一个任务已完成,然后转到第二个任务。

现在,这里有一件事,在读取代码时,调用堆栈将检测到已设置了一个 **setTimeout 函数**,它不是 JavaScript 的一部分,而是 Web API 的一部分(参见图 JavaScript 运行时环境),并且具有特殊的特性。发生的事情是 setTimeout 触发了 **WEB API**,并且由于 Web API 收到通知,该函数将从调用堆栈中弹出。

现在,Web API 启动一个三秒的计时器,知道 3 秒内它必须完成任务。记住这里,因为调用堆栈是空的,JavaScript 引擎继续到第 3 行,即 console.log(“z”); 并执行它。这就是为什么我们得到结果 **x,z**,但我们在 Web API 中有三秒的 setTimeout。然后,三秒后,当时间限制到达时,setTimeout 运行并查看其中的内容,然后它就完成了。完成后,Web API 将识别出它有一个 setTimeout 的 **callback()** 函数,并将其添加到 **CALLBACK QUEUE** 中,准备运行它。

我们来到最后一部分,即** EVENT LOOP** 函数,其中包含 **console.log(“z”)**,然后将其放入 **CALL STACK** 并运行。完成后,将其从调用堆栈中弹出。现在一切都是空的,并得到结果 xz y。

**结论**:在这篇文章中,我们看到了很多关于底层发生的事情的信息,以便完全理解 JavaScript 逻辑,包括同步和异步执行的任务。

希望这将有助于新手和高级 JavaScript 程序员享受在 JavaScript 相关框架(如 ReactJS 或 AngularJS)中编码的乐趣,因为这是理解高级逻辑的起点。

**> 编码愉快**