本文着重于对JavaScript代码的执行机制进行剖析和说明。
代码类型
在JavaScript中,可执行的JavaScript代码分三种类型:
- 函数体代码(Function Code)
即用户自定义函数中的函数体JavaScript代码。 - 全局代码(Global Code)
即全局的、不在任何函数里面的代码,包括写在文件以及直接嵌入在HTML页面中的JavaScript代码等。 - 动态执行代码(Eval Code)
即使用eval()函数动态执行的JavaScript代码。
不同类型的代码其执行机制也有所不同。
线程模型
JavaScript引擎线程
JavaScript语言规范没有包含任何线程机制,客户端的JavaScript也没有明确定义线程机制,但浏览器端的JavaScript引擎基本上还是严格按照”单线程”模型去执行JavaScript代码的。
究其原因,应该还是为了简单吧,因为JavaScript的主要用途是与用户交互以及操作DOM,如果采用多线程,将会带来很复杂的同步问题。
JavaScript引擎是基于事件驱动的,引擎维护着一个事件队列,JavaScript引擎线程所作的就是不断的从事件队列中读取事件,然后处理事件,这个过程是循环不断的,所以整个的运行机制又称为事件循环(Event Loop)。
虽然HTML5提出的Web Worker允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,所以可以说,Web Worker并没有改变JavaScript单线程的本质。
浏览器的其他线程
JavaScript引擎是单线程的,但浏览器本身是多线程的,JavaScript引擎线程只是浏览器里的一个线程,除此之外,浏览器通常至少还有以下四类线程:
- GUI渲染线程
在JavaScript中,GUI渲染操作也是异步的,DOM操作的代码会在GUI渲染线程的事件队列中生成一个任务,GUI渲染处理由GUI渲染线程而不是JavaScript引擎线程执行。
但需要注意 GUI渲染线程与JavaScript引擎线程是互斥的,当JavaScript引擎线程执行时GUI渲染线程会被挂起,而GUI渲染线程执行时,JavaScript引擎线程肯定不在执行状况。 - 用户交互线程
当一个用户入力事件(鼠标点击,键盘入力等)被触发时该线程会把事件添加到JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 - 网络通信线程
网络通信线程负责网络通信,并且在服务器回复之后会把事件添加到JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。 - 定时器线程
定时触发(setTimeout 和 setInterval)是由浏览器的定时器线程执行定时计数,然后在定时时间结束时把定时处理函数的执行代码插入到 JavaScript引擎线程的事件队列的队尾,等待JavaScript引擎线程的处理。
所以用这两个函数的时候,实际的执行时间是大于指定时间的,并不保证能准确定时。
执行上下文
不仅仅是JavaScript,解释性语言都存在执行上下文(execution context,也称执行环境,运行上下文)这样一个概念。
执行上下文定义了执行中的代码有权访问的其他数据,决定了它们各自的行为。
分类
执行上下文大致可以分为两类:
- 全局上下文(global context)
最外围的一个执行上下文,全局上下文取决于执行环境,在浏览器中则是window。 - 局部上下文(函数执行上下文)
每个函数都有自己的执行上下文,当执行进入一个函数时,函数的执行上下文就会被推入一个执行上下文栈的顶部并获取执行权。
当这个函数执行完毕,它的执行上下文又从这个栈的顶部被删除,并把执行权并还给之前执行上下文。这就是JavaScript程序中的执行流。
而由eval()函数动态执行的代码运行在调用者的执行上下文之中,不会产生新的执行上下文。
与作用域的关系
执行上下文与作用域很容易被混淆成同一个东西,事实上两者的概念是完全不同的。
以函数为例,函数的执行上下文是完全与函数代码运行相关联的动态存在,相关代码运行结束了,与之相关联的执行上下文也就被释放了,而作用域更多的是一个静态的概念,如闭包作用域就与代码是否正在执行没有关系。
执行上下文与作用域的关联是:执行上下文会为执行中的代码维护一个作用域链,里面包含了代码可以访问的各个名字对象,当代码中出现访问某个标识符(变量名,函数名等),JavaScript引擎会根据这个作用域链顺序进行查找,如果存在则直接返回该对象,如果整个链里都不存在则会产生异常。
构成
执行上下文只是一个抽象概念,在具体JavaScritp引擎实现中,它会被表示为一个至少包含以下三个属性的内部对象:
- 变量对象(Variable Object)
环境中定义的所有变量和函数(函数声明,函数形参)都保存在这个对象中。 - 作用域链
一个由变量对象组成单向链表,用于变量或其他标识符查找,本质上,它是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
详细说明请参考执行上下文的作用域链 - this
this被赋予函数所属的Object,具体来说:
- 当函数被作为某个对象的方法被调用时,this代表该对象。
- 全局函数和匿名函数里的this代表全局对象的window,但如果是strict模式,this则是undefined。
- 构造函数中的this代表构造函数所创建的对象。
- apply()和call()方法在参数里明确指示函数执行时的this对象。
流程
在JavaScript中,程序代码是在执行上下文环境里被执行的,这包括两个阶段:
- 为代码创建执行上下文 包括
- 创建变量对象
- 创建arguments对象,初始化参数名称和值
- 扫描代码中的函数声明,将该函数对象放入变量对象
- 扫描代码中的变量声明,将该变量对象放入变量对象,这个阶段变量的赋值语句并不执行,所以所有变量的值都是undefined
- 初始化作用域链
- 判断this对象
- 创建变量对象
- 执行代码 在当前上下文上解释执行代码
从以上记述可以看到, 函数执行之前,函数的代码首先会被全部扫描,内部声明的函数,变量不分位置,全部事先登记到执行上下文的变量对象里。这为JavaScript语言带来了一个提升(Hoisting)的概念,即后面定义的名字,前面的代码也可访问。
示例代码如下:
(function() { log(); //正常输出hello,因为下面定义的log()函数的作用域被提升到顶端 v(); //异常,因为下面定义的v变量的作用域虽被提升到顶端但值为undefined function log() { console.log('hello'); } var v = log; }());
异步处理
JavaScritp的异步处理是通过回调函数实现的,即通过事件队列,在主线程执行完当前的任务,主线程空闲后轮询事件队列,并将事件队列中的任务(回调函数)取出来执行。
异步处理大致有以下几大类型,不同的异步处理由不同的浏览器内核模块调度执行,调度会将相关回调添加到事件队列中。
- DOM事件回调
- 定时触发回调
- 网络通信回调
- Promise回调
其中,Promise的优先级最高,排在其他所有类型的异步处理之前,而Promise以外的异步处理之间并没有优先级差别。