大厂面试(五):作用域、作用域链和闭包

163 阅读10分钟

一、作用域与作用域链

1. 作用域的基础概念

作用域 决定了变量、函数和对象在代码中的 可见性生命周期,它通过定义变量的“作用范围”,控制哪些代码可以访问特定的标识符(变量、函数等)。

简单来说就是:一个变量能够被看到,被拿来用的一个范围

举个例子:

let a = "我是变量1"
function foo() {
    let b = "我是变量2";
    console.log(b); 
}

foo();  // 调用foo函数,输出“ 我是变量2 ”
console.log(a); //  输出 "我是变量1"
console.log(b);  //  报错:ReferenceError(无法访问函数作用域变量)

解析:

这段代码中,因为a是用 let 声明在全局作用域(脚本最外层),所以,和它同一层的 console.log(a) 在函数能够正常输出。

再看b,它因为声明在函数foo()中,所以在外面访问不到出现报错,只有通过调用foo()函数才能对b进行输出。

2. 作用域链(Scope Chain)

作用域链 是 JavaScript 引擎查找变量的路径,它遵循 “先当前,再向外” 的规则,在查找变量时执行流程如下:

  1. 当前执行上下文:查找变量时,首先它在当前函数或块级作用域中查找。
  2. 外部作用域链:如果未找到,则沿着作用域链向外部作用域逐层查找。
  3. 全局作用域:如果所有作用域均未找到变量,则抛出错误 ReferenceError

举个例子:

let a = "我是全局变量a"
function outer() {
    let b = "我是out里的变量b";
    inner();
   function inner(){
        let c = "我是inner里的变量c";
        console.log(a); // 我是全局变量a
        console.log(i); // 报错:ReferenceError(无法访问函数作用域变量)
    }
}
outer();

解析: 在上面代码中,在查找变量a时,它先是访问inner函数,当inner函数中未找到,它便会向外一层outer里面查找,就这样一层一层向外,直到在全局作用域中发现a

所以,它的作用域链便是:inner -> outer -> global


二、作用域的类型

1. 全局作用域(Global Scope)

定义

全局作用域 是最外层的作用域,其变量或函数在程序的任何地方都可访问,在浏览器环境中,全局作用域的变量会成为 window 对象的属性。

特点

  • 变量声明:未使用 varlet 或 const 声明的变量默认属于全局作用域。

  • 生命周期:全局变量在程序运行期间始终存在,直到页面关闭或程序结束。

  • 污染风险:过度使用全局变量可能导致命名冲突和内存泄漏。

示例

let globalVar = "我是全局变量"; 

function funcA() {
    console.log("我是全局函数 funcA");
}

function funcB() {
    console.log(globalVar); 
    funcA();
}

funcB(); // 我是全局变量  我是全局函数 funA

解析:

在这段代码中,在全局作用域中定义了全局变量globalVar和全局函数funcA(),它们都可以被任何其他函数调用。


2. 函数作用域(Function Scope)

定义

函数作用域 是函数内部定义的变量或函数的作用域,仅在该函数内部有效。使用 var 声明的变量属于函数作用域,无论其在 函数内部 哪个位置声明。


特点

  • 变量提升var 声明的变量会被提升到函数顶部,但赋值不会提升。
  • 嵌套规则:内部函数可以访问外部函数的变量,但外部函数无法访问内部函数的变量。
  • 作用域限制:函数作用域内的变量在函数外部不可见。

示例

function outer() {
    var a = "我是outer函数作用域变量 a";   
    function inner() {
        var b = "我是inner函数作用域变量 b";
        console.log(a);  // 输出 "我是outer函数作用域变量 a"
    }
 
    inner(); // 调用内部函数 
    console.log(b); //报错:ReferenceError(外部无法访问内部变量)
}

console.log(a); // 报错:ReferenceError(函数作用域变量外部不可见)

