1.什么是执行上下文
JavaScript是一个单线程语言,意味着同一时间只能执行一个任务。当JavaScript解释器初始化执行代码时, 它首先默认进入全局执行环境(execution context),从此刻开始,函数的每次调用都会创建一个新的执行环境。
2.执行环境的分类
- 全局环境——JavaScript代码运行时首次进入的环境。
- 函数环境——当函数被调用时,会进入当前函数中执行代码。
- Eval——eval内部的文本被执行时(因为eval不被鼓励使用,此处不做详细介绍)。
3.执行上下文栈
概念:
当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文会构成了一个执行上下文栈(Execution context stack,ECS)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。
代码在执行过程时遇到以上三种执行环境的代码时,都会生成一个对应的执行上下文,压入执行上下文栈中,当栈顶的上下文执行完毕之后,会自动出栈。下面用一个例子说明,可参考上面的执行环境栈图片
var a = 1; function fn1() { function fn2() { console.log(a); } fn2(); } fn1();
第一步,全局执行上下文入栈。
第二步,遇到fn1(),执行代码,创建自己的执行上下文,入栈。
第三步,fn1的上下文入栈之后,接着执行其中的代码,遇到fn2(),创建自己的执行上下文,入栈。
第四步,在fn2的执行上下文中未创建新的执行上下文,代码执行完毕之后,fn2的执行上下文出栈。
第五步,fn2的执行上下文出栈之后,继续执行fn1的可执行代码,也未创建新的执行上下文,出栈。这个时候栈中只剩下全局执行上下文了。
有5个需要记住的关键点,关于执行栈(调用栈):
- 单线程。
- 同步执行。所有的执行上下文都得等到栈顶的执行之后才能顺序执行
- 只有一个全局执行上下文。
- 函数上下文是无限制的。
- 每次函数被调用时都会创建新的执行上下文,包括调用自己
3.执行上下文的构成
三个重要的属性,变量对象(Variable object,VO),作用域链(Scope chain)和this。这三个属性跟代码运行的行为有很重要的关系
变量对象(Variable object)
变量对象(缩写为VO)是一个与执行上下文相关的特殊对象
变量对象是与执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。也就是说,一般VO中会包含以下信息:
- 变量 (var, Variable Declaration);
- 函数声明 (Function Declaration, FD);
- 函数的形参
在JavaScript解释器内部,每次调用执行上下文,分为两个阶段:
创建阶段(此时函数被调用,但未执行内部代码):
- 设置[[Scope]]属性的值
- 设置变量对象VO,创建变量,函数和参数。
- 设置this的值。
激活/代码执行阶段:
在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值和函数的引用。
创建阶段
1.根据函数的参数,创建并初始化arguments object。
2.扫描上下文的函数声明:对于找到的函数声明,将函数名和函数引用存入VO中,如果VO中已经有同名函数,那么就进行覆盖。
3.扫面上下文的变量声明:对于找到的每个变量声明,将变量名存入VO中,并且将变量的值初始化为undefined。如果变量的名字已经在变量对象里存在,不会进行任何操作并继续扫描。
要记住:函数扫描是在变量之前。
4.提升(Hoisting)
(function() { console.log(typeof name); // function console.log(typeof another); // undefined var name = ‘Abby‘, another = function() { return ‘Lucky‘; }; function name() { return ‘Abby‘; } console.log(typeof name); // string console.log(typeof another); // function }())
此时的创建阶段的过程是:
1.函数name和其引用被存入到VO之中。
2.变量name发现在VO之中存在同名的属性,因此忽略。
3.变量another存入到VO之中,并赋值为undefined。(这也是函数表达式不会提升的原因)
此时代码从上到下执行的时候激活阶段的过程是:
1.console.log(typeof name); 此时name在VO中是函数。
2.console.log(typeof another); 此时another在VO中的值是undefined。
3.指出函数name的引用。
4.将name赋值为’Abby’。
5.将another赋值为函数表达式的值。
6.console.log(typeof name); 此时的name由于被函数被字符串赋值覆盖因此是string类型。
7.console.log(typeof another); 此时的another被赋值成函数表达式因此是function类型。
因此理解执行上下文之后也就很好理解了为什么我们能在name声明之前访问它,为什么之后的name的类型值发生了变化,为什么another第一次打印的时候是undefined等等问题了。
5.小结
当javascript代码被浏览器载入后,默认最先进入的是一个全局执行环境。当在全局执行环境中调用执行一个函数时,程序流就进入该被调用函数内,此时JS引擎就会为该函数创建一个新的执行环境,并且将其压入到执行环境堆栈的顶部。浏览器总是执行当前在堆栈顶部的执行环境,一旦执行完毕,该执行环境就会从堆栈顶部被弹出,然后,进入其下的执行环境执行代码。这样,堆栈中的执行环境就会被依次执行并且弹出堆栈,直到回到全局执行环境。