【译】JavaScript 是如何工做的(上)

原文地址:How JavaScript Works. Why understanding the fundamentals is… | by Ionel Hindorean | Better Programmingjavascript

为何咱们要理解基本原理java

你可能想知道为何有人会在 2019 年费心写一篇关于 JavaScript 核心的长文。git

这是由于我相信,若是没有对基础知识的扎实了解,很容易在JS生态系统中迷失方向,并且几乎不可能探索更高级的内容。github

了解 JavaScript 的工做原理可使阅读和编写代码变得更容易,减小挫折感,让你专一于你的应用程序的逻辑,而不是与语言的语法做斗争。浏览器

它是如何工做的?

计算机不懂JavaScript,浏览器才懂。服务器

除了处理网络请求、监听鼠标点击、解释 HTML 和 CSS 以在屏幕上绘制像素外,浏览器还内置有一个 JavaScript 引擎。markdown

JavaScript 引擎是一个用 C++ 编写的程序,它逐字逐句地查看全部的 JavaScript 代码,并将其 "转化 "为计算机 CPU 可以理解和执行的东西--机器代码网络

这个过程是同步进行的,也就是说,他们在一条时间线上,并且是按顺序进行。多线程

他们这样作是由于机器代码很难,并且不一样的 CPU 制造商的机器代码指令是不一样的。因此,他们把全部这些麻烦从JavaScript 开发者那里抽象出来,不然,网络开发会更难,更不受欢迎,咱们也不会有像 Medium 这样的东西,让咱们能够写像这样的文章(而我如今就在睡觉)。app

JavaScript 引擎能够机器的地浏览每一行 JavaScript,一遍又一遍(解释器),或者它能够变得更聪明,检测出一些东西,好比常常被调用而且老是产生相同结果的函数。

而后,它能够把这些东西编译成机器代码,只需一次,这样下次遇到它时,它就会运行已经编译好的代码,这就快多了(及时编译)。

或者,它能够提早将整个东西编译成机器代码,而后执行(见编译器)。

V8 就是这样一个 JavaScript 引擎,谷歌在2008年将其开源。2009年,一个叫 Ryan Dahl 的人想到用 V8 来建立 Node.js,这是一个在浏览器以外的 JavaScript 运行环境,这意味着该语言也能够用于服务器端应用。

函数执行上下文

像其余语言同样,JavaScript 对于函数、变量、数据类型,以及这些数据类型能够存储的确切数值,在代码中哪些地方能够访问,哪些地方不能够,等等都有本身的规则。

这些规则由一个名为 Ecma International 的组织定义标准,它们共同构成了语言规范文件(你能够在这里找到最新版本)。

所以,当引擎将 JavaScript 代码转换为机器代码时,它须要考虑这些规范。

若是代码中包含一个非法的赋值,或者它试图访问一个变量,而根据语言的规范,这个变量不该该从代码的特定部分被访问,怎么办?

每次函数被调用时,它都须要弄清全部这些事情。它经过建立一个被称为 "执行上下文 "的包装来实现这一目的。

为了更具体一些,避免未来出现混淆,我将把这个称为函数执行上下文,由于每次调用函数都会建立一个。不要被这个术语所吓倒,暂时不要想太多,后面会详细说明。

只要记住,它决定了一些事情,好比。"在那个特定的函数中,哪些变量是能够访问的,在它里面这个值是什么,哪些变量和函数在它里面被声明?"

全局执行上下文

可是,并非全部的 JavaScript 代码都在一个函数里面(尽管大部分代码都在里面)。

在任何函数以外,在全局层面上也可能有代码,所以,JavaScript 引擎首先要作的一件事就是建立一个全局执行上下文。

这就像一个函数执行上下文,在全局层面上起到一样的做用,但它有一些特殊性。

好比,有且只有一个全局执行上下文,在执行开始时建立,全部的 JavaScript 代码都在其中运行。

