JS基础-执行上下文

执行上下文(也称执行环境)是JS一个重要的概念,它定义了变量或者函数有权访问的其他数据。其中变量对象是上下文中一个重要的概念,它就好比存储了改上下文变量和函数的容器。

执行上下文

JS的代码并不是一行一行执行的,而是一段一段的解析执行的。这里的一段就是指执行上下文。

1
2
3
4
5
6
7
8
9
a = 2;

foo(); // 2

var a;

function foo() {
console.log(a);
}

在JS中分成两种执行环境,分别为全局环境和函数环境。全局环境在浏览器就是脚本的最外层。

1
2
3
4
5
6
7
8
9
<script>
// 全局上下文

var a = 1;

function fun() {
// 函数上下文
}
</script>

JS代码的执行上下文的执行顺序用一个栈来维护,叫做执行上下文栈。脚本一开始执行就把全局上下压进栈里,当执行一个函数时,就会创建一个函数上下文进栈。当函数执行完时就会出栈。全局环境永远在栈底,直到程序退出后才出栈。

1
2
3
4
5
6
7
8
9
function foo() {
bar();
}

function bar() {
//
}

foo();

上面代码的执行上下文栈的变化如下:

1
2
3
4
5
ECStack.push(globalContext)
ECStack.push(foo)
ECStack.push(bar)
ECStack.pop()
ECStack.pop()

变量对象

每个执行环境都有一个存储变量和函数的对象,称为变量对象(VO)。全局环境的这个对象我们一般认为是window, 所以我们在全局环境声明的变量或函数都会成为window对象的属性:

1
2
3
4
5
6
var a = 1;

function foo() {}

console.log(a === window.a); // true
console.log(foo === window.foo); // true

每个函数的执行环境也有一个变量对象,可以存储函数参数,变量和函数,但是变量对象我们无法访问。只有当函数被执行的时候,变量对象会用arguments初始化并激活,这是可以称为活动对象(AO), 并且可以访问定义的变量。因此变量对象和活动对象其实是一个东西。

函数的执行分为两个阶段:

  • 进入阶段:

    • 函数参数会作为活动对象的属性,值为参数的传值,没有则为undefined
    • 声明的函数会作为对象的属性,值为函数的引用。函数声明会存在同名覆盖
    • 声明的变量会作为对象的属性,值为undefined。不会影响到同名变量的形参
  • 执行阶段: 变量的值求赋值AO相应的属性

如下面的例子:

1
2
3
4
5
6
7
8
9
10
function foo(a) {
var b = 2;
function c() {}
var d = function() {};

b = 3;

}

foo(1);

在进入阶段对象的AO为:

1
2
3
4
5
6
7
8
9
10
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}

在执行阶段b和d会被赋值:

1
2
3
4
5
6
7
8
9
10
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}

变量提升

知道了JS的执行上下文从创建到执行的变化就不难解析变量提升的机制了,比如上面的例子中:

1
2
3
4
5
6
7
8
9
a = 2;

foo(); // 2

function foo() {
console.log(a);
}

var a;

当执行这段代码的进入阶段,变量对象会激活为活动对象,处理变量和函数的声明:

1
2
3
4
AO = {
a: undefined,
foo: reference to function foo(){
}

到了执行阶段,当遇到a=2时,会为活动对象的属性赋值。这时候a已经定义在对象上,所以不会报未声明的错误。最后活动对象就变成了:

1
2
3
4
AO = {
a: 2,
foo: reference to function foo(){
}

上面的代码看起来像变量的声明被提升到执行环境的最顶部,但是这仅限于用var声明的变量。比如es6中的 letconst 就不存在这种机制。

思考题:

下面的代码的执行过程有啥区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();

// 2

var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();

上面两段代码执行结果一样,但是执行过程的上下文栈变化不一样。第一段的栈变化如下:

1
2
3
4
ECStack.push(checkscope)
ECStack.push(f)
ECStack.pop()
ECStack.pop()

而第二段的变化如下:

1
2
3
4
ECStack.push(checkscope)
ECStack.pop()
ECStack.push(f)
ECStack.pop()

参考

JavaScript深入之执行上下文栈