JavaScript 作用域:从执行机制到块级作用域的演进

0 阅读7分钟

JavaScript 作用域:从执行机制到块级作用域的演进

在 JavaScript 中,作用域是控制变量和函数可见性与生命周期的核心机制。理解作用域不仅能帮助我们写出更可预测的代码,还能规避因变量提升、作用域污染等问题导致的 bugs。本文将结合具体代码示例,从执行机制、变量提升到块级作用域的实现,全面解析 JavaScript 作用域的核心概念。

一、作用域的本质:变量的 "生存规则"

作用域指的是程序中定义变量的区域,它决定了变量在哪些地方可以被访问(可见性),以及变量会存在多久(生命周期)。JavaScript 中有三种主要的作用域类型:

  • 全局作用域:在代码的任何地方都能访问,生命周期与页面一致(页面关闭前始终存在)。
  • 函数局部作用域:仅在函数内部可访问,生命周期随函数执行结束而销毁。
  • 块级作用域:ES6 新增,在 {} 包裹的块(如 iffor、单独的 {} 等)内部生效,生命周期随块执行结束而销毁。

二、JS 执行机制:编译与执行的 "两步走"

JavaScript 引擎(如 V8)执行代码分为编译执行两个阶段,这一过程直接影响作用域的表现:

  1. 编译阶段:引擎会扫描代码,确定变量和函数的声明位置,创建执行上下文(包含变量环境、词法环境等信息)。
  2. 执行阶段:引擎按顺序执行代码,处理变量赋值、函数调用等操作,依赖编译阶段创建的执行上下文。

执行上下文是作用域的具体载体,每个函数调用都会创建一个新的执行上下文并压入调用栈,执行完成后出栈并销毁(变量随之回收)。

三、变量提升:设计缺陷与现象

早期 JavaScript(ES5 及之前)存在 "变量提升"(hoisting)特性:变量和函数的声明会被 "提升" 到其作用域的顶部,但赋值不会。这是 JS 设计初期为简化实现留下的缺陷,可能导致与直觉不符的结果。

代码示例:变量提升的表现

showName(); // 输出:"函数showName 执行了"
console.log(myname); // 输出:undefined(变量声明提升,赋值未提升)

var myname = "路飞"; // 变量声明被提升到作用域顶部
function showName() { // 函数整体被提升
    console.log('函数showName 执行了');
}

image.png

解释

  • 函数 showName 的声明被完整提升,因此可以在定义前调用。
  • var 声明的 myname 被提升到作用域顶部,但赋值 ="路飞" 留在原地,因此执行 console.log(myname) 时为 undefined

四、全局作用域与函数局部作用域

1. 全局作用域

在函数外声明的变量属于全局作用域,可在任何地方访问。

2. 函数局部作用域

在函数内用 var/let/const 声明的变量属于局部作用域,仅在函数内可见。

代码示例:全局与局部作用域

var globalVar = "我是全局变量"; // 全局作用域
function myFunction(){
    var localVar = "我是局部变量"; // 函数局部作用域
    console.log(globalVar); // 输出:"我是全局变量"(可访问全局变量)
    console.log(localVar); // 输出:"我是局部变量"(可访问局部变量)
}
myFunction();
console.log(globalVar); // 输出:"我是全局变量"(全局可见)
console.log(localVar); // 报错:localVar is not defined(局部变量外部不可见)

image.png

解释

  • globalVar 在全局作用域声明,因此函数内外都可访问。
  • localVar 在 myFunction 内部声明,属于局部作用域,函数外部访问会报错。

五、块级作用域:ES6 的改进

ES5 不支持块级作用域,var 声明的变量会 "穿透" iffor 等块结构,导致变量污染。ES6 引入 let 和 const,正式支持块级作用域,变量仅在 {} 内部有效。

1. var 无块级作用域的问题

var name = "流萤"; // 全局变量
function showName(){
    console.log(name); // 输出:undefined(var声明的name被提升到函数顶部)
    if(true){ 
        var name = "大厂的苗子"; // var无块级作用域,声明被提升到函数作用域
    }
    console.log(name); // 输出:"大厂的苗子"(块内赋值影响函数内变量)
}
showName();