全局执行上下文建立了两个东西,这两个东西对它来讲是特定的,即便没有代码要执行。

  • 一个全局对象。当 JavaScript 在浏览器内运行时,这个对象是窗口对象。当它在浏览器外运行时,就像在 Node.js 中那样,它将是相似 global 的对象。不过为了简单起见,我将在本文中使用 window

  • 一个特殊的变量 this

在全局执行上下文中,也只有 this,这实际上等于全局对象 window。它基本上是一个对 window 的引用。

this === window // logs true
复制代码

全局执行上下文和函数执行上下文之间的另外一个微妙区别是,任何在全局层面上声明的变量或函数(在任何函数以外),都会自动做为属性附加到窗口对象上,并隐含在特殊变量 this 上。

尽管函数也有特殊变量 this,但在函数执行环境中不会发生这种状况。

foo; // 'bar'
window.foo; // 'bar'
this.foo; // 'bar'
(window.foo === foo && this.foo === foo && window.foo === this.foo) // true
复制代码

全部的 JavaScript 内置变量和函数都附着在全局窗口对象上: setTimeout(), localStorage, scrollTo(), Math, fetch(),等等。这就是为何它们能够在代码的任何地方被访问。

执行栈

咱们知道,每次函数被调用时都会建立一个函数执行上下文。

因为即便是最简单的 JavaScript 程序也有至关多的函数调用,全部这些函数执行上下文都须要以某种方式进行管理。

请看下面的例子:

function a() {
  // some code
}

function b() {
  // some code
}

a();
b();
复制代码

当遇到函数 a() 的调用时,如上所述建立一个函数执行上下文,并执行该函数内的代码。

当代码的执行完成后返回语句或到达函数的包围},函数 a() 的函数执行上下文被销毁。

而后,会遇到 b() 的调用,对函数 b() 重复一样的过程。

但这种状况不多发生,即便在很是简单的 JavaScript 程序中。大多数状况下,会有一些函数在其余函数中被调用:

function a() {
  // some code
  b();
  // some more code
}

function b() {
  // some code
}

a();
复制代码

在这种状况下,a() 的函数执行上下文被建立,但就在 a() 的执行过程当中,遇到了 b() 的调用。

b() 建立了一个全新的函数执行上下文,可是没有破坏 a() 的执行上下文,由于它的代码尚未彻底执行。

这意味着在同一时间有许多函数执行上下文。然而,在任什么时候候,它们中只有一个在实际运行。

为了跟踪当前正在运行的函数,咱们使用了一个堆栈,其中当前正在运行的函数执行上下文位于栈的顶部。

一旦它执行完毕,它将被从堆栈中弹出,下一个执行上下文的执行将继续,以此类推,直到执行堆栈为空。

这个栈被称为执行栈,以下图所示:

image.png

当执行堆栈为空时,咱们以前讨论过的、从未被销毁的全局执行上下文就成为当前运行的执行上下文。

事件队列

还记得我说过,JavaScript 引擎只是浏览器的一个组件,与渲染引擎或网络层并列。

这些组件都有内置的 Hooks,引擎用这些 Hooks 来通讯,以启动网络请求,在屏幕上绘制像素,或者监听鼠标点击。

当你在 JavaScript 中使用相似 fetch 的东西来作一个 HTTP 请求时,引擎实际上会将其传达给网络层。每当请求的响应到来时,网络层将把它传回给 JavaScript 引擎。

但这可能须要几秒钟的时间,当请求正在进行时,JavaScript 引擎会作什么?

简单地中止执行任何代码,直到响应到来?继续执行剩下的代码,每当响应到来时,就中止一切并执行其回调?当回调完成后,继续执行它离开的地方?

以上都不是,尽管第一个能够经过使用 await 来实现。

在多线程语言中,这能够经过一个线程在当前运行的执行环境中执行代码,另外一个线程执行事件的回调来处理。但这在 JavaScript 中是不可能的,由于它是单线程的。