解析

  1. 函数作用域变量 a

    • 使用 var 声明的变量a,仅在 outer() 函数内部有效。
    • 内部函数 inner() 可以访问 a(嵌套规则),但outer()函数外部无法访问。
  2. 变量提升(Hoisting)

    • 如果 a 在函数中未声明直接使用,JavaScript 会将其提升到函数顶部。
  3. 嵌套规则

    • 内部函数 inner() 可以访问外部函数 outer() 的变量 a
    • 但外部函数 outer() 无法访问内部函数 inner() 的变量 b
  4. 作用域限制

    • a 和 b 都是函数作用域变量,在函数外部无法访问,因此 console.log(a) 和 console.log(b) 都会报错。

3. 块级作用域(Block Scope)

定义

块级作用域 是由 {} 定义的代码块(如 ifforwhile 等)内的作用域,使用 let 和 const 声明的变量属于块级作用域,而 var 声明的变量不具有块级作用域。


特点

  • 块级绑定let 和 const 声明的变量仅在代码块内有效。
  • 暂时性死区(TDZ)let 和 const 声明的变量在声明前访问会抛出错误,而 var 会返回 undefined
  • 避免变量提升let 和 const 不会像 var 一样提升到代码块顶部。

示例

function foo(){
    console.log(funcVar); // 输出: undefined(TDZ)
    if (true) {
        console.log(blockVar); // 报错:ReferenceError(TDZ)

        let blockVar = "我是块级作用域变量";
        const blockConst = "我是块级常量";  
        var funcVar = "我是函数作用域变量"; 
        
        console.log(blockVar); // 输出: 我是块级作用域变量
    }
console.log(funcVar); //  输出: 我是函数作用域变量
console.log(blockVar); // 报错:ReferenceError(块级作用域变量外部不可见)
}

console.log(blockVar); //  报错:ReferenceError(块级作用域变量外部不可见)

解析

  1. 块级作用域变量 blockVarblockConst

    • 使用 let 和 const 声明,该变量/常量仅在 if 代码块内有效。
    • if 代码块外部无法访问blockVar 。
  2. var 的非块级行为

    • funcVar 使用 var 声明,属于函数作用域,因此在if代码块外部可访问。
    • 这也是 var 与 let/const 的关键区别。
  3. 避免变量提升

    • let/const 声明的变量不会被提升到代码块顶部,因此在声明前访问会触发错误。

4. 词法作用域(Lexical Scope)

定义

词法作用域 也称静态作用域,是 JavaScript 的默认作用域规则,变量的作用域由代码中函数声明的位置决定,而非调用位置,词法作用域在代码编译阶段就已确定,因此被称为“静态作用域”。


特点

  • 作用域链:变量查找路径遵循“当前作用域 → 外部作用域 → 全局作用域”的顺序。
  • 闭包基础:词法作用域是闭包实现的前提条件,内部函数可以访问外部函数的变量。

示例

function outer() {
    var outerVar = "我是外部函数变量"; 
    function inner() {
        console.log(outerVar); // 输出 "我是外部函数变量"
    }
    inner(); // 调用内部函数 inner()
}
outer(); // 输出 "我是外部函数变量"

解析

  1. 词法作用域的定义

    • outerVar 是在 outer 函数中声明的变量,inner 函数在其内部定义,因此 inner 可以访问 outerVar,即使 outerVar 的声明在 inner 之前。
    • 这种嵌套关系在代码编写时就已经确定,与函数调用位置无关。
  2. 作用域链规则

    • inner 函数在查找 outerVar 时,遵循以下路径:

      • 首先检查 inner 函数的作用域(无 outerVar)。
      • 然后向上查找 outer 函数的作用域(找到 outerVar)。
      • 最终输出 outerVar 的值。
  3. 闭包的实现基础

    • inner 函数即使在 outer 函数执行结束后,仍能访问 outerVar,这是闭包的体现。
    • 闭包依赖于词法作用域的特性:内部函数始终能访问外部函数的作用域链。
  4. 静态 vs 动态作用域

    • 与词法作用域相对的是动态作用域(如部分 Shell 脚本语言),变量作用域由函数调用位置决定。
    • JavaScript 采用词法作用域,确保代码可预测性和稳定性。

作用域的对比与关系

