深入JS中的作用域和执行上下文

作用域:代码中定义变量的区域,确定执行代码访问变量的权限。

JS采用词法作用域(静态作用域),所以函数的作用域在函数定义的时候就决定了,也就是说函数的作用域基于函数创建的位置。相对的是动态作用域,函数的作用域在函数调用的时候决定。

作用域分类:全局作用域、函数作用域和eval()作用域(不讨论)

JS代码的整个执行过程,分为两个阶段:代码编译阶段和代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码。执行阶段由js引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

执行上下文

当JS执行到一段可执行代码的时候就会创建对应的执行上下文,JS引擎创建了执行上下文栈(ECS)管理执行上下文。执行上下文有三个重要属性:变量对象(VO)、作用域链(Scope Chain)、this。

执行上下文的生命周期可以分为两个阶段:创建阶段和代码执行阶段。

创建上下文阶段

在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。

变量对象(VO)

与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

  1. 全局上下文中的变量对象初始化是全局对象
  2. 函数上下文中的变量对象(VO只包括Arguments对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始化的属性值,这个时候VO会被激活为AO
  4. 代码执行阶段,会再次修改变量对象的属性值
>变量对象的创建
  1. 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性与属性值
  2. 检查当前上下文中的函数声明,在VO中以函数名建立一个属性,函数所在内存地址的引用为属性值。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖
  3. 检查当前的变量声明,在VO中以变量名建立一个属性名,属性值为undefined。如果该变量名已经存在,为了防止同名函数被修改为undefined,会直接跳过该变量,原属性值不会被修改

代码执行阶段

顺序执行代码,根据代码,修改变量对象的值

总结完了那看两个代码吧~

1
2
3
4
5
console.log(foo);
function foo() {
console.log('function foo')
}
var foo = 20;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 首先,创建上下文阶段:

EC = {
VO: {},
scopeChain: {},
this: '',
}

// 接着,创建变量对象:
VO = {
arguments: {···},
foo: <foo reference> // 表示函数foo的地址引用
// 因为在检查var声明的变量foo的时候发现与当前的变量对象中的foo属性同名,则忽略给其赋值undefined
}

// 进入执行阶段,VO-->AO,顺序执行代码
AO = {
arguments: {···},
foo: <foo reference>
}

// 执行顺序变成如下:
function foo() { // 这里foo函数在函数上下文顶部是因为函数声明提升
console.log('function foo')
}
console.log(foo); // ƒ foo() { console.log('function foo') }
foo = 20; // 顺序执行代码到这里,才会给var声明的同名变量赋值
1
2
3
4
5
function foo() { 
console.log('function foo')
}
var foo = 20;
console.log(foo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 首先,创建上下文阶段:

EC = {
VO: {},
scopeChain: {},
this: '',
}

// 接着,创建变量对象:
VO = {
arguments: {···},
foo: <foo reference> // 表示函数foo的地址引用
// 因为在检查var声明的变量foo的时候发现与当前的变量对象中的foo属性同名,则忽略给其赋值undefined
}

// 进入执行阶段,VO-->AO,顺序执行代码
AO = {
arguments: {···},
foo: <foo reference>
}

// 执行顺序变成如下:
function foo() { // 这里foo函数在函数上下文顶部是因为函数声明提升
console.log('function foo')
}
foo = 20;
console.log(foo); // 20