console.log(person);
console.log(fun);
var person = "Eric";
console.log(person);
function fun() {
console.log(person);
var person = "Tom";
console.log(person);
}
fun();
console.log(person);
注:创建变量对象发生在预编译阶段,但尚未进入执行阶段,该变量对象都是不能访问的,因为此时的变量对象中的变量属性尚未赋值,值仍为 undefined,只有进入执行阶段,变量对象中的变量属性进行赋值后,变量对象(Variable Object)转为活动对象(Active Object)后,才能进行访问,这个过程就是 VO –> AO 过程。
概述
js 是一种非常灵活的语言,理解 js 引擎的执行过程对我们学习 javascript 非常重要,但是网上讲解 js 引擎的文章也大多是浅尝辄止或者只局部分析,例如只分析事件循环(Event Loop)或者变量提升等等,并没有全面深入的分析其中过程。所以我一直想把 js 执行的详细过程整理成一个较为详细的知识体系,帮助我们理解和整体认识 js。
在分析之前我们先了解以下基础概念:
- javascript 是单线程语言 在浏览器中一个页面永远只有一个线程在执行 js 脚本代码(在不主动开启新线程的情况下)。 - javascript 是单线程语言,但是代码解析却十分的快速,不会发生解析阻塞。 javascript 是异步执行的,通过事件循环(Event Loop)的方式实现。
下面我们先通过一段较为简单的代码(暂不存在事件循环(Event Loop))来检验我们对 js 引擎执行过程的理解是否正确,如下:
我们可以先分析上面的代码,按自己的理解分析输出的顺序是什么,然后在浏览器执行一次,结果一样的话,那么代表你已经对 js 引擎执行过程有了正确的理解;如果不是,则代表还存在模糊或者概念不清晰等问题。结果我们不在这里进行讨论,我们利用上面简单的例子全面分析 js 引擎执行过程,相信在理解该过程后我们就不难得出结果的,js 引擎执行过程分为三个阶段
1. 语法分析 2. 预编译阶段 3. 执行阶段
语法分析
js 脚本代码块加载完毕后,会首先进入语法分析阶段。该阶段主要作用是:
分析该 js 脚本代码块的语法是否正确,如果出现不正确,则向外抛出一个语法错误(SyntaxError),停止该 js 代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入预编译阶段
语法错误报错如下图:
预编译阶段
js 代码块通过语法分析阶段后,语法正确则进入预编译阶段。在分析预编译阶段之前,我们先了解一下 js 的运行环境,运行环境主要有三种:
- 全局环境(JS 代码加载完毕后,进入代码预编译即进入全局环境) - 函数环境(函数调用执行时,进入该函数环境,不同的函数则函数环境不同) - eval(不建议使用,会有安全,性能等问题)
每进入一个不同的运行环境都会创建一个相应的执行上下文(Execution Context),那么在一段 JS 程序中一般都会创建多个执行上下文,js 引擎会以栈的方式对这些执行上下文进行处理,形成函数调用栈(call stack),栈底永远是全局执行上下文(Global Execution Context),栈顶则永远是当前执行上下文。
函数调用栈
函数调用栈就是使用栈存取的方式进行管理运行环境,特点是先进后出,后进先出。
我们分析下段简单的JS脚本代码来理解函数调用栈:
上面的代码块通过语法分析后,进入预编译阶段,如下图:
创建执行上下文
执行上下文可理解为当前的执行环境,与该运行环境相对应。创建执行上下文的过程中,主要做了以下三件事件,如图:
创建变量对象
创建变量对象主要经过以下几个过程,如图:
创建 arguments 对象,检查当前上下文中的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行,全局环境没有此过程
检查当前上下文的函数声明,按代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则为指向该函数所在堆内存地址的引用,如果存在,则会被新的引用覆盖。
检查当前上下文的变量声明,按代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有该变量名属性,则在该变量对象以变量名建立一个属性,属性值为 undefined;如果存在,则忽略该变量声明
所以函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明。
我们分析一段简单的代码,帮助我们理解该过程,如下
这里我们在全局环境调用 fun 函数,创建 fun 执行上下文,这里为了方便大家理解,暂时不讲解作用域链以及 this 指向,如下:
<test reference>
表示 test 函数在堆内存地址的引用建立作用域链
作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
理清作用域链可以帮助我们理解 js 很多问题包括闭包问题等,下面我们结合一个简单的例子来理解作用域链,如下:
在上面的例子中,当执行到调用 innerTest 函数,进入 innerTest 函数环境。全局执行上下文和 test 函数执行上下文已进入执行阶段,innerTest 函数执行上下文在预编译阶段创建变量对象,所以他们的活动对象和变量对象分别是 AO(global),AO(test)和 VO(innerTest),而 innerTest 的作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,如下:
我们这里直接使用数组表示作用域链,作用域链的活动对象或变量对象可以直接理解为作用域。
在这里我们顺便思考一下,什么是闭包?
我们先看下面一个简单例子,如下:
因为对于闭包有很多不同的理解,包括我看的一些书籍(例如js高级程序设计),我这里直接以浏览器解析,以浏览器理解的闭包为准来分析闭包,如下图:
如上图所示,chrome浏览器理解闭包是foo,那么按浏览器的标准是如何定义闭包的,总结为三点:
确定this指向
在全局环境下,全局执行上下文中变量对象的this属性指向为window;函数环境下的this指向却较为灵活,需根据执行环境和执行方法确定,需要举大量的典型例子概括,本文先不做分析。