JavaScript 到底是如何工作的?
一、工作原理
JavaScript到底是:
- 同步还是异步?
- 单线程还是多线程?
-
JavaScript 中的一切都发生在
Execution Context (执行上下文)中
- 您可以假设这个执行上下文 是一个大盒子或一个容器,在其中执行整个 JavaScript 代码。
- 这个大盒子里有两个组件:
- Memory(内存组件):这是所有变量和函数存储为键值对的地方。这个**“内存组件”也称为变量环境**。因此,它是一种环境,其中所有这些变量和函数都存储为键值对。
- Code(代码组件):这是代码逐行执行的地方。这个“代码组件”也称为执行线程。所以,这个执行线程是一个单线程,整个代码一次只执行一行。
-
结论:JavaScript 是一种同步单线程语言。
- 单线程 意味着 JavaScript 一次只能执行一个命令。
- 同步单线程 意味着 JavaScript 一次只能以特定顺序每次执行一个命令。这意味着它只能在当前行完成执行后转到下一行。这就是同步单线程的意思。
很惊诧吧,实际 javascripts 有单线程 event loop 大循环来完成很多不可思议的事情。
二、实际工作过程分析
JavaScript 代码是如何执行的?
当你运行 JavaScript 代码时会发生什么?
会创建一个Execution Context(执行上下文)。
让我们使用实际的代码来举个例子:
1var n = 2;
2function square(num) {
3 var ans = num * num;
4 return ans;
5}
6var square2 = square(n);
7var square4 = square(4);
-
执行上述代码时,会创建 一个执行上下文
-
此执行上下文分两个阶段创建:
-
一、创建:
创建阶段也称为内存创建阶段
。这是一个非常关键的阶段。
-
在内存创建的第一阶段,JavaScript 会为所有的变量和函数分配内存。
-
首先 JavaScript 遇到
var n = 2;
,它就会分配内存给n
.当为
n
分配内存时,它会先存储一个特殊值undefined
。undefined
在 JavaScript 中被视为特殊的占位符。 -
在遇到 function
square(num)
时,它也会为这个 function () 分配内存。 -
在为 function() 分配内存的情况下,它将该函数的整个代码存储在内存空间中。
-
后面为两个变量
square2
和square4
分配了内存,存储的同样是undefined
。 -
为了完成这个创建阶段,JavaScript 会逐行从上到下遍历扫描代码。
-
-
二、代码执行:
- 扫描完毕,现在 JavaScript 再次逐行运行整个程序。
- 当它遇到时
var n = 2;
,它实际上将2
作为值n
放入内存组件中。 - 当它遇到 的函数定义时
square(num)
,它没有什么可执行的,所以 JavaScript 简单地跳过了。 - 当它遇到 时
var square2 = square(n);
,我们现在正在调用一个函数。 - 函数是 JavaScript 的核心。它们在 JavaScript 中的行为与在任何其他语言中的行为非常不同。
- 每当调用一个函数时,都会创建一个全新的Execution Context(执行上下文)。
- 因此,从技术上讲,在整个**Execution Context 的代码组件中又创建了一个全新的Execution Context ** 。
- 这个新的内部 Exection Context 执行上下文也有它自己的内存组件和代码组件。
- 现在在内部发生的事情是:
- 在这种情况下,有 2 个变量,即
num
(参数)和ans
。 - 所以内存将分配给
num
和ans
。 - 在第 1 阶段,与外部执行上下文一样,
undefined
将分配给num
和ans
。 - 现在进入阶段 2(代码执行阶段),参数的值被分配给参数。因此,在我们调用函数的语句
var square2 = square(n);
时,我们将参数n
的值 2 传递给函数square(num)
,并且该参数的值替换了内部执行上下文中num
的内存组件中的占位符undefined
。 - 计算后
num * num
,将值存储在 中ans
。 - 在遇到 时
return ans;
,将存储的值ans
返回到调用的位置,并且此内部执行上下文结束。当它结束时,内部执行上下文实际上被删除了。
- 在这种情况下,有 2 个变量,即
- 现在,遇到下一行时遵循相同的过程
var square4 = square(4);
。 - 最后一行成功执行后,整个执行上下文也被删除。这种*“整体”*执行上下文也称为全局执行上下文。
-
-
那么,JavaScript 是如何管理这种链条式的 ** 执行上下文 ** 的呢?
- 它实际上在后台管理一个堆栈。
- 此堆栈也称为Call Stack(调用堆栈)。
- GEC(Global Execution Context 全局执行上下文)始终位于此堆栈的底部。
- 每当创建一个新的执行上下文时,它就会被压入这个堆栈,并在完成其目的时被弹出。
- 控件与此调用堆栈的最顶部元素保持一致。
- 此调用堆栈仅用于管理执行上下文。
- 在成功执行最后一条语句时,调用堆栈被清空。
调用堆栈维护执行上下文的*执行顺序。*
给个图更好理解:
调用堆栈具有以下花哨的名称,也可以通过这些名称来引用它:
- Execution Context Stack(执行上下文堆栈)
- Program Stack(程序栈)
- Control Stack(控制堆栈)
- Runtime Stack(运行时堆栈)
- Machine Stack(机器堆栈)
这样大家就理解了吧,这样后面的变量提升 Hoisting 就很好理解了。