第 4 章 变量、作用域与内存(4.2 执行上下文与作用域)

121 阅读7分钟

4.2 执行上下文与作用域

在JavaScript中,执行上下文是理解变量和函数如何访问数据的核心概念。每个上下文都关联一个变量对象(虽然无法直接访问),用于存储该上下文中的变量和函数。

关键要点

  • 全局上下文:最外层的执行环境,在浏览器中通常对应window对象。通过var声明的全局变量和函数会成为window的属性或方法。letconst声明的顶级变量在全局作用域中有效,但不会在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变量。下图展示了前面这个例子的作用域链。

image.png

上图展示了不同执行上下文之间的线性、有序关系,这些上下文由矩形表示。内部上下文能通过作用域链访问外部上下文中的变量和函数,但外部上下文无法反向访问内部上下文的内容。作用域链确保了每个上下文在搜索变量和函数时,会首先查找自己的变量对象,若未找到,则继续向上一级上下文搜索,直至全局上下文。

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引入了letconst,这两个关键字现在更受推崇。

  1. var的函数作用域声明

    • var声明的变量会被添加到最接近的上下文中,通常是函数的局部上下文。
    • 如果未声明就初始化变量,它会成为全局变量(严格模式下会报错)。
    • var声明存在“提升”现象,即变量声明会被提升到作用域顶部,但赋值不会。
  2. let的块级作用域声明

    • let提供块级作用域,由花括号{}界定。
    • 在同一作用域内不能重复声明let变量。
    • let变量也存在提升,但存在“暂时性死区”,在声明前引用会报错。
    • let非常适合在循环中声明迭代变量,避免变量泄漏到循环外部。
  3. 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