JavaScript 执行上下文和执行栈

3,706 阅读5分钟

执行上下文是什么和为什么要了解

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 就指向谁(谁动我打谁,哈哈)。

你可以

上一篇:Javascript 堆、栈和队列

下一篇:Javascript 函数节流