作用域 | 执行上下文与作用域

执行上下文

执行上下文:当前代码的执行环境,变量或者函数的上下文决定了它们可以访问哪些数据,以及它们的行为

执行上下文可以分为:

  • 全局执行上下文:只有一个,程序首次运行时创建,它会在浏览器中创建一个全局对象(window对象),使this指向这个全局对象
  • 函数执行上下文:函数被调用时创建,每次调用都会为该函数创建一个新的执行上下文
  • Eval 函数执行上下文:运行eval函数中的代码时创建的执行上下文,少用且不建议使用

执行上下文会在其所有代码执行完毕后被销毁,包括定义在它上面的所有变量和函数

执行上下文栈

执行上下文栈是一种拥有后进先出的数据解构,用于存储代码执行时创建的执行上下文

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var color = 'blue';

function changeColor() {
var anotherColor = 'red';

function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}

swapColors();
}

changeColor();

console.log(color); // red

执行过程可以在 devToolcall stack 中看到,其中 anonyomus 为全局上下文栈;其余为函数上下文栈

img

图解: img

执行过程:

  1. 首先创建了全局执行上下文,压入执行栈,其中的可执行代码开始执行。
  2. 然后调用 changeColor 函数,JS引擎停止执行全局执行上下文,激活函数 changeColor 创建它自己的执行上下文,且把该函数上下文放入执行上下文栈顶,其中的可执行代码开始执行。
  3. changeColor 调用了 swapColors 函数,此时暂停了 changeColor 的执行上下文,创建了 swapColors 函数的新执行上下文,且把该函数执行上下文放入执行上下文栈顶。
  4. swapColors 函数执行完后,其执行上下文从栈顶出栈,回到了 changeColor 执行上下文中继续执行。
  5. changeColor 没有可执行代码,也没有再遇到其他执行上下文了,将其执行上下文从栈顶出栈,回到了 全局执行上下文 中继续执行。
  6. 一旦所有代码执行完毕,JS引擎将从当前栈中移除 全局执行上下文
1
注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。

变量对象

每个执行上下文都有一个关联的变量对象(variable object),这个执行上下文中定义的所有变量和函数都存在于这个变量上

如果执行上下文是函数,则其活动对象(activation object)作为变量对象,活动对象最初只有一个定义变量arguments

作用域

作用域由函数或代码块创建,变量只能在定义它的函数或代码块内使用,超出范围则不可访问,作用域决定了代码区块中变量和其他资源的可见性

作用域可分为:

  • 全局作用域:
  • 函数作用域
  • 块级作用域

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域

  • 最外层函数 和 在最外层函数外面 定义的变量拥有全局作用域
  • 所有未定义直接复制的变量自动声明为拥有全局作用域
  • 所有 window 对象的属性拥有全局作用域

函数作用域

函数作用域顾名思义就是指声明在函数内部的变量,函数作用域一般只在固定的代码片段内可以访问到

1
2
3
4
5
6
7
8
9
10
11
function sayName() {
const name = 'Katrina';
function innerSay() {
console.log(name);
}
innerSay();
};

sayName(); // Katrina
console.log(name); // name is not defined
innerSay(); // innerSay is not defined

块级作用域

块级作用域可以通过let和const声明,所声明的变量在指定块级作用域外无法被访问,会将变量的作用域限制在当前代码块中

块级作用域的特点详见:变量 | var let const区别及应用

词法作用域

词法作用域是定义表达式并能被访问到的区间,即一个声明的词法作用域就是它被定义时所在的作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个全局作用域变量
const name = 'Katrina';

// 在函数体内调用name变量
function sayName() {
return name;
};

console.log(sayName()); // Katrina

/*
name的词法作用域是全局作用域,因为name是在全局环境定义的,词法作用域正是取决于变量定义时的作用域而不是调用时的作用域
*/

作用域链

在当前作用域没有找到所需变量,就去父级寻找,父级没有找到再一层层向上找,直到全局作用域还没找到就放弃,这种一层层的关系就是作用域链

作用域与执行上下文

JavaScript 属于解释型语言,JavaScript 的执行分为:解释执行两个阶段,这两个阶段所做的事并不一样:

解释阶段:

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段:

  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

JavaScript 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的

执行上下文最明显的就是 this 的指向是执行时确定的,而作用域访问的变量是编写代码的结构确定的

作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

一个作用域下可能包含若干个上下文环境,有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)

同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值

参考

你真的了解执行上下文吗?

掌握JavaScript面试:什么是闭包?

深入理解 JavaScript 作用域和作用域链