深入了解JS执行过程
文章翻译至: Exploring the Magic of JavaScript: A Deep Dive into Its Execution Process
目录
JavaScript 是 Web 开发中使用的最流行的编程语言之一。它是一种高级编程语言,可以解释和动态类型。JavaScript 的主要特性之一是其事件驱动的架构,它允许它无缝地处理异步代码执行。在本文中,我们将讨论 JavaScript 如何在幕后工作
我们用高级语言编写代码。但我们的浏览器只能理解二进制/机器代码。我们必须以某种方式将这种高级语言转换为机器可理解的语言。
在读取 HTML 代码时,当浏览器遇到任何<script>
标签或任何包含任何事件(如 onmouseover
或 onClick
)的属性时,它会将其发送到其 javascript 引擎(V8等......)
然后,Javascript 引擎创建一个名为 Execution context 的特定环境来执行代码。在执行上下文运行时,解析器解析特定代码,变量和函数存储在内存中,最后生成可执行字节码,最终由 javascript 引擎执行。
主要有两种类型的执行上下文。
1. 全局执行上下文(GEC)
每当 javascript 引擎收到<script>
文件时,它首先会创建一个默认的执行上下文,称为全局执行上下文。它是默认的执行上下文,所有不在函数中的 javascript 代码都会在这里被执行。
2. 函数执行上下文(FEC)
当调用函数时,js 引擎会在全局执行上下文中创建不同类型的执行上下文,称为函数执行上下文,以求值和执行函数中的代码。
每个函数调用都有自己的函数执行上下文,因此会有多个函数执行上下文。
执行上下文是如何创建和工作的?
JS 中的一切都发生在执行上下文中。想象一下一个密封的容器,JS在其中运行。它是一个抽象的概念,包含有关环境的信息。在当前代码中执行。
在容器中,第一个组件是内存组件(内存创建阶段),第二个组件是代码组件(代码执行阶段)。内存组件具有键值对中的所有变量和函数。它也称为可变环境。
代码组件是一次执行一行代码的地方。它也被称为执行线程。
1. 内存创建阶段
在此阶段,执行上下文首先与执行上下文对象相关联。此对象存储了大量重要数据,这些数据将在代码执行阶段使用。
此阶段分为三个阶段:
- 创建变量对象( Variable Object - VO)
它是一种执行上下文中创建的类似对象的容器。它存储执行上下文中定义的变量和函数声明。
在全局执行上下文中,对于使用 var 关键字声明的变量,将向 VO 添加一个指向该变量并设置为undefined
的属性。
对于函数,将一个属性添加到 VO 中,指向该函数,并且该属性存储在内存中。这意味着所有函数声明都将在 VO 中存储和访问,甚至在代码开始运行之前。
另一方面,函数执行上下文不构造 VO,而是生成一个称为argument
对象的类似数组的对象,其中包括提供给函数的所有参数。
在执行代码之前将变量和函数声明存储在内存中的这种过程称为Hoisting(提升)。
- 创建作用域链
每个函数执行上下文都会创建其作用域,可以通过作用域的过程来访问其定义的变量和函数的空间。
当一个函数在另一个函数中定义时,内部函数可以访问外部函数中定义的代码,反之亦然。此行为称为词法范围。
这个范围的概念在 JavaScript 中带来了一个相关的现象,称为closures(闭包)。内部函数可以访问其外部函数的变量,即使外部函数执行完毕后也能继续访问。
js引擎在 最内层函数 > 内部函数 > 全局执行上下文的范围,以解决其中调用的变量和函数的概念被称为: Scope Chain(作用域链)
当 JavaScript 引擎无法解析作用域链中的变量时,它才会停止执行并引发错误。
不过,解析不能向后。也就是说,全局作用域/外部作用域永远无法访问内部函数的变量,除非它们从函数返回。
作用域链就像一个单向玻璃。你可以看到外面,但外面的人看不到你。
- 设置“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}`);
}
首先,脚本被加载到 js 引擎中。然后 js 引擎创建 GEC 并将其设置到执行栈的底部。
变量
name
因为在函数之外定义,因此它位于 GEC 中并存储在其 VO 中。函数也会发生相同的过程(变量a
,b
,c
使用var
创建)。当 js 引擎遇到第一个函数
first
时,会为其创建一个新的 FEC。这个新上下文位于当前上下文(即 GEC)之上。在第一次函数调用的持续时间内,其FEC执行上下文将成为活动上下文(位于栈顶)。在第一个函数中,变量
var a = “hi”
存储在其 FEC 中,而不是 GEC 中。接下来,在第一个函数中调用第二个函数。由于 js 的单线程性质,第一个函数的执行将被暂停。它必须等到第二个函数完成。同样,js 引擎为第二个函数创建了一个新的 FEC,并将其放在堆栈的顶部,使其成为活动上下文。变量
var b=“hey!”
存储在其 FEC (second
所属的FEC)中,接着,第三个函数在第二个函数中调用。FEC被创建并放置在执行栈的顶部。在第三个函数中,变量
var c = “Hello”
存储在其 FEC 中(third
所属的FEC),然后,消息“Hello Victor”
被记录到控制台中。第三个函数已经执行完毕,它的 FEC 从堆栈的顶部被移除,此时第二个函数的 FEC 又变为了活动上下文。在第二个函数中,
"hey! Victor"
被记录到控制台。该函数执行完毕,并且 EC 将从调用栈中弹出。然后第一个函数执行完毕,第一个函数的 EC 堆栈从堆栈中弹出,因此控制返回到代码的 GEC。最后,当整个代码的执行完成时,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()
函数放入称为回调队列或任务队列的队列中:
事件循环: 监视调用栈和回调队列。如果调用栈为空,则事件循环将从队列中获取第一个回调函数,并将其推送到调用栈,从而有效地运行它。
如果调用栈不为空,则事件循环将回调队列中的下一个函数放置到调用栈执行。直到它为空。