了解JavaScript运行机制有助于我们避免bug,并写出高性能的代码,当然还有一大用处就是有助于我们通过造火箭环节的面试。
具体而言你会搞清楚以下问题:
而产生的『后果』是,你可以应对几乎所有的JavaScript作用域、闭包、执行等层面的面试题,还有一个可能的后果,就是面对复杂度不是那么高的代码时,你的脑子中会自己把执行过程像放动画一样过一遍(虽然这个动画也不非常准确)。
在了解JavaScript运行机制之前,我们需要搞清楚几个主要概念,这有助于我们接下来的理解。
赋予一段代码意义的正是JavaScript引擎,目前JavaScript引擎有许多种:
而最为大家熟知的无疑是V8引擎,他用于Chrome浏览器和Node中。
V8引擎由两个主要部件组成:
想让JavaScript真正运作起来,单单靠JavaScript Engine是不够的,JavaScript Engine的工作是编译并执行 JavaScript 代码,完成内存分配、垃圾回收等,但是缺乏与外部交互的能力。
比如单靠一个V8引擎是无法进行ajax请求、设置定时器、响应事件等操作的,这就需要JavaScript运行时(JavaScript Runtime)的帮助,它为 JavaScript 提供一些对象或机制,使它能够与外界交互。
比如,虽然Chrome和node都是用了V8引擎,但是他们的运行时却不同,比如process、fs浏览器都无法提供。
一段JavaScript代码的运行我们可以分为两个阶段:
编译阶段:
执行阶段
本文的重点在于执行阶段。
JavaScript并非简单的一行行解释执行,而是将JavaScript代码分为一块块的可执行代码块进行执行,那么如何划分代码块?
目前有三类代码块:
我们先看一个简单的例子:
看到这个例子思考一下JavaScript应该是如何执行它的?
如果你头脑里没有任何细节的概念,那么接下来的内容就很适用于你了。
我们之前提到过JavaScript引擎两个重要部分:
而上面的代码声明正是被存放在『堆』中。
此时虽然变量和函数都被声明了,但是函数还没有执行,我们现在执行say
函数。
那么接下来又会发生什么呢?
调用栈(Call Stack)这个概念对于经常调试JavaScript代码的同学应该不陌生。
我们声明的函数与变量被储存在『内存堆』中,而当我们要执行的时候,就必须借助于『调用栈』来解决问题。
如果熟悉数据结构的同学应该知道,栈是一个基础的数据结构,它的特点就是先进后出。
我们仍然看这个例子,当say
函数被调用的时候,他会被压入栈底。
那么是不是将函数压入栈内就结束了?肯定没有这么简单,这里需 要在引入一个概念,执行上下文(execution context)。
执行上下文在代码块执行前创建,作为代码块运行的基本执行环境,那么执行上下文分为几种?
前面我们提到过,JavaScript中有三种可执行代码块,当然也对应着三种执行上下文。
肯定会有人好奇,这个执行上下文到底包含哪些东西呢,他是如何运行的呢?
执行上下文分为两个阶段:
我们主要讨论创建阶段,执行阶段的主要工作就是分配变量
执行上下文的创建阶段主要解决以下三点:
你可能在一些过时的教材或者文章中见过变量对象(VO)这种说法,它的意思与词法环境类似,但是那是ES3的标准,现在早已经改了,改变的原因讨论如下Why variable object was changed to lexical environment in ES5?
伪代码如下:
我们应该知道this的指向是在代码执行阶段确定的,所谓的『代码执行阶段』正是『执行上下文的创建阶段』。
默认情况下this指向全局对象,比如浏览器中的window.
此外可能存在隐式绑定的情况,比如通过对象调用函数:
这个时候this指向对象。
然后就是显示绑定对象(call apply bind)等,最后优先级最高的就是new调用构造函数生成一个对象。
词法环境分为三大类:
词法环境本身包括两个部分:
对于『环境记录器』而言,它又分为两个主要的环境记录器类型:
比如我们在全局声明一个函数:
那么他的词法环境可以这样表示(下图我们省略了this绑定、变量环境等信息,便于理解):
变量环境的定义在es5标准和es6标准是略有不同的,我们采用es6的标准
变量环境也是一个词法环境,但不同的是词法环境被用来存储函数声明和变量(let 和 const)绑定,而变量环境只用来存储 var 变量绑定。
在了解了这么多概念之后,我们就可以把本节开头的例子再拓展一下:
我们就一步步复盘一下上述代码是如何执行的(不考虑解析、预解释等操作,只考虑执行):
name
和函数声明say
被白存在堆中。全局上下文的伪代码如下:
示意图:
say函数的执行上下文伪代码如下:
play函数的执行上下文伪代码如下
示意图:
将上下文中的变量赋值,然后执行代码,执行完毕栈顶的play函数后弹出,接着执行say函数,完毕后弹出。
我们通过本文了解了相关的JavaScript执行机制,现在可以回答这几个问题了。
在创建可执行上下文的时候,根据代码的执行条件,来判断分别进行默认绑定、隐式绑定、显示绑定等。
可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,这些引用串联起来一直指向全局的词法环境,因此形成了作用域链。
可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,因此形成了闭包。
参考
← 如何实现一个Event HTTP协议 →