执行上下文、作用域

执行上下文(执行环境)EC

当JS 执行一段可执行代码(executable code)时,会创建对应的执行上下文,即执行环境(execution context)。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

可执行代码

JS中的可执行代码有:

  1. 全局代码-全局执行环境:最外围的一个执行环境

    浏览器中全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法来创建的。某个执行环境的代码执行完毕之后该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局环境直到应用程序退出/关闭网页时才会被销毁)

  2. 函数代码

    当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,在函数执行完后,栈将其环境弹出,把控制权返回给之前的执行环境

  3. eval代码

EC有三个重要的属性

  1. 变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  2. 作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
  3. this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ECObj: {
    scopeChain: {
    /* 变量对象 + 所有父级执行上下文的变量对象*/
    },
    variableObject: {
    /*函数 arguments/参数,内部变量和函数声明 */
    },
    this: {}
    }
  4. 创建阶段:创建作用域、变量对象(arguments参数,函数,变量)、this的值

    S 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined

  5. 执行阶段:初始化变量的值和函数的引用

执行上下文栈 ECS

当JS开始执行代码的时候,最先遇到的是全局代码,所以初始化的时候首先会向执行上下文栈压入一个全局执行上下文,并且只有当整个应用程序结束的时候,ECStack才会被清空,所以程序结束之前, ECStack 最底部永远有个全局的执行上下文。当执行一个函数的时候,就会创建一个函数执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出

浏览器中的JS解释器被实现为单线程,这也就意味着同一时间只能发生一件事情,其他的行为或事件将会被放在叫做执行栈里面排队。

变量对象 VO

环境中定义的所有变量和函数都保存在这个对象中

  1. 全局环境中,变量对象就是全局对象
  2. 函数环境中,用活动对象AO(activation object, AO)来表示变量对象

未进入执行阶段之前,变量对象(VO)中的属性都不能访问。进入执行阶段之后,变量对象(VO)被激活,变为活动对象(AO),里面的属性都能被访问,然后开始进行执行阶段的操作。

创建变量对象的顺序:

  1. 形参(arguments和其他命名的参数)

    初始化参数名称和值并创建引用的复制,对于没有传递的参数,其值为undefined

  2. 函数声明(非函数表达式)

    为发现的每一个函数,在变量对象上创建一个属性(函数的名字),其有一个指向函数在内存中的引用。如果变量对象已经包含了相同名字的属性,则替换它的值

  3. 变量声明

    为发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为undefined。如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。

  • 上面说的有没有影响说的是声明过程中。函数的声明比变量优先级要高,并且定义过程不会被变量覆盖,除非是赋值

let 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

执行阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(i){
var a = 'hello'
var b = function(){}
function c(){}
}
foo(22)

// 当我们调用foo(22)时 声明过程
// 仅负责处理形参/实参,定义属性的名字,而并不为他们指派具体的值
ECObj = {
scopChain: {...},
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ECObj = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}

当代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域的用途是保证对执行环境有权访问的所有变量和函数的有序访问

提升

  1. 变量声明的提升
  2. 函数声明和定义的提升

    函数表达式不进行提升

作用域链 scope chain

作用域链的前端始终是当前执行环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。作用域的下一个变量对象来自外部(包含)环境,再下一个变量对象则来自下一个外部环境,这样一直延续到全局执行环境

全局执行环境的变量对象始终都是作用域链中的最后一个对象

scope

函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父层环境的变量对象到其中。当调用函数时,会将函数的变量对象添加到作用链的前端。

当查找变量的时候,始终先从当前环境的变量对象中查找(作用域链的前端),如果没有找到,就会从父级(词法层面上的父级)环境的变量对象中查找,一直找到全局环境的变量对象,也就是全局对象,找不到时通常会导致错误发生。这样由多个执行环境的变量对象构成的链表就叫做作用域链

嵌套作用域链(函数内部又定义了函数):内部环境可以通过作用域链访问所有的外部环境

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
//打印的是函数

console.log(foo);
var foo = 1;
function foo(){
console.log("foo");
}
//打印的是函数


function foo(a){
console.log(a)
function a(){}
}
foo(20)
//'function a(){}'

function foo(a){
console.log(a)
var a = 10
}
foo(20)
//'20'


function foo(){
console.log(a)
var a = 10
function a(){}
}
foo()
//'function a(){}'


function foo(a){
var a = 10
function a(){}
console.log(a)
}
foo(20)
//'10'

function foo(a){
var a
function a(){}
console.log(a)
}
foo(20)
//'function a(){}'

function foo3(a){
console.log(a);
var a = 10
function a(){}
console.log(a)
}
foo3(20)
// ƒ a(){}
// 10

延长作用域链

有些语句可以在作用域链的前端添加一个变量对象,该变量对象会在代码执行后被移除

1. try/catch语句的catch块

会创建一个新的变量对象,其中包含的是抛出的错误对象的声明

2. with语句

会将指定的对象添加到作用域链中。将代码的作用域设置到一个特定的对象中

1
2
3
4
5
6
7
function buildUrl(){
var qs= '?debug=true'
with(location){
var url= href + qs
}
return url
}

with语句接收的是location对象,因此其变量对象中就包含了location对象的所有属性和方法。引用变量href时,实际引用的是location.href

严格模式不允许使用with语句

没有块级作用域

  1. 声明变量

    使用var声明的变量会自动添加到最接近的环境。在函数内部,最接近的环境就是函数的局部环境。在with语句中最接近的环境就是函数环境。如果初始化变量没有使用var声明,该变量会自动被添加到全局环境(window)

ES6中的块级作用域

在使用大括号{ }时,声明了一个const或者let的变量时,你就只能在大括号内部使用这一变量

作用域的分类

  1. 词法作用域:函数的作用域在函数定义的时候决定(JS)

    如果在函数内部又定义了函数,那么内层函数可以访问外层函数的变量,但反过来则不行

  2. 动态作用域:调用时决定
1
2
3
4
5
6
7
8
9
10
// 词法作用域
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); //1

在分别定义的不同的函数时,虽然可以在一个函数里调用一个函数,但一个函数依然不能访问其他函数的作用域内部。