一、什么是执行上下文?
执行上下文(Execution Context): 函数执行前进行的准备工作(也称执行上下文环境)
JavaScript 在执行一个“代码段”之前,即解析 (预处理) 阶段,会先进行一些“准备工作”,例如扫描 JS 中 var 定义的变量、函数名等,进而生成执行上下文。
name
–
变量对象(VO, variable object)
当前函数定义的变量、函数、参数
作用域链(Scope chain)
源代码定义时形成的作用域链
this
JS 中的“代码段”分为三种:全局代码段、函数体代码段、eval 代码段。(注:ES6 之前,JS 不存在“代码块”作用域的概念,即除了函数之外所有“{}”里的代码,都属于全局作用域)
全局代码段“准备工作”包括:
1. 变量、函数表达式 —— 变量声明,默认赋值为 undefined;
2.this —— 赋值;
3. 函数声明 —— 赋值。
函数体代码段“准备工作”包括:
1. 变量、函数表达式 —— 变量声明,默认赋值为 undefined;
2.this —— 赋值;
3. 函数声明 —— 赋值;
4. 参数 —— 赋值;
5.argument —— 赋值;
6. 自由变量的取值作用域 —— 赋值。
evel()不推荐使用,所以不再分析 evel 代码段。
至此,“执行上下文”的定义可以通俗化为 —— 在执行代码段之前(预处理阶段),把将要用到的所有变量都事先拿出来,有的直接赋值,有的先用 undefined 占个空,这些变量共同组成的词法环境,即为执行上下文环境。
二、执行上下文栈
javaScript 是单线程语言,简单理解下单线程,就是同个时间段只能做一件任务,完成之后才可以继续下一个任务。函数编程中,代码中会声明多个函数,对应的执行上下文也会存在多个。在 JavaScript 中,通过栈的存取方式来管理执行上下文,我们可称其为执行栈,或函数调用栈(Call Stack)。
1. 栈数据结构
要简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图左侧。栈遵循 ” 先进后出,后进先出 ” 的规则,或称 LIFO (“Last In First Out”) 规则。
如图所示,我们只能从栈顶取出或放入乒乓球,最先放进盒子的总是最后才能取出。栈中 ” 放入 / 取出 ”,也可称为 ” 入栈 / 出栈 ”。
总结栈数据结构的特点:
后进先出,先进后出
出口在顶部,且仅有一个
2. 执行上下文栈 ECS(Execution Context Stack)(函数调用栈)
程序执行进入一个执行环境时,它的执行上下文就会被创建,并被推入执行栈中(入栈);程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。
因为 JS 执行中最先进入全局环境,所以处于 ” 栈底的永远是全局环境的执行上下文 ”。而处于 ” 栈顶的是当前正在执行函数的执行上下文 ”,当函数调用完成后,它就会从栈顶被推出(理想的情况下,闭包会阻止该操作,闭包后续文章深入详解)。
“ 全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底 ”
function foo () {
function bar () {
return ‘I am bar’;
}
return bar();
}
foo();
3. 执行上下文的生命周期
执行上下文的生命周期有两个阶段:
创建阶段(进入执行上下文)
执行阶段(代码执行)
创建阶段:函数被调用时,进入函数环境,为其创建一个执行上下文,此时进入创建阶段执行阶段:执行函数中代码时,此时执行上下文进入执行阶段
创建阶段的操作
创建变量对象
函数环境会初始化创建 Arguments 对象(并赋值)
函数声明(并赋值)
变量声明,函数表达式声明(未赋值)
确定 this 指向(this 由调用者确定)
确定作用域(词法环境决定,哪里声明定义,就在哪里确定)
执行阶段的操作
变量对象赋值
变量赋值
函数表达式赋值
2. 调用函数 3. 顺序执行其它代码
变量对象和活动对象的区别:
当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。
“ 创建阶段对函数声明做赋值,变量及函数表达式仅做声明,真正的赋值操作要等到执行上下文代码执行阶段 ”。
代码例子 1:变量提升
function foo() {
console.log(a); // 输出 undefined
var a = ‘I am here’; // 赋值
}
foo();
// 实际执行过程
function foo() {
var a; // 变量声明,var 初始化 undefined
console.log(a);
a = ‘I am here’; // 变量重新赋值
}
代码例子 2:函数声明优先级
function foo() {
console.log(bar);
var bar = 20;
function bar() {
return 10;
}
var bar = function() {
return 30;
}
}
foo(); // 输出 bar()整个函数声明
函数声明,变量声明,函数表达式的优先级
函数声明,如果有同名属性,会替换掉
变量,函数表达式
函数声明优先 > 变量,函数表达式
4. 执行上下文的数量限制(堆栈溢出)
执行上下文可存在多个,虽然没有明确的数量限制,但如果超出栈分配的空间,会造成堆栈溢出。常见于递归调用,没有终止条件造成死循环的场景。
// 递归调用自身
function foo() {
foo();
}
foo();
// 报错:Uncaught RangeError: Maximum call stack size exceeded
三、执行上下文流程图