作用域类型声明方式变量提升作用域范围生命周期
全局作用域var、不声明整个代码程序运行期间
函数作用域var函数内部函数执行期间
块级作用域letconst代码块(如 iffor代码块执行期间
词法作用域静态规则-由函数定义位置决定依赖闭包和作用域链

三、词法作用域与闭包:静态绑定与动态引用

1. 闭包的本质:词法作用域的动态绑定

闭包(Closure)是函数与其 词法作用域的绑定关系。其核心原理是:

  • 函数在定义时捕获其外部作用域的变量,即使外部函数已执行完毕,这些变量依然被保留。
  • 词法作用域决定了闭包的“查找路径” :函数内部变量的查找优先级始终遵循代码中声明的位置。

示例:闭包的形成

function outer() {
    const secret = "密码"; // 外部函数的局部变量
    return function inner() { // 内部函数引用外部变量
        console.log(secret);
    };
}

const closure = outer(); // outer执行完毕
closure(); // 输出 "密码"(secret未被释放)

解析

  • inner 是 outer 的内部函数,它引用了 outer 的变量 secret

  • outer 执行后,其作用域按理应被销毁,但由于 inner 仍持有 secret 的引用,JavaScript 引擎会保留 outer 的作用域链,形成闭包,就像是outer离开前留给inner的一个小背包,里面有outer留下的一些属性。


2. 闭包的底层机制:作用域链与垃圾回收

(1)作用域链的持久化

  • 每个函数在创建时都会生成一个 作用域链(Scope Chain) ,包含所有外部作用域的引用。
  • 当函数返回时,如果其内部函数仍存在对变量的引用,作用域链不会被销毁,而是作为闭包的一部分保留在内存中。

(2)内存管理与垃圾回收

  • 闭包变量不会被回收:只要闭包函数或其引用的变量未被释放,外部作用域的变量会一直驻留内存。

  • 潜在风险:过度使用闭包可能导致内存泄漏,例如:

    function createLargeArray() {
        const data = new Array(1000000).fill("数据");
        return function () {
            console.log(data[0]);
        };
    }
    const closure = createLargeArray(); // data始终占用内存
    

3. 闭包的核心应用场景

(1)数据封装与私有状态

通过闭包实现模块的私有变量,防止外部直接修改内部状态:

function createCounter() {
    let count = 0; // 私有变量
    return {
        increment: () => count++,
        decrement: () => count--,
        getValue: () => count
    };
}

const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
// 无法直接访问 count

(2)回调函数与异步操作

闭包常用于异步回调中,绑定外部作用域的变量:

function createGreeting(name) {
    return function (message) {
        console.log(`Hello, ${name}! ${message}`);
    };
}

const greetAlice = createGreeting("Alice");
setTimeout(greetAlice, 1000, "How are you?"); 
// 1秒后输出 "Hello, Alice! How are you?"

(3)函数工厂与柯里化

闭包可以动态生成函数,绑定特定参数:

function multiplyBy(factor) {
    return function (number) {
        return number * factor;
    };
}

const double = multiplyBy(2);
console.log(double(5)); // 10
const triple = multiplyBy(3);
console.log(triple(5)); // 15

4. 闭包的注意事项与陷阱

(1)this 的绑定问题

闭包函数中的 this 取决于调用方式,而非定义时的作用域:

const obj = {
    name: "Alice",
    logName: function () {
        setTimeout(function () {
            console.log(this.name); // undefined(this指向window)
        }, 100);
    }
};

obj.logName();

解决方案

  • 使用箭头函数继承外部 this

    logName: function () {
        setTimeout(() => {
            console.log(this.name); // "Alice"
        }, 100);
    }
    
  • 或显式绑定 this

    setTimeout(function () {
        console.log(this.name);
    }.bind(this), 100);
    

(2)避免内存泄漏

  • 及时释放无用闭包:当闭包不再需要时,将其赋值为 null,解除引用,允许垃圾回收:

    const closure = createLargeArray();
    closure = null; // 释放闭包
    

五、总结

JavaScript 的作用域和变量提升机制是理解高级编程的关键。通过词法作用域规则,JavaScript 引擎能够高效地查找变量;通过变量提升,开发者需要理解声明与赋值的时间差;通过闭包,函数可以动态绑定其定义时的环境,实现数据封装和状态持久化。