这个笔记系列我将会对 JavaScript 中,执行上下文与执行栈做一个深入的探究,若有不对的地方希望各位指正。
开始前,进行下简单介绍:这一系列的笔记,都是在浏览器环境中运行代码;声明变量都是通过 var 关键字,并未讨论 let、const 关键字的声明情况。
文章目录
1、执行上下文
① 概述
执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
② 分类
JavaScript 中有三种执行上下文类型:
-
全局执行上下文 —— 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
-
函数执行上下文 —— 每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
-
Eval 函数执行上下文 —— 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。
我主要会讲全局执行上下文和函数执行上下文
③ 执行上下文中存储的内容
在不同的执行上下文中所存储的内容不尽相同,但是每一个执行上下文,都会存在一个 变量对象(Variable Object 简称-VO), 这个变量对象将会存储当前执行函数的变量声明等内容。具体的存储内容,我会在后面介绍全局执行上下文和函数执行上下文的时候进行详细的介绍。
2、执行栈
① 概述
-
执行栈(Execution Context Stack 简称-ECS), 也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
-
当 JavaScript引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
-
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
3、全局执行上下文
① 概述
所有的 JavaScript 代码在执行之前就会创建一个执行栈,而每一个执行栈的最开始的时候,都会存放一个 全局执行上下文(Global Execution Context 简称-GEC)。 因为执行栈是一个栈结构,那么则表示 GEC(全局执行上下文) 会存在于栈底,当全部的 JavaScript 代码执行完毕之后,GEC(全局执行上下文) 才会出栈。
② GEC(全局执行上下文) 中存储的内容
GEC(全局执行上下文) 中存在两块内容:
-
全局对象(Global Object 简称-GO): 这里面存储的是全局中声明的所有变量声明和函数声明。但是在 GO(全局对象) 中存储的变量声明与函数声明的存储形式,又是截然不同:
- 变量声明: 在 GO(全局对象)中存储的变量声明,在声明的时候,全部声明为 undefined,在真正执行的时候,才会给每一个变量进行赋值操作。
- 函数声明: 在 GO(全局对象)中存储的函数声明,在声明的时候,就已经为函数创造了其内存空间, 在函数声明中引入的则是该内存空间的地址,该函数的内部代码将会存储在一个独立的内存空间当中。
-
执行代码: 就是在全局中将要执行的所有代码
② window 下的全局执行上下文
在浏览器中执行 JavaScript 代码(或者说是 V8 引擎下执行 JavaScript 代码) 的时候,GEC(全局执行上下文) 中的 VO(变量对象),会被赋值为 GO(全局对象),这个时候 VO(变量对象)和 GO(全局对象)是同一个东西,在浏览器下就是 window 对象。所以在浏览器执行的 JavaScript 代码的时候,在全局中打印 this 的话,打印出来的是 window 对象。
执行下面的代码,我将该代码在浏览器中运行时的GEC(全局执行上下文)画个草图展示一下
var foo = 'hello world';
function bar() {
console.log(name);
}
bar();
因为在浏览器中的 window 对象中,window 对象本身就绑定了很多属性,比如定时器、一些浏览器事件函数等等。但是最神奇的一点是在 window 对象中,又绑定了 window,如此循环。如果打印一下console.log(window.window.window....)
,并不会报错,而是继续打印出 window 对象。这是因为在 window 这个对象中,又绑定了这个 GO(全局对象),因为浏览器中的全局对象是 window,所以如此往复,window 中嵌套 window。
4、函数执行上下文
① 概述
在整个 ECS(执行栈)中,除了最底层的 GEC(全局执行上下文),剩下的其他执行上下文就是 函数执行上下文(Function Execution Context 简称-FEC)。
其实还有 Eval 函数执行上下文, 但是该系列文章我不会进行讲解 Eval 函数执行上下文
JavaScript 代码中的每一个函数都会创建一个 FEC(函数执行上下文),只有在执行到该函数的时候,才会将其 FEC(函数执行上下文)存入执行栈中,当执行完后又会将该 FEC(函数执行上下文)进行销毁,即出栈。
② FEC(函数执行上下文)中存储的内容
在 FEC(函数执行上下文)中存储的内容又与 GEC(全局执行上下文)中存储的内容不尽相同:
-
活动对象(Activation Object 简称-AO): 这里面存储的是全局中声明的所有变量声明和函数声明。但是在 AO(活动对象) 中存储的变量声明与函数声明的存储形式,又是截然不同:
- 变量声明: 在 AO(活动对象)中存储的变量声明,在声明的时候,全部声明为 undefined,在真正执行的时候,才会给每一个变量进行赋值操作。
- 函数声明: 在 AO(活动对象)中存储的函数声明,在声明的时候,就已经为函数创造了其内存空间, 在函数声明中引入的则是该内存空间的地址,该函数的内部代码将会存储在一个独立的内存空间当中。
-
执行代码: 就是在全局中将要执行的所有代码
在上面介绍 GEC(全局执行上下文)的时候,已经说过当 GEC(全局执行上下文)在浏览器下运行的时候,其 VO(变量对象)会被赋值为 GO(全局对象); 那么在 FEC(函数执行上下文)中的 VO(变量对象),会被赋值为 AO(活动对象)。
执行下面的代码,我将该代码在浏览器中运行时整个 ECS(执行栈)画个草图展示一下
var foo = 'hello world';
function bar() {
var age = 10;
console.log(name);
}
bar();
5、经典案例
接下来我会将下面这个经典的代码执行顺序进行一个讲解:
比较下面两段代码,试述两段代码的不同之处
// 第一个
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()();
上面这两个代码的执行结果都为 local scope
,这个是很简单的,但是这两个代码在执行栈的执行过程则是截然不同的,接下来我对两端代码进行逐一分析:
① 代码一的解析
-
第一步:首先会创建一个 GEC(全局执行上下文),压入 ECS(执行栈)的栈底;将全局中声明的所以变量和函数都绑定在 GO(全局对象)中。
-
第二步:开始执行 GEC(全局执行上下文),给
变量 scope 赋值 global scope
;当执行到checkscope()
调用 checkscope 函数的时候,会给 checkscope 函数创建一个 FEC(函数执行上下文),压入 ECS(执行栈)中
-
第三步:执行 checkscope 函数 FEC(函数执行上下文)中的代码,给
变量 scope 赋值 local scope
;当执行到return f()
的时候,因为又调用了函数 f,所以又要创建一个函数 f 的执行上下文,压入到 ECS(执行栈)中。
-
第四步:当函数 f 全部执行完毕之后,那么他的执行上下文将会出栈
-
第五步:当函数 checkscope 全部执行完毕之后,他的执行上下文将会出栈
-
第六步:当 ECS(执行栈)中只剩下 GEC(全局执行上下文),没有任何其他的代码需要去执行的时候, GEC(全局执行上下文)也会出栈,将 ECS(执行栈)清空。当再次执行代码的时候,继续重复上述的步骤。
② 代码二的解析
我上面讲解了代码一的执行顺序,代码二可以先自己考虑一下,试着画一个执行顺序图。
-
第一步:首先会创建一个 GEC(全局执行上下文),压入 ECS(执行栈)的栈底;将全局中声明的所以变量和函数都绑定在 GO(全局对象)中。这里需要注意,在代码执行中我写的是执行
checkscope()
,并不是执行checkscope()()
,因为执行函数是只有一个小括号才表示执行;第二个小括号表示的是,执行checkscope()
后返回的函数再去执行。
-
第二步:开始执行 GEC(全局执行上下文),给
变量 scope 赋值 global scope
;当执行到checkscope()
调用 checkscope 函数的时候,会给 checkscope 函数创建一个 FEC(函数执行上下文),压入 ECS(执行栈)中。注意我红色圈起来的部分,代码中只是将 f 这个函数返回了,并没有去执行,所以下一步并不会给 f 函数创建一个执行上下文。
-
第三步:经过第二步之后,checkscope 所有的代码都已经执行完毕,注意 return 返回的是 f 函数,并不是
f()
,只有函数调用的时候才会创建 FEC(函数执行上下文)。所以这里将 checkscope 函数执行完毕之后,就会将 checkscope 的执行上下文销毁。
-
第四步:这时候,checkscope 函数最后返回的函数 f,会被调用 —— 因为
checkscope()() => f()
。这里才对 f 函数进行了调用,所以又会创建一个函数 f 的执行上下文。
-
第五步:函数 f 执行完毕之后,就会将 f 的执行上下文进行销毁。
-
第六步:当 ECS(执行栈)中只剩下 GEC(全局执行上下文),没有任何其他的代码需要去执行的时候, GEC(全局执行上下文)也会出栈,将 ECS(执行栈)清空。当再次执行代码的时候,继续重复上述的步骤。
上述这个经典的例题,结果当然是很简单得到答案的,最主要的是 JavaScript 代码的执行顺序要搞明白。