深度解析:作用域、作用域链与闭包全攻略(附面试题)

一、作用域:变量访问的规则边界

1.1 作用域的核心定义

作用域(Scope)是JavaScript中变量和函数可访问的上下文范围,决定了标识符(变量名、函数名)的可见性和生命周期。它像一道隐形的边界,将代码划分为不同的访问区域。

关键特性

  • 静态性:ES6前的作用域(函数作用域)在代码编写阶段确定,ES6的块级作用域(let/const)在词法分析阶段确定。
  • 层级性:作用域可嵌套,形成作用域链。
  • 隔离性:内层作用域可访问外层变量,反之则不行(除非通过闭包)。

1.2 作用域的分类与代码示例

1.2.1 全局作用域

任何不在函数或代码块中的变量都属于全局作用域,可通过window对象(浏览器)或global对象(Node.js)访问。

  1. var globalVar = "I'm global";
  2. function checkGlobal() {
  3. console.log(globalVar); // 输出: I'm global
  4. }
  5. checkGlobal();
  6. console.log(window.globalVar); // 浏览器中输出: I'm global

风险点:全局变量易被污染,导致命名冲突。

1.2.2 函数作用域

通过function声明的变量仅在函数内部有效。

  1. function outer() {
  2. var funcVar = "Function scope";
  3. console.log(funcVar); // 输出: Function scope
  4. }
  5. outer();
  6. console.log(funcVar); // 报错: funcVar is not defined

1.2.3 块级作用域(ES6+)

letconst声明的变量仅在代码块({}、循环、条件语句)内有效。

  1. if (true) {
  2. let blockVar = "Block scope";
  3. const constVar = "Constant";
  4. console.log(blockVar); // 输出: Block scope
  5. }
  6. console.log(blockVar); // 报错: blockVar is not defined

面试题varlet/const在作用域上的区别?

  • var存在变量提升,无块级作用域;let/const有块级作用域,且存在暂时性死区(TDZ)。

二、作用域链:变量查找的路径

2.1 作用域链的形成机制

当函数被创建时,会保存其所在的作用域链(即定义时的词法环境)。函数执行时,若内部未找到变量,会沿作用域链向上查找,直至全局作用域。

示例

  1. var outerVar = "Outer";
  2. function outer() {
  3. var innerVar = "Inner";
  4. function inner() {
  5. console.log(outerVar); // 输出: Outer(沿作用域链向上查找)
  6. console.log(innerVar); // 输出: Inner(当前作用域)
  7. }
  8. inner();
  9. }
  10. outer();

2.2 作用域链的查找规则

  1. 从内到外:先查找当前作用域,再逐层向外。
  2. 就近原则:优先使用最近定义的变量。
  3. 静态绑定:作用域链在函数定义时确定,与调用位置无关(词法作用域)。

面试题:以下代码的输出是什么?

  1. var a = 1;
  2. function foo() {
  3. console.log(a); // 输出: undefined(变量提升)
  4. var a = 2;
  5. }
  6. foo();
  • 解析:var a在函数内声明并提升,但赋值发生在console.log之后,故输出undefined

三、闭包:作用域的持久化

3.1 闭包的定义与本质

闭包(Closure)是指函数能够访问并记住其定义时的作用域,即使该函数在其定义的作用域之外执行。其本质是函数与其词法环境的组合。

核心条件

  1. 内部函数引用了外部函数的变量。
  2. 外部函数执行完毕后,其作用域未被销毁(因内部函数仍持有引用)。

3.2 闭包的典型应用场景

3.2.1 数据封装与私有变量

  1. function createCounter() {
  2. let count = 0;
  3. return {
  4. increment: () => ++count,
  5. getCount: () => count
  6. };
  7. }
  8. const counter = createCounter();
  9. counter.increment();
  10. console.log(counter.getCount()); // 输出: 1
  11. console.log(count); // 报错: count is not defined(外部无法访问)

3.2.2 函数柯里化

  1. function multiply(a) {
  2. return function(b) {
  3. return a * b;
  4. };
  5. }
  6. const double = multiply(2);
  7. console.log(double(5)); // 输出: 10

3.2.3 事件回调与异步操作

  1. function setupClick(id) {
  2. const element = document.getElementById(id);
  3. element.addEventListener('click', () => {
  4. console.log(`Clicked ${id}`); // 闭包保留id的引用
  5. });
  6. }
  7. setupClick('btn');

3.3 闭包的性能与内存管理

  • 优点:实现数据隐藏、状态保持。
  • 缺点:可能导致内存泄漏(如未清理的DOM事件监听器)。
  • 优化建议
    • 及时解除闭包对无用变量的引用(如设为null)。
    • 避免在循环中创建闭包(可用let或IIFE解决)。

面试题:以下代码的输出及问题?

  1. for (var i = 1; i <= 3; i++) {
  2. setTimeout(() => console.log(i), 100);
  3. }
  4. // 输出: 4 4 4(var无块级作用域,所有回调共享同一个i)
  • 修复方案:使用let或IIFE。
    1. // 方案1:let
    2. for (let i = 1; i <= 3; i++) {
    3. setTimeout(() => console.log(i), 100);
    4. }
    5. // 方案2:IIFE
    6. for (var i = 1; i <= 3; i++) {
    7. (function(j) {
    8. setTimeout(() => console.log(j), 100);
    9. })(i);
    10. }

四、面试真题解析

真题1:闭包与变量提升

  1. var name = "Global";
  2. function showName() {
  3. console.log(name); // 输出什么?
  4. var name = "Local";
  5. }
  6. showName();
  • 答案undefined(变量提升导致nameconsole.log时已声明但未赋值)。

真题2:作用域链的深度

  1. var a = 1;
  2. function f1() {
  3. var a = 2;
  4. function f2() {
  5. var a = 3;
  6. console.log(a); // 输出: 3
  7. function f3() {
  8. console.log(a); // 输出: 3(沿作用域链向上查找最近定义)
  9. }
  10. f3();
  11. }
  12. f2();
  13. }
  14. f1();

真题3:闭包的内存泄漏

  1. function heavyTask() {
  2. const largeData = new Array(1000000).fill('*');
  3. return function() {
  4. console.log('Task executed');
  5. };
  6. }
  7. const task = heavyTask(); // largeData未被释放
  8. // 修复:task = null; 解除引用

五、总结与最佳实践

  1. 作用域选择:优先使用let/const避免变量污染,合理利用块级作用域。
  2. 闭包使用:明确闭包的生命周期,及时释放无用引用。
  3. 性能优化:避免在循环中创建闭包,减少作用域链的查找深度。
  4. 调试技巧:利用开发者工具的“Scope”面板查看闭包变量。

通过深入理解作用域、作用域链和闭包,开发者能够编写出更高效、可维护的代码,并在面试中展现扎实的JavaScript基础。