(附面试题)深入理解作用域、作用域链和闭包

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

1.1 作用域的核心定义

作用域(Scope)是JavaScript中变量和函数的可访问范围,它决定了代码中标识符(变量名、函数名)的可见性和生命周期。JavaScript采用词法作用域(Lexical Scoping),即作用域在函数定义时确定,而非执行时。

示例1:词法作用域验证

  1. function outer() {
  2. const outerVar = 'I am outside!';
  3. function inner() {
  4. console.log(outerVar); // 输出: 'I am outside!'
  5. }
  6. inner();
  7. }
  8. outer();

即使inner函数在outer函数内部执行时被调用,它仍能访问outerVar,因为作用域链在定义时已静态确定。

1.2 作用域的分类与特性

  • 全局作用域:脚本最外层定义的变量,任何函数均可访问。
  • 函数作用域:函数内部定义的变量,仅函数内可访问。
  • 块级作用域(ES6新增):通过let/const定义的变量,仅在代码块(如iffor)内有效。

示例2:块级作用域对比

  1. // var无块级作用域
  2. if (true) {
  3. var varVar = 'var';
  4. }
  5. console.log(varVar); // 输出: 'var'
  6. // let有块级作用域
  7. if (true) {
  8. let letVar = 'let';
  9. }
  10. console.log(letVar); // 报错: letVar is not defined

1.3 面试题解析:作用域相关问题

问题1:以下代码输出什么?为什么?

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

答案:输出三个3。因为var无块级作用域,i在全局作用域中共享,循环结束时i为3。

改进方案:使用let或闭包。

  1. // 使用let
  2. for (let i = 0; i < 3; i++) {
  3. setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2
  4. }
  5. // 使用闭包
  6. for (var i = 0; i < 3; i++) {
  7. (function(j) {
  8. setTimeout(() => console.log(j), 100);
  9. })(i); // 输出: 0, 1, 2
  10. }

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

2.1 作用域链的构成

作用域链(Scope Chain)是函数执行时用于查找变量的链式结构,由当前函数作用域、外层函数作用域及全局作用域依次组成。

示例3:作用域链查找过程

  1. const globalVar = 'Global';
  2. function outer() {
  3. const outerVar = 'Outer';
  4. function inner() {
  5. const innerVar = 'Inner';
  6. console.log(innerVar); // 1. 查找自身作用域
  7. console.log(outerVar); // 2. 查找外层作用域
  8. console.log(globalVar); // 3. 查找全局作用域
  9. }
  10. inner();
  11. }
  12. outer();

2.2 作用域链的优化与性能

  • 避免过度嵌套:深层作用域链会增加变量查找时间。
  • 缓存外层变量:将外层变量赋值给局部变量可减少查找。

示例4:作用域链优化

  1. // 未优化
  2. function heavyCalculation() {
  3. const outerData = fetchData(); // 假设需从外层获取
  4. for (let i = 0; i < 1e6; i++) {
  5. // 每次循环都需沿作用域链查找outerData
  6. process(outerData);
  7. }
  8. }
  9. // 优化后
  10. function heavyCalculation() {
  11. const outerData = fetchData();
  12. const localData = outerData; // 缓存到局部作用域
  13. for (let i = 0; i < 1e6; i++) {
  14. process(localData); // 直接访问局部变量
  15. }
  16. }

2.3 面试题解析:作用域链问题

问题2:以下代码输出什么?为什么?

  1. const a = 1;
  2. function foo() {
  3. console.log(a); // 输出什么?
  4. }
  5. function bar() {
  6. const a = 2;
  7. foo();
  8. }
  9. bar();

答案:输出1。因为foo的作用域链不包含bar的作用域,它仅能访问自身和全局作用域。

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

3.1 闭包的定义与本质

闭包(Closure)是指函数能够访问并记住其定义时的作用域,即使该函数在其定义的作用域之外执行。

示例5:闭包的基本形式

  1. function createCounter() {
  2. let count = 0;
  3. return function() {
  4. count++;
  5. return count;
  6. };
  7. }
  8. const counter = createCounter();
  9. console.log(counter()); // 1
  10. console.log(counter()); // 2

counter函数记住了createCounter作用域中的count变量。

3.2 闭包的常见应用场景

  • 数据封装:创建私有变量。
  • 函数工厂:生成特定功能的函数。
  • 模块模式:实现模块化开发。

示例6:模块模式实现

  1. const module = (function() {
  2. const privateVar = 'Secret';
  3. function privateMethod() {
  4. console.log(privateVar);
  5. }
  6. return {
  7. publicMethod: function() {
  8. privateMethod();
  9. }
  10. };
  11. })();
  12. module.publicMethod(); // 输出: 'Secret'
  13. // module.privateMethod(); // 报错: 无法访问

3.3 闭包的内存管理与注意事项

  • 内存泄漏:闭包会持久化外层变量,需手动释放无用闭包。
  • 循环中的闭包:需注意变量共享问题。

示例7:循环中的闭包问题

  1. // 错误示例
  2. for (var i = 1; i <= 3; i++) {
  3. setTimeout(function() {
  4. console.log('i:', i); // 输出三个3
  5. }, 100);
  6. }
  7. // 正确方案1:使用IIFE
  8. for (var i = 1; i <= 3; i++) {
  9. (function(j) {
  10. setTimeout(function() {
  11. console.log('j:', j); // 输出: 1, 2, 3
  12. }, 100);
  13. })(i);
  14. }
  15. // 正确方案2:使用let
  16. for (let i = 1; i <= 3; i++) {
  17. setTimeout(function() {
  18. console.log('i:', i); // 输出: 1, 2, 3
  19. }, 100);
  20. }

3.4 面试题解析:闭包问题

问题3:如何实现一个计数器,每次调用增加1,且计数器状态不被外部修改?

答案:使用闭包封装私有变量。

  1. function createCounter() {
  2. let count = 0;
  3. return {
  4. increment: function() {
  5. count++;
  6. return count;
  7. },
  8. getCount: function() {
  9. return count;
  10. }
  11. };
  12. }
  13. const counter = createCounter();
  14. console.log(counter.increment()); // 1
  15. console.log(counter.increment()); // 2
  16. console.log(counter.getCount()); // 2
  17. // counter.count = 100; // 无效,count为私有变量

四、综合面试题解析

综合题:以下代码输出什么?为什么?如何修复?

  1. const funcs = [];
  2. for (var i = 0; i < 3; i++) {
  3. funcs.push(function() {
  4. console.log(i);
  5. });
  6. }
  7. funcs.forEach(func => func());

答案

  1. 输出:三个3。因为var无块级作用域,所有函数共享同一个i
  2. 修复方案
    • 使用let
      1. for (let i = 0; i < 3; i++) {
      2. funcs.push(function() {
      3. console.log(i);
      4. });
      5. }
    • 使用闭包:
      1. for (var i = 0; i < 3; i++) {
      2. (function(j) {
      3. funcs.push(function() {
      4. console.log(j);
      5. });
      6. })(i);
      7. }

五、总结与建议

  1. 作用域:理解词法作用域,优先使用let/const避免变量污染。
  2. 作用域链:减少嵌套深度,缓存外层变量提升性能。
  3. 闭包:合理利用封装特性,注意内存泄漏问题。
  4. 面试准备:熟练掌握作用域链查找顺序、闭包应用场景及循环变量问题。

实践建议

  • 编写代码时,明确变量作用域,避免意外覆盖。
  • 使用闭包时,标注闭包持有的变量,便于维护。
  • 通过实际项目练习闭包与模块化开发。