image.png

解释var 声明的 name 会被提升到 showName 函数作用域顶部(而非块级作用域),因此第一次打印时变量已声明但未赋值(undefined),块内赋值后函数内的 name 被覆盖。

2. let 支持块级作用域

let name = "流萤"; // 全局变量
function showName(){
    console.log(name); // 输出:流萤
    if(false){ 
        let name = "大厂的苗子"; // let有块级作用域,仅在if块内有效
    }
}
showName();

image.png

解释let 声明的 name 被限制在 if 块级作用域内,函数作用域中不存在该变量。但由于 "暂时性死区"(let 变量声明前不可访问),函数内打印 name 时会向上查找全局 name

3. 暂时性死区

let name = '流萤';
{
    console.log(name); // 报错:Cannot access 'name' before initialization
    let name = '大厂的苗子'; // let声明触发暂时性死区
}

image.png

解释:块内用 let 声明 name 后,整个块会形成 "暂时性死区",在 let 声明前访问 name 会直接报错(即使外部有同名全局变量)。这避免了变量提升导致的意外覆盖。

六、for 循环中的块级作用域

for 循环是块级作用域的典型场景,let 和 var 的表现差异显著:

function foo() {
    // 若用var声明i:
    // for(var i=0;i<7;i++){} 
    // console.log(i); // 输出7var无块级作用域,i泄漏到函数作用域)

    // 用let声明i:
    for(let i=0;i<7;i++){} 
    console.log(i); // 报错:i is not defined(let限制i在for块内)
}
foo();

解释

  • var 声明的 i 会穿透 for 循环块,泄漏到函数作用域,循环结束后仍可访问(值为 7)。
  • let 声明的 i 被限制在 for 块级作用域内,循环外访问会报错,避免了变量泄漏。

七、执行上下文视角:var 与 let 的 "一国两制"

ES6 为了兼容旧代码,采用了 "一国两制" 的处理方式:在执行上下文中,var 和 let 被分别存储在不同区域:

  • 变量环境:存储 var 声明的变量(支持变量提升,无块级作用域)。
  • 词法环境:存储 let/const 声明的变量(支持块级作用域,有暂时性死区),内部维护一个栈结构,块级作用域执行时入栈,执行完出栈。

代码示例:var 与 let 的作用域混合

function foo (){
    var a = 1; // 存储在变量环境(函数作用域)
    let b = 2; // 存储在词法环境(函数作用域)
    {
        let b = 3; // 词法环境栈顶(块级作用域)
        var c = 4; // 变量环境(函数作用域,穿透块)
        let d = 5; // 词法环境栈顶(块级作用域)
        console.log(a); // 1(访问变量环境的a)
        console.log(b); // 3(访问词法环境栈顶的b)
    }
    console.log(b); // 2(词法环境栈顶出栈,访问函数作用域的b)
    console.log(c); // 4(变量环境的c,块外可访问)
    console.log(d); // 报错:d is not defined(词法环境栈顶出栈,d已销毁)
}
foo();

解释

  • var 声明的 a 和 c 存储在变量环境,属于函数作用域,块内外均可访问。
  • let 声明的 b(函数内)和 b(块内)、d 存储在词法环境的栈结构中:块执行时,块内 b 和 d 入栈,优先访问栈顶变量;块执行完后栈顶出栈,函数内的 b 恢复可见,d 则被销毁。

八、总结

JavaScript 作用域的发展反映了语言的进化:从 ES5 仅有全局和函数作用域、依赖变量提升的简单设计,到 ES6 引入块级作用域、通过 let/const 解决变量污染问题。核心要点包括:

  1. 作用域控制变量的可见性和生命周期(全局、函数、块级)。
  2. 变量提升是 ES5 的设计缺陷,var 声明的变量会被提升到作用域顶部。
  3. ES6 的 let/const 通过块级作用域和暂时性死区解决了变量提升的问题。
  4. 执行上下文中的变量环境(var)和词法环境(let/const)实现了新旧特性的兼容。
  5. 在实际开发中,建议优先使用 let/const 替代 var,利用块级作用域减少变量污染,写出更可靠的代码。