原文地址:How Does JavaScript Really Work? (Part 2)javascript
JavaScript V8 引擎是如何与内存管理,调用堆栈,线程和事件循环协同工做的。java
在上一篇文章中,我简要概述了编程语言的工做原理及 V8 引擎的细节。 这篇文章将涵盖每一个 JavaScript 程序员都必须知道的一些重要概念,不只限于 V8 引擎。程序员
时间复杂度和空间复杂度是全部程序员都关注两个问题。 上一篇文章介绍了 V8 的速度和优化部分,以提升 JavaScript 的执行时间,该部分将重点介绍内存管理方面。web
var a = 10
时,内存将分配一个位置来存储 a
的值。为了肯定能够从内存中安全删除的对象,使用了这种简单有效的算法。 该算法的名称描述了其工做原理;将对象标记为可访问/不可访问,并清除不可访问的对象。算法
垃圾收集器会按期从根对象或全局对象开始,而后遍历它们所引用的对象,而后再遍历这些引用所引用的对象,依此类推。而后清除全部没法访问的对象。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" 复制代码
calculate()
函数。calculate
函数并计算总和。multiplyByTwo()
函数。multiplyByTwo
函数,并执行算术运算x * 2。multiplyByTwo()
,而后返回calculate()
函数。calculate()
函数返回时,从堆栈中弹出calculate
,而后继续执行代码。在不弹出堆栈的状况下,连续压栈量取决于堆栈的大小。 若是您继续压栈达到堆栈容量的极限,将致使堆栈溢出,此时 chrome 浏览器 会报错,同时生成堆栈快照,也称为堆栈帧
。
递归:当函数调用自身时,称为递归。 在您想减小算法执行的时间(时间复杂度),可是其它方法理解和实现起来很复杂时,递归就显得很是有用。
在下面这个示例中,return 1
语句永远不会被执行,而且 lonely
函数会不断调用自身而不会返回,最终致使堆栈溢出。
function lonely() { if (false) { return 1; // the base case } lonely(); // the recursive call } 复制代码
多个线程表示您能够同时独立执行程序的多个部分。 肯定一种语言是单线程仍是多线程的最简单方法是看它拥有有多少个调用堆栈。 JS 只有一个,因此它是单线程语言。
您可能会想这不是瓶颈吗? 若是我运行多个耗时的操做,也称为阻塞操做(如HTTP请求),那么该程序将必须等待每一个操做的响应完成后,再执行下一个操做。
为了解决这个问题,咱们须要一种异步执行任务的方法来解决单线程的弊端,事件循环为此而生。
目前,上面提到的大部份内容都被 V8 囊括,可是若是您在 V8 代码库中搜索诸如 setTimeout 或 DOM 之类的内容的实现,那你可能什么都找不到。由于除了运行时引擎外,JS 还包含 Web API 模块,这些 API 是浏览器提供来扩展 JS 功能的。
您能够在这个视频中了解个中详情。
编写一门编程语言还有不少工做要作,并且每一年它的实现方式可能也在不断变化。 我但愿这两篇文章能够帮助您成为更好的 JS 程序员,并接纳 JS 怪异的部分。 如今,您应该习惯使用V8
,事件循环
,调用堆栈
等术语。
大多数和我同样人学习 JS 都是从学习一个新的框架开始。 我以为咱们如今应该对引擎内部执行的东西有一些了解,这将有助于咱们编写出更好的代码。