原文地址: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()
的执行上下文,由于它的代码尚未彻底执行。
这意味着在同一时间有许多函数执行上下文。然而,在任什么时候候,它们中只有一个在实际运行。
为了跟踪当前正在运行的函数,咱们使用了一个堆栈,其中当前正在运行的函数执行上下文位于栈的顶部。
一旦它执行完毕,它将被从堆栈中弹出,下一个执行上下文的执行将继续,以此类推,直到执行堆栈为空。
这个栈被称为执行栈,以下图所示:
当执行堆栈为空时,咱们以前讨论过的、从未被销毁的全局执行上下文就成为当前运行的执行上下文。
还记得我说过,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 引擎可能正忙于执行代码,因此它将把事件放在一个队列中,称为事件队列。
咱们已经谈过了执行栈,以及一旦相应函数中的代码执行完毕,当前运行的函数执行上下文是如何从堆栈中弹出的。
而后,下一个执行上下文恢复执行,直到它完成,以此类推,直到堆栈为空,全局执行上下文成为当前运行的执行上下文。
当执行栈中有代码要执行时,事件队列中的事件被忽略,由于引擎正忙于执行栈中的代码。
只有当它完成了,而且执行栈是空的,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
.this
关键字)。在接下来的两个相应章节中,将分别详细介绍这些内容。