摘要:作用域链用于表明上下文的执行顺序。当前上下文执行完毕则出栈,执行下一个上下文。
从一个简单的例子出发
先从一个简单的例子出发(先不涉及异步),看看自己是否大致了解浏览器的执行机制:
console.log(a); var a=1; function foo(a){ console.log(a); var a=2; console.log(a); } foo(a);
undefined 1 2
如果你的预测结果不一样,那你可以看看下面几个常见的误区:
var a=1不是进行了变量提升?为什么第一个打印的结果为 undefined?
答:变量提升只提升变量的声明,并不进行赋值。其中变量提升发生在预编译阶段,此时a=undefined,预编译结束后代码如下
//函数声明和变量声明进行提升,且函数声明优先级更高 function foo(a){ console.log(a); var a=2; console.log(a); } var a; console.log(a); a=1; foo(a);
很明显第一个结果为undefined。
foo函数中参数名和变量名都为a,使用时该以哪一个a为准?
变量声明在顺序上跟在函数声明和形式参数声明之后,同时,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。例子中的var a=2,可以拆分为var a;a=2;其中a=2是对参数a进行赋值。现在我们详细地说一说js的执行机制:
首先你需要理解如下几个概念:
JavaScript中的堆和栈栈(stack) 栈stack为自动分配的内存空间,它由系统自动释放;
堆(heap) 堆heap是动态分配的内存,大小不定也不会自动释放。
JavaScript 中的变量分为基本类型和引用类型。其中,基本类型存在于栈中,引用类型存在于堆中。在js的执行阶段,当执行到a=2这样的赋值语句时,js引擎线程会先判断2是基本类型还是引用类型,如果它是基本类型,则直接对执行栈中的AO进行赋值a=2(AO会在下面的执行上下文中讲到),若是引用类型,则在堆中存入2,然后用2在堆中的地址对AO进行赋值。执行环境
js的执行环境分为三种:
全局环境(JS代码加载完毕后,进入代码预编译即进入全局环境)
函数环境(函数调用执行时,进入该函数环境,不同的函数则函数环境不同)
eval(不建议使用,会有安全,性能等问题)
js每进入一个执行环境就会创建一个执行上下文,并将它放入执行栈中。执行上下文会在下文讲到。
单线程(同步和异步)js是一门单线程语言,但并不意味着参与js执行过程的线程就只有一个。一个有四个线程参与该过程:
JS引擎线程、事件触发线程、定时器触发线程、HTTP异步请求线程。其中,只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进事件队列,等待JS引擎线程执行。
举一个简单的例子来说:
console.log("script start"); setTimeout(function() { console.log("setTimeout"); }, 0); console.log("script end");
JS引擎主线程按代码顺序执行,当执行到console.log("script start");,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行;
JS引擎主线程执行到setTimeout(function() { console.log("setTimeout"); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行;
JS引擎主线程执行到console.log("script end");,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end;
JS引擎主线程上的任务执行完毕(输出script start和script end)后,主线程空闲,则开始读取任务队列中的事件任务,将该任务队里的事件任务推进主线程中,按任务队列顺序执行,最终输出setTimeout,所以输出的结果顺序为script start script end setTimeout;
如果还不清楚,可以看看下图:
首先,这是一个浏览器环境,其中主线程操作堆和执行栈,而RunTime中存在着许多web API,当主线程读取到setTimeOut等API时,它会交给其他线程来处理(setTimeOut则是定时器触发线程),定时器触发线程会先将setTimeOut中的回调函数存放在event table中,当满足触发条件时(如上面的4ms),就将回调函数推入事件队列(callback queue)中,等待主线程空闲(执行栈中为空),回调函数则被推入执行栈中进行执行。
执行上下文可理解为当前的执行环境,与该运行环境相对应。js引擎每进入一个环境就会创建相应的执行上下文,创建执行上下文的过程中,主要做了以下三件事件,如图:
其中,变量对象VO(Variable object)用于存放声明后的变量、函数和形参。我们举一个例子来说:
var a = 10; function test(x) { var b = 20; }; test(30);
对应的变量对象是:
// 全局上下文的变量对象 VO(global) = { a: undefined, test:// 是test函数位于堆中的地址 }; // test函数上下文的变量对象 VO(test) = { arguments: { x:undefined, length:1 }, b: undefined };
当预编译结束,js进入解释执行阶段时,VO就会转化为AO(Active object),也就是活动对象。AO中变量和参数的值不再是undefined,它们的值会随着js的逐步执行而发生变化。
作用域链用于表明上下文的执行顺序。上例中的作用域链为:
scopeChain: [VO(test), AO(global)],
我们这里直接使用数组表示作用域链,作用域链的活动对象或变量对象可以直接理解为作用域;
它的第一项永远是当前作用域(当前上下文的变量对象或活动对象);
最后一项永远是全局作用域(全局执行上下文的活动对象);
作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或函数,找到则会停止查找,找不到则一直查找到全局作用域,再找不到则会抛出引用错误。
this指向当前作用域。这里不做过多分析。
执行的三个阶段js的执行分为三个阶段:
语法分析阶段: