作者:小土豆
博客园:https://www.cnblogs.com/HouJiao/
掘金:https://juejin.im/user/2436173500265335
前言
在正文开始前,先来看两个JavaScript
代码片段。
代码一
console.log(a);
var a = 10;
代码二
fn1();
fn2();
function fn1(){
console.log('fn1');
}
var fn2 = function(){
console.log('fn2');
}
如果你能正确的回答
并解释
以上代码的输出结果
,那说明你对JavaScript
的执行上下文
已经有一定的了解;反之,阅读完这篇文章,相信你一定会得到答案。
什么是执行上下文
var a = 10;
function fn1(){
console.log(a); // 10
function test(){
console.log('test');
}
}
fn1();
test(); // Uncaught ReferenceError: test is not defined
上面这段代码我们在全局环境
中定义了变量a
和函数fn1
,在调用函数fn1
时,fn1
内部可以成功访问全局环境
中定义的变量a
;接着,我们在全局环境
中调用了fn1
内部定义的test
函数,这行代码会导致ReferenceError
,因为我们在全局环境
中无法访问fn1
内部的test
函数。那这些变量
或者函数
能否正常被访问,就和JavaScript
的执行上下文
有着很大的关系。
JavaScript
的执行上下文
也叫JavaScript
的执行环境
,它是在JavaScript
代码的执行过程中创建出来的,它规定了当前代码能访问到的变量
和函数
,同时也支持着整个JavaScript
代码的运行。
在一段代码的执行过程中,如果是执行全局环境
中的代码,则会创建一个全局执行上下文
,如果遇到函数
,则会创建一个函数执行上下文
。
如上图所示,代码在执行的过程中创建了三个执行上下文
:一个全局执行上下文
,两个函数执行上下文
。因为全局环境
只有一个,因此在代码的执行过程中只会创建一个全局执行上下文
;而函数
可以定义多个,所以根据代码有可能会创建多个函数执行上下文
。
同时JavaScript
还会创建一个执行上下文栈
用来管理代码执行过程中创建的多个执行上下文
。
执行上下文栈
也可以叫做环境栈
,在后续的描述中统一简称为执行栈
。
执行栈
和数据结构
中的栈
是同一种数据类型
,有着先进后出
的特性。
执行上下文的创建
前面我们简单理解了执行上下文
的概念,同时知道了多个执行上下文是通过执行栈
进行管理的。那执行上下文
如何记录当前代码可访问的变量
和函数
将是我们接下来需要讨论的问题。
首先我们需要明确执行上下文
的生命周期
包含两个阶段:创建阶段
和执行阶段
。
创建阶段
对应到我们的代码,也就是代码刚进入全局环境
或者函数
刚被调用;而执行阶段
则对应代码一行一行在被执行。
创建阶段
执行上下文
的创建阶段
会做三件事:
- 创建
变量对象(Variable Object,简称VO)
- 创建
作用域链(Scope Chain)
- 确定
this
指向
this
想必大家都知道,那变量对象
和作用域链
又是什么呢,这里先给大家梳理出这两个的概念。
变量对象
: 变量对象保存着当前环境可以访问的变量
和函数
,保存方式为key:value
,其中key
为变量名或者函数名,value
为变量的值或者函数引用。
作用域链
:作用域链
是由变量对象
组成的一个列表
或者链表
结构,作用域链
的最前端是当前环境的变量对象
,作用域
的下一个元素是上一个环境
的变量对象
,再下一个元素是上上一个环境的变量对象
,一直到全局的环境中的变量对象
;全局环境
的变量对象
始终是作用域链
的最后一个对象。当我们在一段代码中访问某个变量
或者函数
时,会在当前环境的执行上下文的变量对象中查找变量
或者函数
,如果没有找到,则会沿着作用域链
一直向下查找变量
和函数
。
这里的描述的
环境
无非两种,一种是全局的环境,一种是函数所在的环境。
此处参考
《JavaScript高级程序设计》
第三版第4章2节。
相信很多人此刻已经没有信心在往下看了,因为我已经抛出了好多的概念:执行上下文
、执行上下文栈
、变量对象
、作用域链
等等。不过没有关系,我们不用太过于纠结这些所谓的名词,以上的内容大致有个印象即可,继续往下看,疑惑会慢慢解开。
全局执行上下文
我们先以全局环境
为例,分析一下全局执行上下文
的创建阶段
会有怎样的行为。
前面我们说过全局执行上下文
的创建阶段
对应代码刚进入全局环境
,这里为了模拟代码刚进入全局环境
,我在JavaScript
脚本最开始的地方打了断点
。
<script>debugger
var a = 10;
var b = 5;
function fn1(){
console.log('fn1 go')
}
function fn2(){
console.log('fn2 go')
}
fn1();
fn2();
</script>
这种调试方式可能不是很准确,但是可以很好的帮助我们理解抽象的概念。
运行这段代码,代码执行到断点
处会停下来。此时我们在浏览器
的console
工具中访问我们定义的变量
和函数
。
可以看到,我们已经能访问到var
定义的变量
,这个叫变量声明提升
,但是因为代码还未被执行,所以变量的值还是undefined
;同时声明的函数
也可以正常被调用,这个叫为函数声明提升
。
前面我们说变量对象
保存着当前环境可以访问到的变量
和函数
,所以此时变量对象
的内容大致如下:
// 变量对象
VO:{
a: undefined,
b: undefined,
fn1: <Function fn1()>, // 已经是函数本身 可以调用
fn2: <Function fn2()> // 已经是函数本身 可以调用
},
此时的this
也已经指向window
对象。
所以this
内容如下:
//this保存的是window对象的地址,即this指向window
this: <window Reference>
最后就是作用域链
,在浏览器的断点调试工具中,我们可以看到作用域链
的内容。
展开Scope
项,可以看到当前的作用域链
只有一个GLobal
元素,Global
右侧还有一个window
标识,这个表示Global
元素的指向是window
对象。
// 作用域链
scopeChain: [Global<window>], // 当前作用域链只有一个元素
到这里,全局执行上下文
在创建阶段
中的变量对象
、作用域链
和this指向
梳理如下:
// 全局执行上下文
GlobalExecutionContext = {
VO:{
a: undefined,
b: undefined,
fn1: <Function fn1()>, // 已经是函数本身 可以调用
fn2: <Function fn2()> // 已经是函数本身 可以调用
},
scopeChain: [Global<window>], // 全局环境中作用域链只有一个元素,就是Global,并且指向window对象
this: <window Reference> // this保存的是window对象的地址,即this指向window
}
前面我们说作用域链
是由变量对象
组成的,作用域链
的最前端是当前环境的变量对象
。那根据这个概念,我们应该能推理出来:GlobalExecutionContext.VO == Global<window> == window
的结果为true
,因为GlobalExecutionContext.VO
和Global<window>
都是我们伪代码中定义的变量
,在实际的代码中并不存在,而且我们也访问不到真正的变量对象
,所以还是来看看浏览器中的断点调试工具。
我们展开Global
选项。
可以看到Global
中是有我们定义的变量a
、b
和函数fn1
、fn2
。同时还有我们经常会用到的变量document
函数alert
、conform
等,所以我们会说Global是指向window
对象的,这里也就能跟浏览器的显示对上了。
最后就是对应的执行栈
:
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
函数执行上下文
此处参考全局上下文
,在fn1
函数执行前打上断点
。
<script>
var a = 10;
var b = 5;
function fn1(param1, param2){ debugger
var result = param1 + param2;
function inner() {
return 'inner go';
}
inner();
return 'fn1 go'
}
function fn2(){
return 'fn2 go'
}
fn1(a,b);
fn2();
</script>
打开浏览器,代码执行到断点
处暂停,继续在console
工具中访问一些相关的变量
和函数
。
根据实际的调试结果,函数执行上下文
的变量对象
如下:
其实在
函数执行山下文
中,变量对象
不叫变量对象
,而是被称之为活动对象(Active Object,简称AO)
,它们其实也只是叫法上的区别,所以后面的伪代码中,我统一写成VO
。
但是这里有必要给大家做一个说明,以免造成一些误解。
// 变量对象
VO: {
param1: 10,
param2: 5,
result: undefined,
inner: <Function inner()>,
arguments:{
0: 10,
1:5,
length: 2,
callee: <Function fn1()>
}
}
对比全局的执行上下文
,函数执行上下文
的变量对象
除了函数内部定义的变量
和函数
,还有函数的参数
,同时还有一个arguments
对象。
arguments
对象是所有(非箭头)函数
中的局部变量
,它和函数的参数有着一定的对应关系,可以使用从arguments
中获得函数的参数。
函数执行上下文
的作用域链
如下:
用代码表示:
// 作用域链
scopeChain: [
Local<fn1>, // fn1函数执行上下文的变量对象,即Fn1ExecutionContext.VO
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
]
作用域链
最前端的元素是Local
,也就是当前环境
(当前环境
就是fn1
函数)的变量对象
。我们可以展开Local
,其内容基本和前面我们总结的变量对象VO
一致。
这个
Local
展开的内容和前面总结的活动对象AO
基本一致,这里只是Chrome
浏览器的展示方式,不用过多纠结。
this
对象同样指向了window
。
fn1函数内部的this指向window对象,源于
fn1
函数的调用方式。
总结函数执行上下文
在创建阶段
的行为:
// 函数执行上下文
Fn1ExecutionContext = {
VO: {
param1: 10,
param2: 5,
result: undefined,
inner: <Function inner()>,
arguments:{
0: 10,
1:5,
length: 2,
callee: <Function fn1()>
}
},
scopeChain: [
Local<fn1>, // fn1函数执行上下文的变量对象,即Fn1ExecutionContext.VO
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
此时的执行栈
如下:
// 执行栈
ExecutionStack = [
Fn1ExecutionContext, // fn1执行上下文
GlobalExecutionContext // 全局执行上下文
]
执行阶段
执行上下文
的执行阶段
,相对来说比较简单,基本上就是为变量赋值和执行每一行代码。这里以全局执行上下文
为例,梳理执行上下文执行阶段
的行为:
// 函数执行上下文
Fn1ExecutionContext = {
VO: {
param1: 10,
param2: 5,
result: 15,
inner: <Function inner()>,
arguments:{
0: 10,
1:5,
length: 2,
callee: <Function fn1()>
}
},
scopeChain: [
Local<fn1>, // fn1函数执行上下文的变量对象,即Fn1ExecutionContext.VO
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
执行上下文的扩展
坚持看到这里的同学,相信大家对JavaScript
的执行上下文已经有了一点的认识。那前面为了让大家更好的理解JavaScript
的执行上下文,我省略了一些特殊的情况,那接下来缓口气,我们在来看看有关执行上下文
的更多内容。
let和const
对ES6
特性熟悉的同学都知道ES6
新增了两个定义变量的关键字
:let
和const
,并且这两个关键字不存在变量声明提升
。
还是前面的一系列调试方法,我们分析一下全局环境
中的let
和const
。首先我们运行下面这段JavaScript
代码。
<script> debugger
let a = 0;
const b = 1;
</script>
断点处访问变量a
和b
,发现出现了错误。
那这个说明在执行上下文
的执行阶段
,我们是无法访问let
、const
定义的变量,即进一步证实了let
和const
不存在变量声明提升
。也说明了在执行上下文
的创建阶段
,变量对象
中没有let
、const
定义的变量。
函数
函数一般有两种定义方式,第一种是函数声明
,第二种是函数表达式
。
// 函数声明
function fn1(){
// do something
}
// 函数表达式
var fn2 = function(){
// do something
}
接着我们来运行下面的这段代码。
<script> debugger
function fn1(){
return 'fn1 go';
}
var fn2 = function (){
return 'fn2 go';
}
</script>
代码运行到断点处暂停,手动调用函数:fn1
和fn2
。
从结果可以看到,对于函数声明
,因为存在函数声明提升
,所以可以在函数定义前使用函数;而对于函数表达式
,在函数定义前使用会导致错误,说明函数表达式
不存在函数声明提升
。
这个例子补充了前面的内容:在执行上下文
的创建阶段
,变量对象
的内容不包含函数表达式
。
词法环境
在梳理这篇文章的过程中,看到很多文章提及到了词法环境
和变量环境
这个概念,那这个概念是ES5
提出来的,是前面我们所描述的变量对象
和作用域链
的另一种设计和实现。基于ES5
新提出来这个概念,对应的执行上下文
表示也会发生变化。
// 执行上下文
ExecutionContext = {
// 词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: { },
// 外部环境引用
outer: <outer reference>
},
// 变量环境
VariableEnvironment: {
// 环境记录
EnvironmentRecord: { },
// 外部环境引用
outer: <outer reference>
},
// this指向
this: <this reference>
}
词法环境
由环境记录
和外部环境引用
组成,其中环境记录
和变量对象
类似,保存着当前执行上下文
中的变量
和函数
;同时环境记录
在全局执行上下文中称为对象环境记录
,在函数执行上下文中称为声明性环境记录
。
// 全局执行上下文
GlobalExecutionContext = {
// 词法环境
LexicalEnvironment: {
// 环境记录之对象环境记录
EnvironmentRecord: {
Type: "Object" // type标识,表明该环境记录是对象环境记录
},
// 外部环境引用
outer: <outer reference>
}
}
// 函数执行上下文
FunctionExecutionContext = {
// 词法环境
LexicalEnvironment: {
// 环境记录之声明性环境记录
EnvironmentRecord: {
Type: 'Declarative' // type标识,表明该环境记录是声明性环境记录
},
// 外部环境引用
outer: <outer reference>
}
}
这点就类似变量对象
也只存在于全局上下文中
,而在函数上下文中
称为活动对象
。
词法环境
中的外部环境
保存着其他执行上下文的词法环境
,这个就类似于作用域链
。
除了词法环境
之外,还有一个名词
叫变量环境
,它实际也是词法环境
,这两者的区别是变量环境
只保存用var
声明的变量,除此之外像let
、const
定义的变量
、函数声明
、函数中的arguments
对象等,均保存在词法环境中
。
以这段代码为例:
var a = 10;
var b = 5;
let m = 10;
function fn1(param1, param2){
var result = param1 + param2;
function inner() {
return 'inner go';
}
inner();
return 'fn1 go'
}
fn1(a,b);
如果以ES5
中新提及的词法环境
和变量环境
概念来表示执行上下文
,应该是下面这样:
// 执行栈
ExecutionStack = [
fn1ExecutionContext, // fn1执行上下文
GlobalExecutionContext, // 全局执行上下文
]
// fn1执行上下文
fn1ExecutionContext = {
// 词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: 'Declarative', // 函数的环境记录称之为声明性环境记录
arguments: {
0: 10,
1: 5,
length: 2
},
inner: <Function inner>
},
// 外部环境引用
outer: <GlobalLexicalEnvironment>
},
// 变量环境
VariableEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: 'Declarative', // 函数的环境记录称之为声明性环境记录
result: undefined, // 变量环境只保存var声明的变量
},
// 外部环境引用
outer: <GlobalLexicalEnvironment>
}
}
// 全局执行上下文
GlobalExecutionContext = {
// 词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: 'Object', // 全局执行上下文的环境记录称为对象环境记录
m: < uninitialized >,
fn1: <Function fn1>,
fn2: <Function fn2>
},
// 外部环境引用
outer: <null> // 全局执行上下文的外部环境引用为null
},
// 变量环境
VariableEnvironment: {
// 环境记录
EnvironmentRecord: {
Type: 'Object', // 全局执行上下文的环境记录称为对象环境记录
a: undefined, // 变量环境只保存var声明的变量
b: undefined, // 变量环境只保存var声明的变量
},
// 外部环境引用
outer: <null> // 全局执行上下文的外部引用为null
}
}
以上的内容基本上参考这篇文章:【译】理解 Javascript 执行上下文和执行栈。关于词法环境
相关的内容没有过多研究,所以本篇文章就不在多讲,后面的一些内容还是会以变量对象
和作用域链
为准。
调试方法说明
关于本篇文章中的调试方法,仅仅是我自己实践的一种方式,比如在断点
处代码暂停运行,然后我在console
工具中访问变量
或者调用函数
,其实大可以将这些写入代码中。
console.log(a);
fn1();
fn2();
var a = 10;
function fn1(){
return 'fn1 go';
}
var fn2 = function (){
return 'fn2 go';
}
在代码未执行到变量声明
和函数声明
处,都可以暂且认为处于执行上下文
的创建阶段
,当变量访问出错或者函数调用出错,也可以得出同样的结论,而且这种方式也非常的准确。
反而是我这种调试方法的实践过程中,会出现很多和实际不符的现象,比如下面这个例子。
前面我们其实给出过正确结论:函数声明
,可以在函数定义前使用函数,而函数表达式不可以。而如果是我这种调试方式,会发现此时调用inner
和other
都会出错。
其原因我个人猜测应该是浏览器console
工具的上层实现的原因,如果你也遇到同样的问题,不必过分纠结,一定要将实际的代码运行结果和书中的理论概念结合起来,正确的理解JavaScript
的执行上下文
。
躬行实践
台下十年功,终于到了台上的一分钟了。了解了JavaScript
的执行上下文
之后,对于网上流传的一些高频面试题和代码,都可以用执行上下文
中的相关知识来分析。
首先是本文开篇贴出的两段代码。
代码一
console.log(a);
var a = 10;
这段代码的运行结果相信大家已经了然于胸:console.log
的结果是undefined
。其原理也很简单,就是变量声明提升
。
代码二
fn1();
fn2();
function fn1(){
console.log('fn1');
}
var fn2 = function(){
console.log('fn2');
}
这个示例应该也是小菜一碟,前面我们已经做过代码调试:fn1
可以正常调用,调用fn2
会导致ReferenceError
。
代码三
var numberArr = [];
for(var i = 0; i<5; i++){
numberArr[i] = function(){
return i;
}
}
numberArr[0]();
numberArr[1]();
numberArr[2]();
numberArr[3]();
numberArr[4]();
此段代码如果刷过面试题的同学一定知道答案,那这次我们用执行上下文
的知识点对其进行分析。
step 1
代码进入全局环境
,开始全局执行上下文
的创建阶段
:
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
// 全局执行上下文
GlobalExecutionContext = {
VO: {
numberArr: undefined,
i: undefined,
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
step 2
接着代码一行一行被执行,开始全局执行上下文
的执行阶段
。
当代码开始进入第一个循环:
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
// 全局执行上下文
GlobalExecutionContext = {
VO: {
// 这种写法代表number是一个Array类型,长度为1,第一个元素是一个Function
numberArr: Array[1][f()],
i: 0,
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
上面总结的
执行上下文
内容是代码已经进入到第一个循环,跳过了numberArr
的声明
和赋值
,后面所有的代码只分析关键部分
,不会一行一行的分析。
step 3
代码进入第五次循环(第五次循环因为不满足条件并不会真正执行,但是i
值已经加1
):
省略
i=2
、i = 3
和i = 4
的执行上下文内容。
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
// 全局执行上下文
GlobalExecutionContext = {
VO: {
// 这种写法代表number是一个Array类型,长度为5,元素均为Function
numberArr: Array[5][f(), f(), f(), f(), f()],
i: 5,
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
循环
部分结束以后,我们发现i
此时的值已经是5
了。
step 4
接着我们访问numberArr
中的元素
(numberArr
中的每一个元素都是一个匿名函数
,函数返回i
的值)并调用。首先是访问下标为0
的元素,之后调用对应的匿名函数
,既然是函数调用
,说明还会生成一个函数执行上下文
。
// 执行栈
ExecutionStack = [
FunctionExecutionContext // 匿名函数执行上下文
GlobalExecutionContext // 全局执行上下文
]
// 匿名函数执行上下文
FunctionExecutionContext = {
VO: {}, // 变量对象为空
scopeChain: [
LocaL<anonymous>, // 匿名函数执行上下文的变量对象,即FunctionExecutionContext.VO
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <numberArr reference> // this指向numberArr this == numberArr 值为true
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
// 这种写法代表number是一个Array类型,长度为5,元素均为Function
numberArr: Array[5][f(), f(), f(), f(), f()],
i: 5,
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
调用匿名函数
时,函数执行上下文
的变量对象
的值为空,所以当该匿名函数
返回i
时,在自己的变量对象
中没有找到对应的i
值,就会沿着自己的作用域链(scopeChain)
去全局执行上下文的变量对象Global<window>
中查找,于是返回了5
。
那后面访问numberArr
变量的第1个
、第2个
、...
、第4个
元素也是同样的道理,均会返回5
。
代码四
var numberArr = [];
for(let i = 0; i<5; i++){
numberArr[i] = function(){
return i;
}
}
console.log(numberArr[0]());
console.log(numberArr[1]());
console.log(numberArr[2]());
console.log(numberArr[3]());
console.log(numberArr[4]());
这段代码和上面一段代码基本一致,只是我们将循环中控制次数的变量i
使用了let
关键字声明,那接下来开始我们的分析。
step 1
首先是全局执行上下文
的创建阶段
:
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
// 全局执行上下文
GlobalExecutionContext = {
VO: {
numberArr: undefined
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
因为let
关键字不存在变量提升
,因此全局执行上下文
的变量对象
中并没有变量i
。
step 2
当代码一行一行的执行,开始全局执行上下文
的执行阶段
。
以下是代码执行进入第一次循环:
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
// 全局执行上下文
GlobalExecutionContext = {
VO: {
// 这种写法代表number是一个Array类型,长度为1,第一个元素是一个Function
numberArr: Array[1][f()],
},
scopeChain: [
Block, // let定义的for循环形成了一个块级作用域
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
可以看到当循环开始执行时,因为遇到了let
关键字,因此会创建一个块级作用域
,里面包含了变量i
的值。这个块级作用域
非常的关键,正是因为这个块级作用域
在循环的时候保存了变量的值,才使得这段代码的运行结果不同于上一段代码。
step 3
i
值为5
时:
省略
i=1
、i = 3
和i = 4
的执行上下文内容。
GlobalExecutionContext = {
VO: {
// 这种写法代表number是一个Array类型,长度为2,元素均为Function
numberArr: Array[5][f(), f(), f(), f(), f()],
},
scopeChain: [
Block,
Global<window>
],
this: <window reference>
}
此时块级作用域
中变量i
的值也同步更新为5
。
step 4
接着就是访问数组中的第一个元素,调用匿名函数
,匿名函数
在执行的时候会创建一个函数执行上下文
。
// 执行栈
ExecutionStack = [
FunctionExecutionContext, // 匿名函数执行上下文
GlobalExecutionContext // 全局执行上下文
]
// 匿名函数执行上下文
FunctionExecutionContext = {
VO: {}, // 变量对象为空
scopeChain: [
LocaL<anonymous>, // 匿名函数执行上下文的变量对象,即FunctionExecutionContext.VO
Block, // 块级作用域
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <numberArr reference> // this指向numberArr this == numberArr 值为true
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
// 这种写法代表number是一个Array类型,长度为2,元素均为Function
numberArr: Array[5][f(), f(), f(), f(), f()],
},
scopeChain: [
Global<window>
],
this: <window reference>
}
该匿名函数
因为保存着let
关键字定义的变量i
,因此作用域链
中会保存着第一次循环
时创建的那个块级作用域
,这个块级作用域
前面我们说过也在浏览器的调试工具中看到过,它保存着当前循环的i
值。
所以当return i
时,当前执行上下文的变量对象为空,就沿着作用域向下查找,在Block
中找到对应的变量i
,因此返回0
;后面访问numberArr[1]()
、numberArr[2]()
、...、numberArr[4]()
也是同样的道理。
代码五
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
这段代码包括下面的都是在梳理这篇文章的过程中,看到的一个很有意思的示例,所以贴在这里和大家一起分析一下。
step 1
代码进入全局环境
,开始全局执行上下文
的创建阶段
:
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: undefined,
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
step 2
全局执行上下文
的执行阶段
:
// 执行栈
ExecutionStack = [
GlobalExecutionContext // 全局执行上下文
]
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope', // 变量赋值
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
step 3
当代码执行到最后一行:checkscope()
,开始checkscope函数执行上下文
的创建阶段
。
// 执行栈
ExecutionStack = [
CheckScopeExecutionContext, // checkscope函数执行上下文
GlobalExecutionContext // 全局执行上下文
]
// 函数执行上下文
CheckScopeExecutionContext = {
VO: {
scope: undefined,
f: <Function f>, // 函数已经可以被调用
},
scope: [
Local<checkscope>, // checkscope执行上下文的变量对象 也就是CheckScopeExecutionContext.VO
Global<window> //全局执行上下文的变量对象 也就是GlobalExecutionContext.VO
],
this: <window reference>
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope',
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
step 4
接着是checkscope函数执行上下文
的执行阶段
:
// 执行栈
ExecutionStack = [
CheckScopeExecutionContext, // 函数执行上下文
GlobalExecutionContext // 全局执行上下文
]
// 函数执行上下文
CheckScopeExecutionContext = {
VO: {
scope: 'local scope', // 变量赋值
f: <Function f>, // 函数已经可以被调用
},
scope: [
Local<checkscope>, // checkscope执行上下文的变量对象 也就是CheckScopeExecutionContext.VO
Global<window> //全局执行上下文的变量对象 也就是GlobalExecutionContext.VO
],
this: <window reference>
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope',
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
step 5
执行到return f()
时,进入f函数执行上下文
的创建阶段
:
// 函数执行上下文的创建阶段
FExecutionContext = {
VO: {},
scope: [
Local<f>, // f执行上下文的变量对象 也就是FExecutionContext.VO
Local<checkscope>, // checkscope执行上下文的变量对象 也就是CheckScopeExecutionContext.VO
Global<window> //全局执行上下文的变量对象 也就是GlobalExecutionContext.VO
],
this: <window reference>
}
// 函数执行上下文
CheckScopeExecutionContext = {
VO: {
scope: 'local scope',
f: <Function f>, // 函数已经可以被调用
},
scope: [
Local<checkscope>, // checkscope执行上下文的变量对象 也就是CheckScopeExecutionContext.VO
Global<window> //全局执行上下文的变量对象 也就是GlobalExecutionContext.VO
],
this: <window reference>
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope',
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
当f函数
返回scope
变量时,当前f执行上下文中
的变量对象
中没有名为scope
的变量,所以沿着作用域链
向上查找,发现checkscope
执行上下文的变量对象Local<checkscope>
中包含scope
变量,所以返回local scope
。
代码六
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
这段代码和上面的代码非常的相似,只不过checkscope
函数的返回值没有直接调用f
函数,而是将f
函数返回,在全局环境
中调用了f
函数。
step 1
全局执行上下文
的创建阶段
:
// 执行栈
ExcutionStack = [
GlobalExcutionContext
];
// 全局执行上下文的创建阶段
GlobalExecutionContext = {
VO: {
scope: undefined,
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
step 2
全局执行上下文
的执行阶段
:
// 执行栈
ExcutionStack = [
GlobalExcutionContext // 全局执行上下文
];
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope', // 变量赋值
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象,即GlobalExecutionContext.VO
],
this: <window reference>
}
step 3
当代码执行到最后一行:checkscope()()
,先执行checkscope()
,也就是开始checkscope函数执行上下文
的创建阶段
。
// 执行栈
ExcutionStack = [
CheckScopeExecutionContext, // checkscope函数执行上下文
GlobalExcutionContext // 全局执行上下文
]
// checkscope函数执行上下文的创建阶段
CheckScopeExecutionContext = {
VO: {
scope: undefined,
f: <Function f>, // 函数已经可以被调用
},
scopeChain: [
Local<checkscope>, // checkscope执行上下文的变量对象 也就是CheckScopeExecutionContext.VO
Global<window> //全局执行上下文的变量对象 也就是GlobalExecutionContext.VO
],
this: <window reference>
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope',
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [Global<window>],
this: <window reference>
}
step 4
接着是checkscope函数执行上下文
的执行阶段
:
// 执行栈
ExcutionStack = [
CheckScopeExecutionContext, // checkscope函数执行上下文
GlobalExcutionContext // 全局执行上下文
]
// checkscope函数执行上下文
CheckScopeExecutionContext = {
VO: {
scope: 'local scope',
f: <Function f>, // 函数已经可以被调用
},
scopeChain: [
Local<checkscope>, // checkscope执行上下文的变量对象 也就是CheckScopeExecutionContext.VO
Global<window> //全局执行上下文的变量对象 也就是GlobalExecutionContext.VO
],
this: <window reference>
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope',
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象
],
this: <window reference>
}
step 5
执行到return f
时,此处并不同上一段代码,并没有调用f
函数,所以不会创建f
函数的执行上下文,因此直接将函数f
返回,此时checkscope
函数执行完毕,会从执行栈
中弹出checkscope
的执行山下文
。
// 执行栈 (此时CheckScopeExecutionContext已经从栈顶被弹出)
ExcutionStack = [
GlobalExecutionContext // 全局执行上下文
];
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope',
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [
Global<window> // 全局执行上下文的变量对象
],
this: <window reference>
}
step 6
在step3
中,checkscope()()
代码的前半部分执行完毕,返回f函数
;接着执行后半部分()
,也就是调用f函数
。那此时进入f函数执行上下文
的创建阶段
:
// 执行栈
ExcutionStack = [
fExecutionContext, // f函数执行上下文
GlobalExecutionContext // 全局执行上下文
];
// f函数执行上下文
fExecutionContext = {
VO: {}, // f函数的变量对象为空
scopeChain: [
Local<f>, // f函数执行上下文的变量对象
Local<checkscope>, // checkscope函数执行上下文的变量对象
Global<window>, // 全局执行上下文的变量对象
],
this: <window reference>
}
// 全局执行上下文
GlobalExecutionContext = {
VO: {
scope: 'global scope',
checkscope: <Function checkscope>, // 函数已经可以被调用
},
scopeChain: [Global<window>],
this: <window reference>
}
我们看到在f
函数执行上下文的创建阶段
,其变量对象
为空字典,而其作用域链
中却保存这checkscope执行上下文
的变量对象
,所以当代码执行到return scope
时,在f
函数的变量对象
中没找到scope
变量,便沿着作用域链,在chckscope
执行上下文的变量对象Local<checkscope>
中找到了scope
变量,所以返回local scope
。
总结
相信很多人和我一样,在刚开始学习和理解执行山下文
的时候,会因为概念过于抽象在加上没有合适的实践方式,对JavaScript
的执行上下文百思不解。作者也是花了很久的时间,阅读很多相关的书籍和文章,在加上一些实践才梳理出来这篇文章,希望能给大家一些帮助,如果文中描述有误,还希望不吝赐教,提出宝贵的意见和建议。
文末
如果这篇文章有帮助到你,️关注+点赞+收藏+评论+转发️鼓励一下作者
文章公众号
首发,关注不知名宝藏女孩
第一时间获取最新的文章
笔芯️~