一、执行上下文
1.什么是执行上下文?
简单来说,执行上下文就是当前javascript代码解析和执行时的环境的抽象概念。javascript任何代码的运行都是在执行上下文中进行的。
2.执行上下文的类型
- 全局执行下文:全局执行上下文是最基础的执行上下文。不存在于任何函数中的代码都属于全局执行上下文。全局执行上下文做了两件事:1.创建一个变量对象,将window对象赋值给该变量对象(变量对象=window)。2.确定this的指向。全局执行上下文只会存在一个。全局上下文只有在应用关闭或者浏览器关闭的时候才会被销毁。
- 函数执行上下文:每个函数都有自己的执行上下文,当函数被调用时才会创建执行上下文。函数每次调用的时候,都会创建一个新的执行上下文。
- Eval执行上下文:使用eval函数调用的代码也会创建自己的执行上下文。
3.执行上下文的生命周期
执行上下文的生命周期分为三个阶段:创建阶段 → 执行阶段 → 回收阶段。
1.创建阶段:
在调用函数,但未执行任何代码前,会创建执行上下文,紧接着执行上下文做了三件事:
- 创建变量对象:会使用函数的形参初始化arguments对象,将arguments对象作为变量对象的初始化属性。提升变量声明和函数声明。在函数中,js会使用活动对象(activation object)来作为变量对象。
- 创建作用域链:创建作用域链在创建变量对象之后,因为作用域链会包含变量对象,可以将作用域链看做是变量对象的一个链表,作用域链的第一个变量对象始终会是当前执行上下文的变量对象,下一个对象来自父级函数的变量对象,全局执行上下文的变量对象(window)始终存在作用域链的底部。作用域链的作用是按顺序查找变量。查找变量时会从作用域链的顶端开始逐一向后查找,直至到全局执行上下文的变量对象。在此当中找到了则会直接返回该变量,如果没有找到则会报错(Reference Error)。
- 确定this的指向。
2.执行阶段:
变量赋值,执行代码。
3.回收阶段:
执行上下文执行完毕后,会从执行上下文栈中弹出,等待被回收。
二、执行上下文栈
全局执行上下文只有一个,但是函数执行上下文却会有很多个,那么JavaScript是怎么来管理的呢?答案是:执行上下文栈(Execution Context Stack)。
我们可以将执行上下文栈看做是一个具体的对象:
EcStack = [];
当启动程序的时候,首先执行的是全局代码,所以会创建全局执行上下文,并将全局执行上下文压入执行上下文栈。
EcStack = [
globalContext
];
当其它js代码运行时,执行栈会怎么变化呢?我们来看下面这个例子:
var a = 1;
function foo() {
console.log(a);
}
function bar() {
foo();
}
bar();
执行上下文栈:
- 调用bar函数,创建了bar函数的执行上下文,添加进执行上下文栈
- 在执行bar函数代码时,又遇到了foo函数,所以又会创建foo函数的执行上下文,添加进执行上下文栈
- 然后foo函树中又遇到log函数,则又会创建log函数的执行上下文
//调用bar函数
EcStack.push(barContext);
//调用foo函数
EcStack.push(fooContext);
//调用log函数
EcStack.push(logContext);
//log函数执行完毕
EcStack.pop(logContext);
//foo函数执行完毕
EcStack.pop(fooContext);
//bar函数执行完毕
EcStack.pop(barContext);
我们再来看一个例子:
var a = 1;
function foo() {
function bar() {
console.log(a);
}
return bar;
}
var func = foo();
func();
那现在是怎么向执行上下文栈中添加执行上下文的呢?
首先会调用foo函数,那么就会创建foo函数的执行上下文并添加到执行上下文栈中。foo函数中声明了bar函数,但是没有直接调用,而是将bar函数返回并赋值给func变量,然后调用了func。这时,才会创建func的执行上下文。
//调用foo
EcStack.push(fooContext);
//foo执行完毕
EcStack.pop(fooContext);
//调用func
EcStack.push(funcContext);
//func执行完毕
ExStack.pop(funcContext);
举这个例子是为了强调一点,执行上下文是在函数被调用时生成的。执行上下文执行完毕后,则会被弹出执行上下文栈,等待被回收。