4.2 执行上下文与作用域
在JavaScript中,执行上下文是理解变量和函数如何访问数据的核心概念。每个上下文都关联一个变量对象(虽然无法直接访问),用于存储该上下文中的变量和函数。
关键要点
- 全局上下文:最外层的执行环境,在浏览器中通常对应
window对象。通过var声明的全局变量和函数会成为window的属性或方法。let和const声明的顶级变量在全局作用域中有效,但不会在window对象上创建属性。 - 函数上下文:每次函数调用时创建,有自己的变量对象(活动对象),包含
arguments(除非使用箭头函数)。函数执行完毕后,上下文被销毁。 - 上下文栈:管理执行流,函数调用时上下文被推入栈,执行完毕弹出。
- 作用域链:决定变量和函数的访问顺序。执行上下文中的变量对象位于作用域链的前端,函数上下文的活动对象紧随其后,然后是包含它的上下文,依此类推,直至全局上下文。
示例1:全局变量访问
var color = "blue";
function changeColor() {
if (color === "blue") {
color = "red";
} else {
color = "blue";
}
}
changeColor();
在这个例子中,changeColor函数可以访问全局变量color,因为color在全局上下文的作用域链中。
示例2:局部作用域与变量替换
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();
在这个例子中,有三个上下文:
- 全局上下文:包含变量
color和函数changeColor。 changeColor局部上下文:包含变量anotherColor和函数swapColors,可以访问全局变量color。swapColors局部上下文:包含变量tempColor,只能在此上下文中访问。swapColors可以访问其父上下文changeColor和全局上下文中的变量。
作用域链确保了变量和函数按照正确的顺序被访问,从而实现了JavaScript的动态作用域特性。在swapColors函数中,可以访问到所有父上下文中的变量,但在外部(如changeColor函数或全局作用域)中,无法访问到swapColors中定义的tempColor变量。下图展示了前面这个例子的作用域链。
上图展示了不同执行上下文之间的线性、有序关系,这些上下文由矩形表示。内部上下文能通过作用域链访问外部上下文中的变量和函数,但外部上下文无法反向访问内部上下文的内容。作用域链确保了每个上下文在搜索变量和函数时,会首先查找自己的变量对象,若未找到,则继续向上一级上下文搜索,直至全局上下文。
在swapColors()的局部上下文中,作用域链包含三个对象:首先是swapColors()自身的变量对象,其次是changeColor()的变量对象,最后是全局变量对象。这一链式结构使得swapColors()能够依次在这三个对象中搜索所需的变量和函数。
相比之下,changeColor()上下文的作用域链仅包含两个对象:其自身的变量对象和全局变量对象。因此,changeColor()无法直接访问swapColors()上下文中的任何内容,因为作用域链是单向的,从内部向外部延伸。
4.2.1 作用域链增强
作用域链增强是JavaScript中一个重要的概念,它允许在执行上下文的基础上临时扩展变量的查找路径。这主要发生在try/catch语句的catch块和with语句中。
try/catch 语句的 catch 块
在try/catch结构中,如果try代码块中的代码抛出了异常,控制流将转移到catch块。此时,catch块会创建一个新的变量对象,并自动将这个对象添加到作用域链的前端。这个新对象包含一个属性,该属性通常命名为error(除非你使用了其他标识符),并引用了被抛出的错误对象。这样,你就可以在catch块中直接通过error(或你指定的标识符)来访问错误对象了。
with 语句
with语句允许你将一个对象作为上下文来执行一系列操作。当执行with语句时,指定的对象会被添加到作用域链的前端。这意味着,在with语句块内部,你可以直接通过属性名来访问该对象的属性,而无需每次都指定对象名。然而,需要注意的是,with语句可能会引入难以追踪的变量引用,因为它会改变作用域链中变量的查找路径。因此,在现代JavaScript编程中,通常建议避免使用with语句。
function buildUrl() {
let qs = "?debug=true";
with(location) {
let url = href + qs; // 这里访问的是 location.href
// 注意:这里的 url 变量是块级作用域,仅在 with 块内有效
}
// 由于 url 是在 with 块内用 let 声明的,因此它在这里是不可见的
// return url; // 这行代码会导致错误,因为 url 在这里未定义
}
在上面的例子中,with语句将location对象添加到了作用域链的前端。因此,在with块内部,你可以直接通过href来访问location.href。然而,需要注意的是,由于url变量是在with块内使用let声明的,因此它仅在with块内有效。一旦退出with块,url变量就不再可用了。
另外,由于qs变量是在buildUrl函数内部使用let声明的,因此它不受with语句的影响,并且在整个函数内部都是可见的。但是,如果qs是在with块内部声明的,那么它也会受到块级作用域的限制,并且仅在with块内有效。
4.2.2 变量声明
JavaScript的变量声明在ES6之后经历了显著变化。var关键字曾是声明变量的唯一方式,但ES6引入了let和const,这两个关键字现在更受推崇。
-
var的函数作用域声明:var声明的变量会被添加到最接近的上下文中,通常是函数的局部上下文。- 如果未声明就初始化变量,它会成为全局变量(严格模式下会报错)。
var声明存在“提升”现象,即变量声明会被提升到作用域顶部,但赋值不会。
-
let的块级作用域声明:let提供块级作用域,由花括号{}界定。- 在同一作用域内不能重复声明
let变量。 let变量也存在提升,但存在“暂时性死区”,在声明前引用会报错。let非常适合在循环中声明迭代变量,避免变量泄漏到循环外部。
-
const的常量声明:const声明的变量必须初始化,且在其生命周期内不能重新赋值。const也遵循块级作用域规则。- 对于对象或数组,
const保证引用不变,但对象内部属性仍可修改。若需完全不可变,可使用Object.freeze()。 - 应尽可能使用
const声明,除非确实需要重新赋值。
标识符查找
- 标识符查找从作用域链前端开始,依次搜索变量对象中的标识符。
- 如果在局部上下文中找到标识符,则停止搜索。
- 如果局部上下文中不存在,则继续沿作用域链向上搜索,直至全局上下文。
- 如果全局上下文中也未找到,则标识符未声明。
// 使用 var 的全局作用域声明
var color = 'blue';
function getColorUsingVar() {
return color; // 返回 'blue',因为全局变量 color 可见
}
console.log(getColorUsingVar()); // 'blue'
// 使用 let 的块级作用域声明
function getColorUsingLet() {
let color = 'red';
{
let color = 'green'; // 块级作用域内的局部变量
return color; // 返回 'green'
}
}
console.log(getColorUsingLet()); // 'green'
// 使用 const 的常量声明
const o1 = {};
o1 = {}; // TypeError: 给常量赋值(不允许重新赋值)
const o2 = { name: 'Jake' };
o2.name = 'John'; // 允许修改对象属性
console.log(o2.name); // 'John'
const frozenObj = Object.freeze({});
frozenObj.newProp = 'value'; // 静默失败,不会添加新属性
console.log(frozenObj.newProp); // undefined