照片来源于Unsplash上的Samuel Zellerjavascript
在这篇文章的第一部分,我简要概述了编程语言的通常工做机制,并深刻探讨了 V8 引擎的管道。第二部分将介绍一些更重要的概念,这些概念是每个 JavaScript 程序员都必须了解的,而且不只仅和 V8 引擎有关。 对于任何一个程序员来讲,最关注的两个问题无非就是:时间复杂度和空间复杂度。第一部分介绍了 V8 为改进 JavaScript 执行时间所作的速度提高和优化,第二部分则将着重介绍内存管理方面的知识。java
Orinoco 的 logo:V8 的垃圾回收器程序员
var a = 10
的时候,内存会分配一个位置用于存储 a
的值标记和清除算法算法
咱们一般会使用这种简单有效的算法来断定能够从内存堆中安全清除的对象。算法的工做方式正如其名:将对象标记为可得到/不可得到,并将不可得到的对象清除。 垃圾回收器周期性地从根部或者全局对象开始,移向被它们引用的对象,接着再移向被这些对象引用的对象,以此类推。全部不可得到的对象会在以后被清除。chrome
虽然垃圾回收器很高效,可是开发者不该该就此将内存管理的问题束之高阁。管理内存是一个很复杂的过程,哪一块内存再也不须要并非单凭一个算法就能决定的。 内存泄漏指的是,程序以前须要用到部份内存,而这部份内存在用完以后并无返回到内存池。 下面是一些会致使你的程序出现内存泄漏的常见错误: 全局变量:若是你不断地建立全局变量,无论有没有用到它们,它们都将滞留在程序的整个执行过程当中。若是这些变量是深层嵌套对象,将会浪费大量内存。编程
var a = { ... }
var b = { ... }
function hello() {
c = a; // 这是一个你没有意识到的全局变量
}
复制代码
若是你试图访问一个此前没有声明过的变量,那么将在全局做用域中建立一个变量。在上面的例子中,c
是没有使用 var
关键字显式建立的变量/对象。浏览器
事件监听器:为了加强网站的交互性或者是制做一些浮华的动画,你可能会建立大量的事件监听器。而用户在你的单页面应用中移向其余页面时,你又忘记移除这些监听器,那么也可能会致使内存泄漏。当用户在这些页面来回移动的时候,这些监听器会不断增长。安全
var element = document.getElementById('button');
element.addEventListener('click', onClick)
复制代码
Intervals 和 Timeouts:当在这些闭包中引用对象时,除非闭包自己被清除,不然不会清除相关对象。数据结构
setInterval(() => {
// 引用对象
}
// 这时候忘记清除计时器
// 那么将致使内存泄漏!
复制代码
移除 DOM 元素:这个问题很常见,相似于全局变量致使的内存泄漏。DOM 元素存在于对象图内存和 DOM 树中。用例子来解释可能会更好:多线程
var terminator = document.getElementById('terminate');
var badElem = document.getElementById('toDelete');
terminator.addEventListener('click', function() {memory
badElem.remove();
});
复制代码
在你经过 id = ‘terminate’
点击了按钮以后,toDelete
会从 DOM 中移除。不过,因为它仍然被监听器引用,为这个对象分配的内存并不会被释放。
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 浏览器将会抛出一个错误以及被称为栈帧的栈快照。
递归:递归指的是函数调用自身。递归能够大幅度地减小执行算法所花费的时间(时间复杂度),不过它的理解和实施较为复杂。 下面的例子中,基本事件永远不会执行,lonley
函数在没有返回值的状况下不断地调用自身,最终会致使栈溢出。
function lonely() {
if (false) {
return 1; // 基本事件
}
lonely(); // 递归调用
}
复制代码
一个线程表明着在同一时间段内能够单独执行的程序部分的数目。要想查看一门语言是单线程的仍是多线程的,最简单的方式就是了解它有多少个调用栈。JS 只有一个,因此它是单线程语言。 这样不是会阻碍程序运行吗?若是我运行多个耗时的阻塞操做,例如 HTTP 请求,那么程序必须得在每个操做获得响应以后才能执行后面的代码。 为了解决这个问题,咱们须要找到一种能够在单线程下异步完成任务的办法。事件循环就是用来发挥这个做用的。
到如今为止,咱们谈到的内容大多包含在 V8 里面,可是若是你去查看 V8 的代码库,你会发现它并不包含例如 setTimeout 或者 DOM 的实现。事实上,除了运行引擎以外,JS 还包括浏览器提供的 Web API,这些 API 用于拓展 JS。 关于事件循环的概念,菲利普·罗伯茨讲得比我更好,能够看下面这段视频: player.youku.com/embed/XNDIw…
[译者注:原视频来自 YouTube]
关于制做一门编程语言,其实还有不少内容,而且语言的实如今这些年也是不断变化的。我但愿这两篇博客能够帮助你成为一名更好的 JS 程序员,而且接受 JS 中那些晦涩难懂的内容 。对于诸如“V8”,“事件循环”,“调用栈”这样的术语,你如今应该熟悉了。 大部分的学生(好比我)是从一个新的框架起步,以后再去学习原生 JS。如今他们应该熟悉代码背后发生的事情了,反过来,这将帮助他们写出更好的代码。