作用域和执行上下文
本文最后更新于:2020年11月4日 凌晨
作用域和执行上下文
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。
每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上;
根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象;
- 因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法(函数内部的使用var定义的不是全局变量)。
- 使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的;
- 上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。
这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。
代码正在执行的上下文的变量对象始终位于作用域链的最前端。
活动对象最初只有一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。
1
2
3
4
5
6
7
8
9
10
11// 举例
// 如下,changeColord的作用域链包括了自己的变量对象, 还包括了全局上下文的变量对象;能访问color是因为可以在作用域链上找到
var color = "blue";
function changeColor() {
if (color === "blue") {
color = "red";
} else {
color = "blue";
}
}
changeColor();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 下面代码现寻找几个上下文,1.全局上下文,2.changeColor局部上下文, 3.swapColors局部上下文
var color = "blue";
function changeColor() {
let anotherColor = "red";
function swapColors() {
let tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问 color、anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor,但访问不到 tempColor
swapColors();
}
// 这里只能访问 color
changeColor();- 内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。
- 上下文之间的连接是线性的、有序的;
- 每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。
作用域增强
执行上下文主要有全局上下文和函数上下文两种;
某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除:
- try/catch 语句的 catch 块—对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
- with 语句—对 with 语句来说,会向作用域链前端添加指定的对象;
1 |
|
变量声明
var的变量声明
在使用 var 声明变量时,变量会被自动添加到最接近的上下文;
如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文;name = "xxx"
;在严格模式下,未经声明就初始化变量会报错。
1 |
|
var 声明会被拿到当前作用域(比如函数)或全局作用域的顶部,位于作用域中所有代码之前;这个现象叫作“提升”(hoisting)。
提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。
1 |
|
let的变量声明
- let的作用域是块级的,块级作用域由最近的一对包含花括号{}界定。
- let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。
- 不能提前使用let变量是因为let变量的暂时性死区;
1 |
|
const变量声明
使用 const 声明的变量必须同时初始化为某个值;
一经声明,在其生命周期的任何时候都不能再重新赋予新值。
其他方面与 let 声明是一样的;
1
2
3
4if (true) {
const a = 0;
}
console.log(a); // ReferenceError: a 没有定义const 声明只应用到顶级原语或者对象。赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。
1
2
3
4
5
6const o1 = {};
o1 = {}; // TypeError: 给常量赋值
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake'当我们声明对象,也不想让修改属性,让整个对象不可修改:
1
2
3
4
5// 可以使用 Object.freeze() 方法
// 该方法在修改对象属性不回报错,但是默认失败
const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefinedJavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。
标识符查找
- 如果在局部上下文中找到该标识符,则搜索停止,变量确定;
- 如果没有找到变量名,则继续沿作用域链搜索;这个过程一直持续到搜索至全局上下文的变量对象。
- 如果仍然没有找到标识符,则说明其未声明。
1 |
|
所以在使用局部变量可以让搜索停止,不继续搜索下一个变量对象,所以局部上下文和后续搜索的上下文中标识符冲突,就不能用后续的相同标识符;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var color = 'blue';
function getColor() {
let color = 'red';
return color;
}
console.log(getColor()); // 'red'
var color = 'blue';
function getColor() {
let color = 'red';
{
// 首先是搜索本层上下文,当本层发现需要的标识符就停止搜索,除非使用window.color特定访问最外层color
let color = 'green';
return color;
}
}
console.log(getColor()); // 'green'