Javascript 函数作用域

133 阅读4分钟

在 JavaScript 中,理解作用域的概念对于编写健壮和高效的代码至关重要。作用域决定了变量或函数在代码中的可见性和生命周期。以下是对函数作用域和块作用域的深入探讨:

函数作用域

定义: 函数作用域指的是一个变量在函数内声明后,只能在该函数内部访问。这种作用域类型是 JavaScript 的早期作用域规则。

特性

  1. 函数边界: 函数是作用域的边界。无论在函数内部嵌套了多少层作用域,函数外部都无法访问其中声明的变量。

    function exampleFunction() {
        var a = 10;
        console.log(a); // 10
    }
    console.log(a); // ReferenceError: a is not defined
    
  2. 变量提升: 使用 var 声明的变量会被提升到函数作用域的顶部。这意味着变量可以在声明之前使用,但值为 undefined

    function hoistingExample() {
        console.log(a); // undefined
        var a = 5;
    }
    hoistingExample();
    
  3. 全局污染: 如果在函数内没有使用 varletconst 声明变量,则变量会被默认添加到全局作用域(非严格模式下)。

    function globalPollution() {
        a = 20;
    }
    globalPollution();
    console.log(a); // 20
    

优势与局限

  • 优势: 代码模块化,通过函数封装变量,避免全局命名冲突。
  • 局限: 缺乏细粒度控制,不适合更复杂的作用域管理。

块作用域

定义: 块作用域指的是变量只能在其定义的代码块(由花括号 {} 包围)中访问。ES6 引入了 letconst,为 JavaScript 提供了块级作用域。

特性

  1. 块的边界: 变量只在定义它的块内有效。

    {
        let b = 15;
        console.log(b); // 15
    }
    console.log(b); // ReferenceError: b is not defined
    
  2. 不存在变量提升: 块作用域中的变量在声明之前无法访问,称为“暂时性死区(Temporal Dead Zone, TDZ)”。

    {
        console.log(c); // ReferenceError: Cannot access 'c' before initialization
        let c = 30;
    }
    
  3. 无全局污染: 即使在非严格模式下,使用 letconst 声明的变量也不会被添加到全局作用域。

  4. 迭代变量绑定: let 在循环中具有独立的块作用域。

    for (let i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 0); // 0, 1, 2
    }
    

优势与局限

  • 优势: 提供更精确的变量控制,减少作用域污染,避免常见的 var 引起的错误。
  • 局限: 需要开发者更好地理解 TDZ 和块作用域特性。

函数作用域与块作用域的比较

特性函数作用域块作用域
适用范围整个函数特定代码块
声明关键词varletconst
变量提升
暂时性死区
全局污染风险

常见面试题及答案

  1. 解释变量提升的原理并给出示例。

    • 答:变量提升是指 var 声明的变量会在其作用域内被提升到顶部。在提升阶段,变量的值为 undefined
    console.log(a); // undefined
    var a = 10;
    
  2. 比较 varletconst 的区别。

    • 答:
      1. var 有函数作用域,letconst 有块作用域。
      2. var 存在变量提升,letconst 不存在。
      3. const 声明的变量必须立即赋值且不可变。
    var x = 10; // 函数作用域
    let y = 20; // 块作用域
    const z = 30; // 不可重新赋值
    
  3. 如何避免全局变量污染?

    • 答:使用 IIFE(立即执行函数表达式)、严格模式或模块化。
    (function() {
        var a = 10;
        console.log(a); // 10
    })();
    console.log(a); // ReferenceError
    
  4. 在循环中使用 varlet 的区别是什么?

    • 答:var 声明的变量在整个函数范围内共享,而 let 在每次循环迭代中拥有独立的块作用域。
    for (var i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 0); // 3, 3, 3
    }
    
    for (let i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 0); // 0, 1, 2
    }
    
  5. 什么是暂时性死区(TDZ)?如何触发?

    • 答:TDZ 是指在块作用域中,letconst 声明的变量在声明之前无法被访问。
    {
        console.log(a); // ReferenceError
        let a = 10;
    }
    
  6. 如何在 JavaScript 中创建私有变量?

    • 答:使用闭包或模块化可以创建私有变量。
    function createCounter() {
        let count = 0;
        return function() {
            count++;
            return count;
        };
    }
    const counter = createCounter();
    console.log(counter()); // 1
    console.log(counter()); // 2
    
  7. 以下代码输出什么,为什么?

    function test() {
        console.log(x); // undefined
        var x = 10;
        let y = 20;
        if (true) {
            var x = 30;
            let y = 40;
            console.log(x, y); // 30, 40
        }
        console.log(x, y); // 30, 20
    }
    test();
    
    • 答:
      1. 第一行输出 undefined,因为变量 x 被提升但未赋值。
      2. 块内 var x 改变了函数作用域中的 xlet y 是块作用域变量,独立于外部的 y
      3. 最后一行,x 的值是 30(被 var 修改),y 的值是外部块作用域中的 20。