执行上下文(Execution Context),简称EC。
网上有不少关于执行上下文定义的描述,简单理解一下,其实就是做用域,也就是运行这段JavaScript代码的一个环境。数组
对于每一个执行上下文EC,都有三个重要的属性:浏览器
- 变量对象Variable Object(变量声明、函数声明、函数形参)
- 做用域链 Scope Chain
- this指针
执行上下文分为3类函数
- 全局执行上下文
- 函数执行上下文
- eval执行上下文(几乎不用,暂时不作解释)
代码开始执行前首先进入的环境。
全局执行上下文有且只有一个。客户端中通常由浏览器建立,也就是
window
对象。
(1)使用
var
声明的全局变量,均可以在window
对象中访问到,能够理解为window
是var
声明对象的载体。
this(2)使用
let
声明的全局变量,用window
对象访问不到。线程
函数被调用时,会建立一个函数执行上下文。
函数执行上下文能够有多个,即便调用自身,也会建立一个新的函数执行上下午呢。
以上是对全局执行上下文和函数执行上下文的区别。指针
下面再来看看执行上下文的生命周期。code
执行上下文的生命周期能够分为3个阶段:对象
- 建立阶段
- 执行阶段
- 回收阶段
发生在当函数被调用,可是在 未执行内部代码之前。
建立阶段主要作的事情是:生命周期
(1)建立变量对象Variable Object
(建立函数形参、函数声明、变量声明)
(2)建立做用域链Scope Chain
(3)肯定this指向This Binding
咱们先用代码来更直观的理解下建立阶段的过程:ip
function foo(i){ var a = 100; var b = function(){}; function c(){} } foo(20);
当调用foo(20)
的时候,执行上下文的建立状态以下:
ExecutionContext:{ scopeChain:{ ... }, this:{ ... }, variableObject:{ arguments:{ 0: 20, length: 1 }, i: 20, c:<function>, a:undefined, b:undefined } }
建立完成后,程序自动进入执行阶段,执行阶段主要作的事情是:
(1)给变量对象赋值:给VO中的变量赋值,给函数表达式赋值。
(2)调用函数
(3)顺序执行代码
仍是以上面的代码为例,执行阶段给VO赋值,用伪代码表示以下:
ExecutionContext:{ scopeChain:{ ... }, this:{ ... }, variableObject:{ arguments:{ 0: 20, length: 1 }, i: 20, c:<function>, a:100, b:function } }
全部代码执行完毕,程序关闭,释放内存。
上下文出栈后,虚拟机进行回收。
全局上下文只有当关闭浏览器时才会出栈。
根据以上内容,咱们了解到执行上下文的建立须要建立变量对象,那变量对象究竟是什么呢?
变量对象Variable Object
,简称VO
。简单理解就是一个对象,这个对象存放的是:全局执行上下文的变量和函数。
VO === this === Global
VO
的两种特殊状况:
(1)未通过var
声明的变量,不会存在VO
中
(2)函数表达式(与函数声明相对),也不在VO
中
活动对象Activation Object,也叫激活对象,简称AO。
激活对象是在进入函数执行上下文时(函数执行的前一刻)被建立的。
函数执行上下文中,VO是不能直接访问,因此AO扮演了VO的角色。
VO === AO,而且添加了形参类数组和形参的值
Arguments Object是函数上下文AO的一个对象,它包含的属性有:
(1)callee:指向当前函数的引用
(2)length:真正传递参数的个数
(3)properties-indexes:函数的参数值(按照参数列表从左到右排列)
(1)根据函数参数,建立并初始化arguments
变量声明var、函数形参、函数声明
(2)扫描函数声明
函数声明,是变量对象的一个属性,其属性名和值都是函数对象建立出来的。若变量对象已经包含了相同名字的属性,则替换它的值。
(3)扫描变量声明
变量声明,即变量对象的一个属性,其属性名即变量名,其值为undefined。若是变量名和已经声明的函数名或者函数的参数名相同,则不影响已经存在的属性。
注:函数声明优先级高于变量声明优先级
用代码来理解一下:
function fun(a){ console.log(a); // function a(){} function a(){} } fun(100);
咱们调用了fun(100)
,传入a
的值是100,为何执行console
语句后结果却不是100呢?别急,咱们接着分析~
建立阶段:
步骤 1-1:根据形参建立arguments,用实参赋值给对应的形参,没有实参的赋值为undefined AO_Step1:{ arguments:{ 0: 100, length:1 }, a: 100 } 步骤 1-2:扫描函数声明,此时发现名称为a的函数声明,将其添加到AO上,替换掉已经存在的相同属性名称a,也就是替换掉形参为a的值。 AO_Step2:{ arguments:{ 0: 100, length:1 }, a: 指向function a(){} } 步骤 1-3:扫描变量声明,未发现有变量。
执行阶段:
步骤 2-1:没有赋值语句,第一行执行console命令,而此时a指向的是funciton,因此输出function a(){}
用代码来理解一下
情景1:变量与参数名相同
function fun2(a){ console.log(a); // 100 var a = 10; console.log(a) // 10 } fun2(100); // 分析步骤: 建立阶段: 步骤 1-1:根据arguments建立并初始化AO AO = { arguments:{ 0: 100, length:1 }, a:100 } 步骤 1-2:扫描函数声明,此时没有额外的函数声明,因此AO仍是和上次一致 AO = { arguments:{ 0: 100, length:1 }, a:100 } 步骤 1-3:扫描变量声明,发现AO中已经存在了a属性,因此不修改已存在的属性。 AO = { arguments:{ 0: 100, length:1 }, a:100 } 执行阶段: 步骤 2-1:按顺序执行console语句,此时AO中的a是100,因此输出100. 步骤 2-2:执行到赋值语句,对AO中的a进行赋值,此时a是10。 步骤 2-3:按顺序执行,执行console语句,此时a是10,因此输出10。
情景2:变量与函数名相同
function fun3(){ console.log(a); // function a(){} var a = 10; function a(){} console.log(a) // 10 } fun3(); // 分析步骤: 建立阶段: 步骤 1-1:根据arguments建立并初始化AO AO={ arguments:{ length:0 } } 步骤 1-2:扫描函数声明,此时a指向函数声明(Function Declaration) AO={ arguments:{ length:0 }, a: FD } 步骤 1-3:扫描变量声明,发现AO中已经存在了a属性,则跳过,不影响已存在的属性。 AO={ arguments:{ length:0 }, a: FD } 执行阶段: 步骤 2-1:执行第一行语句console,此时a指向的是函数声明,因此输出函数声明。 AO={ arguments:{ length:0 }, a: FD } 步骤 2-2:执行第二句对AO中的变量对象进行赋值,因此a的值改成10。 AO={ arguments:{ length:0 }, a: 10 } 步骤 2-3:执行第三句,是函数声明,在执行阶段不会再将其添加到AO中,直接跳过。因此AO仍是上次的状态。 AO={ arguments:{ length:0 }, a: 10 } 步骤 2-4:执行第四句,此时a的值是10,因此输出10。 AO={ arguments:{ length:0 }, a: 10 }
根据以上的示例,咱们已经大体明白了EC以及EC的生命周期。
同时,咱们知道函数每次调用都会产生一个新的函数执行上下文。
那么,若是有若干个执行上下文呢,JavaScript是怎样执行的?
这就涉及到 执行上下文栈 的相关知识。
执行上下文栈(Execution context stack,ECS),简称ECS。
简单理解就是若干个执行上下文组成了执行上下文栈。也称为执行栈、调用栈。
用来存储代码执行期间的全部上下文。
咱们知道栈的特色是先进后出。能够理解为瓶子,先进来的东西永远在最底部。
因此
执行上下文栈的特色就是LIFO(Last In First Out)
也就是后进先出。
永远是栈顶处于当前正在执行状态,执行完成后出栈,开始执行下一个。
咱们用代码简单理解一下
示例1:
function f1(){ f2(); console.log(1) } function f2(){ f3(); console.log(2) } function f3(){ console.log(3) } f1(); // 3 2 1
根据执行栈的特色进行分析:
(1)咱们假设执行上下文栈是数组ECStack
,则ECStack=[globalContext]
,存入全局执行上下文(咱们暂且叫它globalStack
)
(2)调用f1()
函数,进入f1
函数开始执行,建立f1
的函数执行上下文,存入执行栈,即ECStack.push('f1 context')
(3)f1
函数内部调用了f2()
函数,则建立f2
的函数执行上下文,存入执行栈,即ECStack.push('f2 context')
,f2
执行完成以前,f1
没法执行console
语句
(4)f2
函数内部调用了f3()
函数,则建立f3
的函数执行上下文,存入执行栈,即ECStack.push('f3 context')
,f3
执行完成以前,f2
没法执行console
语句
(5)f3
执行完成,输出3,并出栈,ECStack.pop()
(6)f2
执行完成,输出2,并出栈ECStack.pop()
(7)f1
执行完成,输出1,并出栈ECStack.pop()
(8)最后ECStack
只剩[globalContext]
全局执行上下文
示例2:
function foo(i){ if(i == 3){ return } foo(i+1); console.log(i) } foo(0); // 2,1,0
分析:
(1)调用foo
函数,建立foo
函数的函数执行上下文,存入EC
,传0
,i=0
,if
条件不知足不执行,
(2)执行到foo(1)
,再次调用foo
函数,建立一个新的函数执行上下文,存入EC
,此时传入的i
为1
,if
条件不知足不执行,
(3)又执行到foo(2)
,又建立新的函数执行上下文,存入EC
,此时i
为2
,if
条件不知足不执行
(3)又执行到foo(3)
,再次建立新的函数执行上下文,存入EC
,此时i
为3
,if
知足直接退出,EC
弹出foo(3)
(4)EC
弹出foo(3)
后执行foo(2)
剩下的代码,输出2
,foo(2)
执行完成,EC
弹出foo(2)
(5)EC
弹出foo(2)
后执行foo(1)
剩下的代码,输出1
,foo(1)
执行完成,EC
弹出foo(1)
(6)EC
弹出foo(1)
后执行foo(0)
剩下的代码,输出0
,foo(0)
执行完成,EC
弹出foo(0)
,此时EC
只剩下全局执行上下文。
- 全局执行上下文只有一个,而且在栈底。
- 当浏览器关闭时,全局执行上下文才会出栈。
- 函数执行上下文能够有多个,而且函数每调用执行一次(即便是调用自身),就会生成一个新的函数执行上下文。
- Js是单线程,因此是同步执行,执行上下文栈中,永远是处于栈顶的是执行状态。
- VO或是AO只有一个,建立过程的顺序是:参数声明>函数声明>变量声明
- 每一个EC能够抽象为一个对象,这个对象包含三个属性:做用域链、VO/AO、this