执行上下文是什么和为什么要了解
execution context,是 Javascript 代码被解析和执行所在环境的抽像概念。作为一个前端工程师,除了码代码,还得要知道代码是如何运行的,比如 Javascript 引擎和执行上下文以及调出堆栈之间是怎么工作的;
类型分类
全局执行上下文
可以这么理解,不在函数中的代码都在全局执行上下文中。在此期间,共发生两件事:
- 创建一个全局对象,这个全局对象就是浏览器中 window;
- 将
this
指针指向这个对象;
注:一个程序中只有一个全局执行上下文对象,不然就乱套了。
函数执行上下文
在函数调用时创建。想想一个程序可以有多少个函数,没人定义,所以函数上下文是没个数限制的。
Eval 函数执行上下文
运行 Eval 函数中的代码,由于此函数不被 Javascript 开发人员待见,所以接下来的内容就不带它玩了。
总结下,不在函数里的就是全局上下文,其他都是函数执行上下文(因为我们不带 Eval 函数执行上下文)。
执行上下文栈(Execution Context Stack)
栈,具有后进先出结构(LIFO, Last In First Out),具有 push 和 pop 功能。代码执行期间所创建的执行上下文就是它存储的。这么一说,它的作用就提现出来了。那存储执行上下文就是执行上下文栈咯。
问题来了,全局执行上下文和函数执行上下文是什么时候进入执行栈中的呢?
- 全局执行上下文:当 Javascript 引擎首次读取脚本时创建并推入当前栈;
- 函数执行上下文:当函数被调用时,Javascript 引擎会创建一个新的执行上下文(因为会有无数个)并推入当前栈;
接下来,Javascript 引擎会执行栈顶端的函数,函数执行完成后就会从栈中弹出,然后执行下一个。
// context-stack-01.js
let name = 'pr';
const firstFunction = () => {
console.log('first function');
secondFunction();
console.log('first function again');
}
const secondFunction = () => {
console.log('second function');
}
firstFunction();
console.log(name);
// first function
// second function
// first function again
// pr
从代码和流程图可见
- 代码在浏览器加载时,引擎创建一个全局执行上下文,入栈;
firstFunction
函数调用时(此时函数内部代码还未执行),引擎创建一个新的函数执行上下文,然后入栈;- 接着
secondFunction
函数调用时(此时函数内部代码还未执行),引擎创建一个新的函数执行上下文,然后入栈; secondFunction
函数执行完后,函数执行上下文出栈(销毁);- 接着
firstFunction
函数执行完后,函数执行上下文出栈(销毁); - 最后全部代码执行完后,全局执行上下文出栈(浏览器关闭时);
为了便于理解,用数组模拟下执行上下文栈的行为
// ESC = execution context stack
ESC = [ globalContext ];
ESC.push(functionContext<firstFunction>);
ESC.push(functionContext<secondFunction>);
ESC.pop(); // secondFunction();
ESC.pop(); // firstFunction();
// globalContext, 直到程序结束才被移除
总结
- 执行栈就这样在拥有和失去中度过(感觉好可怜);
- 执行栈和 Javascript 引擎是好兄弟(姐妹);
上下文的属性
变量对象
简称 VO(Variable object),是与执行上下文相关的数据域,存储了在上下文中定义的变量和函数声明。
我们已经知道执行上下文分全局执行上下文和函数执行上下文,既然执行上下文都有变量对象,那对应就有全局执行上下文变量对象和函数执行上下文变量对象。
全局执行上下文变量对象
这个名字有点长,但是如果叫全局对象,你肯定就觉得熟悉,其实两者是一回事,可以使用 this 来访问(浏览器环境)。
浏览器环境 this 指向 window,node 环境 this 指向 global。
函数执行上下文变量对象
变量对象我们知道是 VO,函数执行上下文变量对象可以叫活动对象(activation object,这里简称 AO),这么称呼的原因是当进入函数执行上下文中,这个执行上下文才被激活,此刻属性才能被访问。
作用域链
我们知道代码是一段一段执行的,这里一段代码指的是执行上下文,而一个程序中有一个全局执行上下文和无数个函数执行上下文(Eval 函数执行上下文我们不带它玩)。所以,通常在函数中,查找变量会先从当前执行上下文的 VO 中查找,如果没有就到其父级执行上下文的 VO 查找,直至全局执行上下文的 VO。这流程所形成的链表就是作用域链(Scope Chain)(是不是有点像原型链,毕竟都是链嘛)。
this 指向
Javascript 特有的一套玩法,不同场景下,指向的对象就不同。
- 全局范围 this - 全局对象;
- 函数调用 this - 全局对象;
- 对象中的方法调用 this - 该方法所在的对象(函数别名除外);
- 构造函数调用 this - 新创建的对象;
- 显式设置
apply/call/bind
this - 函数第一个参数(即所绑定对象);
第3点中有个函数别名除外,这里解释下
var name = 'I am window';
let obj = {
name: 'I am obj',
fn: function(str) {
console.log(str, this.name);
}
}
const fn = obj.fn;
obj.fn('obj.fn =>'); // obj.fn => I am obj
fn('fn =>'); // fn => I am window
一个字,谁调用了,this 就指向谁(谁动我打谁,哈哈)。