执行上下文
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
在一段 js 代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”
“准备工作”包括:
- 变量、函数表达式 -- 变量声明,默认赋值为
undefined
- 函数声明 -- 赋值
this -- 赋值
这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
变量赋值发生在 "执行阶段"。
函数每被调用一次,都会产生一个新的执行上下文环境。
函数在定义的时候(不是调用的时候),就已经确定了函数体内部 自由变量 的作用域(JavaScript 中的作用域是静态作用域)

所以如果代码段是函数体,“准备工作”还需要包括:
- 参数 -- 赋值
arguments -- 赋值
- 自由变量的取值作用域 -- 赋值
给执行上下文环境下一个通俗的定义:在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用 undefined 占个空
上下文结构:

执行上下文栈
执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。


补充
激活其它上下文的某个上下文被称为 调用者(caller),被激活的上下文被称为 被调用者(callee)。
当一个 caller 激活了一个 callee,那么这个 caller 就会暂停它自身的执行,然后将控制权交给这个 callee。于是这个 callee 被放入堆栈,称为进行中的上下文[running/active execution context]。当这个 callee 的上下文结束之后,会把控制权再次交给它的 caller,然后 caller 会在刚才暂停的地方继续执行。在这个 caller 结束之后,会继续触发其他的上下文。一个 callee 可以用返回( return )或者抛出异常( exception )来结束自身的上下文。
所有的 ECMAScript 的程序执行都可以看做是一个执行上下文堆栈[execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。
函数上下文中的变量对象
VO ( variable object ) 变量对象:是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:变量变量、函数声明 ( FunctionDeclaration, 缩写为 FD )、函数的形参。
AO ( activation object ) 活动对象:是在进入函数上下文时被创建的。在函数执行上下文中,VO 是不能直接访问的,此时由 AO 扮演 VO 的角色。所以函数上下文中 VO(functionContext) === AO。
示例:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
当进入带有参数 10 的 test 函数上下文之前,VO 表现为如下
VO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
当进入带有参数 10 的 test 函数上下文时,AO 表现为如下
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
注意,AO 里并不包含函数 x。这是因为 x 是一个函数表达式( FunctionExpression, 缩写为 FE ) 而不是函数声明,函数表达式不会影响 VO。 不管怎样,函数 _e 同样也是函数表达式,但是就像我们下面将看到的那样,因为它分配给了变量 e,所以它可以通过名称 e 来访问。
test 函数执行时,AO 表现如下
这个周期内,AO/VO 已经拥有了属性( 不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值 undefined )。
AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;
再次注意,因为 FunctionExpression _e 保存到了已声明的变量 e 上,所以它仍然存在于内存中。而 FunctionExpression x 却不存在于 AO/VO 中,也就是说如果我们想尝试调用 x 函数,不管在函数定义之前还是之后,都会出现一个错误 “x is not defined”,未保存的函数表达式只有在它自己的定义或递归中才能被调用。
补充示例
arguments:
function foo(x, y, z) {
// 声明的函数参数数量 foo(x, y, z)
console.log(foo.length); // 3
// 真正传进来的参数个数 (x, y)
console.log(arguments.length); // 2
// 参数的 callee 是函数自身
console.log(arguments.callee === foo); // true
// 参数共享
console.log(x === arguments[0]); // true
console.log(x); // 10
arguments[0] = 20;
console.log(x); // 20
x = 30;
console.log(arguments[0]); // 30
// 不过,没有传进来的参数 z,和参数的第 3 个索引值是不共享的
z = 40;
console.log(arguments[2]); // undefined
arguments[2] = 50;
console.log(z); // 40
}
foo(10, 20);
关于 VO 的特殊情况
1. 变量声明
全局对象( global / window )的变量对象( VO )是其自身,即:VO(globalContext) === global / window
任何时候,变量只能通过 var / let / const 关键字才能声明。 a = 10 仅仅是给全局对象创建了一个新属性(但它不是变量)。
关于变量,还有一个重要的知识点。变量相对于简单属性来说,变量有一个特性 (attribute):{DontDelete},这个特性的含义就是不能用 delete 操作符直接删除变量属性。
a = 10;
console.log(window.a); // 10
console.log(delete a); // true
console.log(window.a); // undefined
var b = 20;
console.log(window.b); // 20
console.log(delete b); // false
console.log(window.b); // still 20
但是这个规则在有个上下文里不起作用,那就是 eval 上下文,变量没有 {DontDelete} 特性。
eval('var a = 10;');
console.log(window.a); // 10
console.log(delete a); // true
console.log(window.a); // undefined
2. 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?
在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。
var a = 12;
function f() {}
console.log(window.a); // 12
console.log(window.f); // f(){}
但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。
let aa = 1;
const bb = 2;
console.log(window.aa); // undefined
console.log(window.bb); // undefined

通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中。
参考
执行上下文
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
“准备工作”包括:
undefinedthis-- 赋值这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
函数每被调用一次,都会产生一个新的执行上下文环境。
函数在定义的时候(不是调用的时候),就已经确定了函数体内部 自由变量 的作用域(JavaScript 中的作用域是静态作用域)
所以如果代码段是函数体,“准备工作”还需要包括:
arguments-- 赋值给执行上下文环境下一个通俗的定义:在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用
undefined占个空上下文结构:
执行上下文栈
补充
激活其它上下文的某个上下文被称为 调用者(caller),被激活的上下文被称为 被调用者(callee)。
当一个
caller激活了一个callee,那么这个caller就会暂停它自身的执行,然后将控制权交给这个callee。于是这个callee被放入堆栈,称为进行中的上下文[running/active execution context]。当这个callee的上下文结束之后,会把控制权再次交给它的caller,然后caller会在刚才暂停的地方继续执行。在这个caller结束之后,会继续触发其他的上下文。一个callee可以用返回(return)或者抛出异常(exception)来结束自身的上下文。所有的 ECMAScript 的程序执行都可以看做是一个执行上下文堆栈[execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。
函数上下文中的变量对象
示例:
当进入带有参数 10 的 test 函数上下文之前,VO 表现为如下
当进入带有参数 10 的 test 函数上下文时,AO 表现为如下
注意,AO 里并不包含函数
x。这是因为x是一个函数表达式( FunctionExpression, 缩写为 FE ) 而不是函数声明,函数表达式不会影响 VO。 不管怎样,函数_e同样也是函数表达式,但是就像我们下面将看到的那样,因为它分配给了变量e,所以它可以通过名称e来访问。test 函数执行时,AO 表现如下
这个周期内,AO/VO 已经拥有了属性( 不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值
undefined)。再次注意,因为 FunctionExpression
_e保存到了已声明的变量e上,所以它仍然存在于内存中。而 FunctionExpressionx却不存在于 AO/VO 中,也就是说如果我们想尝试调用x函数,不管在函数定义之前还是之后,都会出现一个错误 “x is not defined”,未保存的函数表达式只有在它自己的定义或递归中才能被调用。补充示例
arguments:关于 VO 的特殊情况
1. 变量声明
但是这个规则在有个上下文里不起作用,那就是
eval上下文,变量没有{DontDelete}特性。2. 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?
在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。
但 ES6 规定,
var命令和function命令声明的全局变量,依旧是顶层对象的属性,但let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。通过上图也可以看到,在全局作用域中,用
let和const声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中。参考