为了理解这其实是如何工做的,让咱们考虑一下咱们以前看过的 a()b() 函数,可是增长一个点击处理程序和一个 HTTP 请求处理程序。

function a() {
  // some code
  b();
  // some more code
}

function b() {
  // some code
}

function httpHandler() {
  // some code here
}

function clickHandler() {
  // some more code here
}

a();
复制代码

JavaScript 引擎从浏览器的其余组件收到的任何事件,如鼠标点击或网络响应,都不会被当即处理。

在这一点上,JavaScript 引擎可能正忙于执行代码,因此它将把事件放在一个队列中,称为事件队列。

image.png

咱们已经谈过了执行栈,以及一旦相应函数中的代码执行完毕,当前运行的函数执行上下文是如何从堆栈中弹出的。

而后,下一个执行上下文恢复执行,直到它完成,以此类推,直到堆栈为空,全局执行上下文成为当前运行的执行上下文。

当执行栈中有代码要执行时,事件队列中的事件被忽略,由于引擎正忙于执行栈中的代码。

只有当它完成了,而且执行栈是空的,JavaScript 引擎才会处理事件队列中的下一个事件(固然,若是有的话),而且会调用它的处理程序。

因为这个处理程序是一个 JavaScrip t函数,它的处理就像 a()b() 的处理同样,也就是说,一个函数的执行上下文被建立并推到执行栈中。

若是该处理程序反过来调用另外一个函数,那么另外一个函数的执行上下文就会被建立并推到堆栈的顶部,以此类推。 只有当执行栈再次为空时,JavaScript 引擎才会再次检查事件队列中的新事件。

这一样适用于键盘和鼠标事件。当鼠标被点击时,JavaScript 引擎会获得一个点击事件,把它放在事件队列中,只有当执行栈为空时才会执行它的处理程序。

你能够经过把下面的代码复制到你的浏览器控制台,轻松地看到这个过程:

function documentClickHandler() {
  console.log('CLICK!!!');
}

document.addEventListener('click', documentClickHandler);

function a() {
  const fiveSecondsLater = new Date().getTime() + 5000;
  while (new Date().getTime() < fiveSecondsLater) {}
}

a();
复制代码

while 循环只是让引擎忙碌五秒钟,不用太担忧。在这五秒钟内开始点击文档上的任何地方,你会看到没有任何东西被记录到控制台。

当五秒钟过去,执行栈为空时,第一次点击的处理程序被调用。

因为这是一个函数,一个函数执行上下文被建立,推送到堆栈,执行,并从堆栈中弹出。而后,第二次点击的处理程序被调用,以此类推。

实际上,setTimeout()(和 setInterval() )的状况也是如此。你提供给 setTimeout() 的处理程序实际上被放在事件队列中。

这意味着,若是你将超时设置为 0,但执行堆栈上还有代码要执行,那么 setTimeout() 的处理程序只有在堆栈为空时才会被调用,这多是许多毫秒以后。

setTimeout(() => {
  console.log('TIMEOUT HANDLER!!!');
}, 0);

const fiveSecondsLater = new Date().getTime() + 5000;
while (new Date().getTime() < fiveSecondsLater) {}
复制代码

注意:被放入事件队列的代码被称为异步的。这是不是一个好的术语是另外一个话题,但人们就是这样称呼它的,因此我想你必须习惯于它。

函数执行上下文步骤

如今咱们已经熟悉了JavaScript程序的执行周期,让咱们再深刻了解一下函数执行上下文究竟是如何建立的。

它发生在两个步骤中:建立步骤和执行步骤。

建立步骤 "设置了一些东西",以便代码能够被执行,而执行步骤其实是执行它。

在建立步骤中发生的两件事很是重要:

  • 肯定 scope.
  • 肯定值。(我将假设你已经熟悉 JavaScript 中的 this 关键字)。

在接下来的两个相应章节中,将分别详细介绍这些内容。

相关文章
相关标签/搜索