执行上下文
执行上下文 可以理解为当前代码的执行环境,同一个函数在不同的环境中执行,会因为访问数据的不同产生不一样的结果。
执行上下文分为三种:
- 全局执行上下文:只有一个,程序首次运行时创建,它会在浏览器中创建一个全局对象(
window
对象),使this
指向这个全局对象 - 函数执行上下文:函数被调用时创建,每次调用都会为该函数创建一个新的执行上下文
- Eval 函数执行上下文:运行
eval
函数中的代码时创建的执行上下文,少用且不建议使用
执行上下文栈
执行上下文栈(Execution context stack,ECS),也叫函数调用栈(call stack
),是一种拥有 LIFO
(后进先出)数据结构的栈,用于存储代码执行时创建的执行上下文
由于JS是单线程的,每次只能做一件事情,通过这种机制,我们能够追踪到哪个函数正在执行,其他函数在调用栈中排队等待执行。
JS引擎第一次执行脚本时,会创建一个全局执行上下文压到栈顶,然后随着每次函数的调用都会创建一个新的执行上下文放入到栈顶中,随着函数执行完毕后被执行上下文栈顶弹出,直到回到全局的执行上下文中。
代码实例🌰
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
console.log(color); // red
复制代码
执行过程可以在 devTool
的 call stack
中看到,其中 anonyomus
为全局上下文栈;其余为函数上下文栈
图解:
执行过程:
- 首先创建了
全局执行上下文
,压入执行栈,其中的可执行代码开始执行。 - 然后调用
changeColor
函数,JS引擎停止执行全局执行上下文,激活函数changeColor
创建它自己的执行上下文,且把该函数上下文放入执行上下文栈顶,其中的可执行代码开始执行。 changeColor
调用了swapColors
函数,此时暂停了changeColor
的执行上下文,创建了swapColors
函数的新执行上下文,且把该函数执行上下文放入执行上下文栈顶。- 当
swapColors
函数执行完后,其执行上下文从栈顶出栈,回到了changeColor
执行上下文中继续执行。 changeColor
没有可执行代码,也没有再遇到其他执行上下文了,将其执行上下文从栈顶出栈,回到了全局执行上下文
中继续执行。- 一旦所有代码执行完毕,JS引擎将从当前栈中移除
全局执行上下文
。
注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。
复制代码
使用 ECStack
来模拟调用栈:
ECStack=[]
复制代码
JS第一次执行代码时就会遇到全局代码,执行上下文栈会压入一个全局上下文,我们用 globalContext
表示它,只有当整个应用程序结束的时候,ECStack
才会被清空,所以 ECStack
最底部永远有个 globalContext
:
ECStack.push(globalContext)
复制代码
使用伪代码模拟上述代码行为:
ECStack.push(<changeColor> functionContext);
ECStack.push(<swapColors> functionContext);
// swapColors出栈
ECStack.pop();
// changeColor出栈
ECStack.pop();
复制代码
为了巩固一下执行上下文的理解,我们再来绘制一个例子的演变过程,这是一个简单的闭包例子。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
f1()() // 999
复制代码
使用伪代码模拟上述代码行为:
ECStack.push(<f1> functionContext);
// f1出栈
ECStack.pop();
ECStack.push(<f2> functionContext);
// f2出栈
ECStack.pop();
复制代码
因为f1中的函数f2在f1的可执行代码中,并没有被调用执行,因此执行f1时,f2不会创建新的上下文,而直到f2执行时,才创建了一个新的。具体演变过程如下。
es3版本
es3版本执行上下文内有三个重要属性:
- 变量对象 VO(variable object)
- 作用域链(scope chain)
- this
可以将每个执行上下文抽象为一个对象。
执行上下文的组成代码示例:
executionContextObj = {
scopeChain: { /* 变量对象(variableObject)+ 所有父执行上下文的变量对象*/ },
[variableObject | activationObject]: {
/*函数 arguments/参数,内部变量和函数声明 */
arguments,
...
},
this: {}
}
复制代码
变量对象
变量对象 是与执行上下文相联的数据作用域,用来存储上下文中定义的变量和函数声明。
不同执行上下文中的变量对象也不一样:
- 全局上下文 中的变量对象就是全局对象,在浏览器中就是 window 对象。在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。所有的全局变量和函数都是作为 window 的属性和方法存在。
console.log(this) //window
var a=1 //挂到window上的属性
console.log(window.a) //1
console.log(this.a) //1
复制代码
- 函数执行上下文 中我们用活动对象 AO (activation object) 来表示变量对象,因为变量对象是规范上的或者说是引擎实现上的,在 JavaScript 环境中是不能被直接访问的,只有当函数被调用时,变量对象被激活为活动对象时,我们才能访问到其中的属性和方法。
活动对象就是变量对象,只不过处于不同的状态和阶段而已。
复制代码
作用域链
对于 JavaScript
来说作用域及作用域链的变量查询是通过存储在浏览器内存中的执行上下文实现的。当查找变量时,首先从当前上下文中的变量对象查找,如果没有就会往上查找父级作用域中的变量对象,最终找到全局上下文的变量对象,如果没有就报错。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
关于作用域和作用域链可以参考我的另一篇文章: 掌握JavaScript面试:什么是闭包ÿ