详解JavaScript执行上下文

130 阅读6分钟

2021_08_26_08_27_IMG_0006.JPG

多巴胺不等于爱情,爱情却一定有多巴胺! --网易热评

执行上下文?

简而言之,字面意思可以得知,上下文就是JS执行时候的环境;

JS中有三种执行上下文类型,分别是全局执行上下文、函数执行上下文,以及Eval函数执行上下文。

上下文类型

全局上下文

这是默认的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:1、创建一个全局的 window 对象(浏览器的情况下),2、并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

函数执行上下文

每当一个函数被调用的时候,就会创建一个上下文,每个函数都有自己的上下文,函数的上下文可以有任意多个

Eval函数执行上下文

执行在 eval 函数内部的代码也会有它属于自己的执行上下文;不过,这个上下文日常开发基本就是凑数的,除了讨论上下文的种类时会谈到,基本用不到,也不要在开发环境中使用,不然出了问题你会欲哭无泪;

🌰

先扔块砖头,热热身

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

这是一段面试文章很常见的代码,相信老鸟一下子就看出了答案,那么,下面这个也是一样的

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

看出二者的区别了吗?先卖个关子,说一下执行栈

执行栈

都知道函数是按照执行栈的方式执行中断的,当有一个函数被调用,这个函数就被push进栈,执行完毕就pop出栈,上下文栈就是这种概念,或者说是同一个东西。

执行栈,也叫调用栈,是一种FIFO结构的数据结构,也即栈,

当JS执行一段脚本,他会创建一个全局上下文并压入栈中,接下来,每执行一次函数,就将一个函数的执行上下文压入栈中,每当栈顶的函数执行完毕后就将执行上下文pop出去;也即--->调用,入栈,执行完毕,出栈

注意:全局上下文是始终存在的,此处写出全局上下文出栈只是为了演示整个生命周期,

先拿一段简单的代码举例:

function methods(a) {
  console.log(a);
}
methods(1)

在这段代码中,依次是

ECStack.push(GlobalContext);
ECStack.push(methodsContext);
ECStack.pop(methodsContext);
ECStack.pop(GlobalContext);

在上述代码中,JS引擎创建了一个GlobalContext全局上下文,并将其压入栈中,遇到methods函数执行的时候,将函数压入栈中,继续执行栈顶元素,此时methods位于栈顶且无其他入栈,当函数执行完毕,出栈,完成整个执行过程。

回到开头,那两块"砖头"的执行调用是怎么样的呢?对于第一块"砖头"来说

ECStack.push(GlobalContext);
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
ECStack.pop(GlobalContext);

第二个也是相似的过程,只不过第二个砖头是存在一个回调函数也就是闭包在里面的,简单写下就是:

ECStack.push(GlobalContext);
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop(GlobalContext);

创建执行上下文

执行上下文被存在一个数据栈中,这个栈保存着代码运行是创建的所有上下文,栈的特点是后进先出(LIFO),也就是说,每创建一个新的上下文,就会将它压入栈的顶部, 当函数执行完毕时,上下文从栈中弹出,控制流程到达栈中的下一个上下文 当JavaScript引擎首次碰到脚本时,会创建一个全局的上下文,并将其压入栈中

如何创建执行上下文:

创建执行上下文分为两个阶段:

1、创建阶段

在创建阶段,会发生三件事:

伪代码:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

说人话就是:

绑定this指向

this的指向是在创建上下文时绑定的, this指向是根据调用函数的对象决定的,如果在全局执行上下文中,this指向全局对象。如果在函数执行上下文中,如果函数是被对象调用的,那么this指向的就是这个对象,

this指定可以参考我之前的这篇文章:call、apply、bind

创建词法环境组件

于词法环境,官方解释:

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

ES6中定义为是一种规范类型。一个词法环境由两部分组成:一个是环境记录器、一个是引用外部词法环境的空值

创建变量环境组件

环境记录器:记录着变量和函数实际声明的位置。

外部词法环境的空值:意味着可以他可以访问它的父级词法环境。

词法环境的类型:有两种,全局环境和函数环境。(对此本人也不是完全了解,就不误导别人👹)

3)变量环境:

变量环境也是一个词法环境,它有着词法环境的一切属性。区别在于:词法环境用来存储函数和变量(let/const)的绑定,而变量环境用来存储var声明的变量绑定。

2、执行阶段

完成变量的分配,执行代码。

总结

  • 函数运行的时,会创建执行上下文,也可以叫做执行环境/执行栈,FIFO 结构,是保存函数调用执行环境的数据结构
  • 执行上下文内部存储了包括:变量对象作用域链this 指向 这些函数运行时的必须数据。
  • 创建执行上下文分为两个阶段:创建阶段和执行阶段

引用

JavaScript执行上下文

JavaScript 深入之执行上下文

JavaScript深入之变量对象

图解Javascript——变量对象和活动对象

[译] 理解 JavaScript 中的执行上下文和执行栈

What is the execution context in javascript

Understanding Execution Context and Execution Stack in Javascript

理解Javascript之执行上下文(Execution Context)

知乎:JS中的作用域链是在什么时候建立的?

VO、AO、执行环境和作用域链

尾调用优化

JavaScript调用栈、尾递归和手动优化