[译] JavaScript 运行原理

原文地址:How Does JavaScript Really Work? (Part 2)javascript

JavaScript V8 引擎是如何与内存管理,调用堆栈,线程和事件循环协同工做的。java

上一篇文章中,我简要概述了编程语言的工做原理及 V8 引擎的细节。 这篇文章将涵盖每一个 JavaScript 程序员都必须知道的一些重要概念,不只限于 V8 引擎。程序员

时间复杂度空间复杂度是全部程序员都关注两个问题。 上一篇文章介绍了 V8 的速度和优化部分,以提升 JavaScript 的执行时间,该部分将重点介绍内存管理方面。web

内存

Orinoco logo: V8 的垃圾回收器

  • 每当您在 JavaScript 中定义变量,常量,对象等时,都须要一些地方来存储它。 这个地方就是内存。
  • 当遇到语句 var a = 10 时,内存将分配一个位置来存储 a 的值。
  • 可用内存是有限的,复杂的程序可能包含许多变量和嵌套对象。 所以合理地利用可用内存相当重要。
  • 与像 C 这样须要显式分配和释放内存的语言不一样,JavaScript 提供了自动垃圾收集的功能。 一旦对象/变量脱离上下文而且再也不使用,它占用的内存将被回收并返回空闲内存池。
  • 在 V8 中,垃圾收集器名为 Orinoco,它能高效地完成上面介绍的过程。

标记扫描算法

标记扫描算法

为了肯定能够从内存中安全删除的对象,使用了这种简单有效的算法。 该算法的名称描述了其工做原理;将对象标记为可访问/不可访问,并清除不可访问的对象。算法

垃圾收集器会按期从根对象或全局对象开始,而后遍历它们所引用的对象,而后再遍历这些引用所引用的对象,依此类推。而后清除全部没法访问的对象。chrome

内存泄漏

尽管垃圾回收是高效的,但这并不意味着开发人员能够对内存管理无论不顾。 管理内存是一个复杂的过程,肯定哪一块内存是不须要的不能彻底依赖算法。编程

内存泄漏是指程序使用过的一部份内存,如今再也不使用了,但这些内存并未返回到内存池。浏览器

如下是一些致使程序内存泄漏的常见错误。安全

全局变量:若是您持续地建立全局变量,即便您不使用它们,它们也会在程序执行过程当中始终存在。若是这些变量是深层嵌套的对象,则会浪费大量内存。bash

var a = { ... }
var b = { ... }
function hello() {
  c = a;  // this is the global variable that you aren't aware of. } 复制代码

若是您访问未声明的变量,则将在全局范围内建立一个变量。 在上面的示例中,c 是您没有使用 var 关键字隐式建立的全局变量/全局对象。

Event Listeners: This may happen when you create a lot of event listeners to make your website interactive or maybe just for those flashy animations and forget to remove them when the user moves to some other page in your single page application. Now when the user moves back and forth between these pages, these listeners keep adding up.

事件监听:假设您在网页中建立了大量监听事件来实现交互或动画,当用户跳转到单页应用程序中的其余页面时,而您忘记了移除它们,就可能会发生内存泄漏。 由于当用户在这些页面之间来回跳转时,这些事件监听会不断累加。

var element  = document.getElementById('button');
element.addEventListener('click', onClick)
复制代码

定时器:当引用这些闭包中的对象时,垃圾收集器将永远不会清除被引用的对象,直到闭包自己被清除。

setInterval(() => {
  // reference objects
}
// now forget to clear the interval.
// you just created a memory leak!
复制代码

被删除的 DOM 节点:有点相似于全局变量内存泄漏,而且很是常见。 DOM 节点存储在 Object Graph memory 和 DOM tree 中。经过一个示例能够更好地说明这种状况。

var terminator = document.getElementById('terminate');
var badElem = document.getElementById('toDelete');
terminator.addEventListener('click', function()  {memory
  badElem.remove();
});
复制代码

在点击 id ='terminate' 的按钮后,toDelete 节点将从 DOM tree 中删除。 可是,因为事件监听中引用了该对象 badElem,所以会认为该对象分配的内存仍在被使用中。

var terminator = document.getElementById('terminate');
terminator.addEventListener('click', function()  {
  var badElem = document.getElementById('toDelete');
  badElem.remove();
});
复制代码

如今,badElem 变量被定义为一个局部变量,当删除操做完成时,垃圾回收器能够回收它的内存。

调用堆栈

堆栈是遵循 LIFO(后进先出)方法来存储和访问数据的数据结构。对 JavaScript 引擎来讲,堆栈用于记住函数中最后执行的命令的位置。

function multiplyByTwo(x) {
  return x*2;
}
function calculate() {
  const sum = 4 + 2;
  return multiplyByTwo(sum);
}
calculate()
var hello = "some more code follows"
复制代码
  1. 首先,引擎知道程序中有两个函数。
  2. 运行calculate()函数。
  3. 在调用堆栈上弹出calculate函数并计算总和。
  4. 运行multiplyByTwo()函数。
  5. 在调用栈上弹出multiplyByTwo函数,并执行算术运算x * 2。
  6. 返回该值时,从堆栈中弹出multiplyByTwo(),而后返回calculate()函数。
  7. calculate()函数返回时,从堆栈中弹出calculate,而后继续执行代码。

堆栈溢出

堆栈溢出

在不弹出堆栈的状况下,连续压栈量取决于堆栈的大小。 若是您继续压栈达到堆栈容量的极限,将致使堆栈溢出,此时 chrome 浏览器 会报错,同时生成堆栈快照,也称为堆栈帧

递归:当函数调用自身时,称为递归。 在您想减小算法执行的时间(时间复杂度),可是其它方法理解和实现起来很复杂时,递归就显得很是有用。

在下面这个示例中,return 1 语句永远不会被执行,而且 lonely 函数会不断调用自身而不会返回,最终致使堆栈溢出。

function lonely() {
 if (false) {
  return 1;  // the base case
 }
 lonely();   // the recursive call
}
复制代码

JavaScript 为何是单线程的?

多个线程表示您能够同时独立执行程序的多个部分。 肯定一种语言是单线程仍是多线程的最简单方法是看它拥有有多少个调用堆栈。 JS 只有一个,因此它是单线程语言。

您可能会想这不是瓶颈吗? 若是我运行多个耗时的操做,也称为阻塞操做(如HTTP请求),那么该程序将必须等待每一个操做的响应完成后,再执行下一个操做。

为了解决这个问题,咱们须要一种异步执行任务的方法来解决单线程的弊端,事件循环为此而生。

事件循环

目前,上面提到的大部份内容都被 V8 囊括,可是若是您在 V8 代码库中搜索诸如 setTimeout 或 DOM 之类的内容的实现,那你可能什么都找不到。由于除了运行时引擎外,JS 还包含 Web API 模块,这些 API 是浏览器提供来扩展 JS 功能的。

您能够在这个视频中了解个中详情。

结语

编写一门编程语言还有不少工做要作,并且每一年它的实现方式可能也在不断变化。 我但愿这两篇文章能够帮助您成为更好的 JS 程序员,并接纳 JS 怪异的部分。 如今,您应该习惯使用V8事件循环调用堆栈等术语。

大多数和我同样人学习 JS 都是从学习一个新的框架开始。 我以为咱们如今应该对引擎内部执行的东西有一些了解,这将有助于咱们编写出更好的代码。

相关文章
相关标签/搜索