什么是执行上下文
在JavaScript中有三种代码运行环境:
- Global Code
JavaScript代码开始运行的默认环境
- Function Code
代码进入一个JavaScript函数
- Eval Code
使用eval()执行代码
为了表示不同的运行环境,JavaScript中有一个执行上下文(Execution context,EC)的概念。也就是说,当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)。
对于执行上下文:
可以理解为某一个函数或某一段代码运行的环境,这个环境中包含了代码需要的所有信息,比如是否能访问作用域中的某一个变量,或者使我这里面的this指向是谁等等...简单来说执行上下文可以是一个对象,对象里包含了执行代码所有需要的信息
因为要执行的代码很多,所以js提供了一个存储执行上下文的数据结构:执行上下文栈
当遇到一个全局代码时,会将一个全局上下文压入栈,同理,遇到函数上下文会将对应的函数上下文压入栈,直至其中所有代码都执行完毕后,才会将上下文弹出栈,举个例子:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
首先,fun1,fun2,fun3都是函数声明,并没有执行其中逻辑,所以不会压入上下文栈,直到fun1被执行,将fun1的上下文压入栈中,然后逻辑中调用fun2,将fun2的执行上下文压入栈中,依此类推,知道fun3中的代码执行完毕,将fun3的执行上下文弹出栈,以此类推,直到所有代码执行完毕,全局上下文弹出栈为止:
//每一个函数执行时都会创建一个执行上下文并被压入执行上下文栈中
//fun1执行了,创建一个context
// 压栈
ECStack.push(<fun1 context>) //发现内部还有`fun2`调用
ECStack.push(<fun2 context>) //发现内部还有`fun3`调用
ECStack.push(<fun3 context>) //发现内部还有log函数调用
ECStack.push(<log context>) //里面没了
打印fun3 //代码执行完了,该弹栈了
ECStack.pop(<log context>)
ECStack.pop(<fun3 context>)
ECStack.pop(<fun2 context>)
ECStack.pop(<fun1 context>)
此时ECStack还剩下[globalContext]
// 继续处理其他代码
// globalContext在程序结束前一直会存在
详解执行上下文
执行上下文
当JS
执行到一段可执行代码时(全局代码、函数代码、eval)就会创建执行上下文,执行上下文内有三个重要属性:
- 变量对象
- this
- 作用域链 变量对象是与执行上下文相关的数据作用域,它的作用是保存上下文中定义的变量声明与函数声明。
不同执行上下文中的变量对象是不同的,下面介绍一下全局变量对象和函数变量对象。
全局对象
MDN的解释:
一个全局对象是一个永远存在于 global scope 的 object。window 对象是浏览器中的全局对象。
任何全局变量或者全局函数都可以通过 window 的属性来访问。
在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。
举例
console.log(this) //window
var a=1 //挂到window上的属性
window.a //1
在顶层作用域(全局上下文)上的变量对象就是全局对象
活动对象
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object ,而只有被激活的变量对象,他上面的各种属性才能被访问。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
执行过程
执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:
1、进入执行上下文
2、代码执行
进入执行上下文
当进入执行上下文阶段,这时候还没有执行代码
变量对象包括
1、函数的所有形参(如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 由于没有实参,所以属性值为undefined
2、函数声明
- 由名称和对应值组成的一个变量对象的属性被创建
- 如果变量对象存在相同名称的属性,则覆盖其属性
3、变量声明
- 由名称和对应值(undefined)组成的一个变量对象的属性被创建
- 如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1)
在调用函数foo并进入函数执行上下文后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
代码执行
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
还是上面的例子,当代码执行完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:
-
全局上下文的变量对象初始化是全局对象
-
函数上下文的变量对象初始化只包括 Arguments 对象
-
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
-
在代码执行阶段,会再次修改变量对象的属性值
关于函数提升和变量提升的先后关系
下面看一段代码
console.log(foo);//???
function foo(){
console.log("foo");
}
var foo = 1;
console.log(foo);//???
可以思考一下打印出来的是什么
第一个log会打印出函数体,而非undefined,第二个log会打印出1
原因是会优先处理函数声明,再处理变量声明。如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性