JavaScript 作用域与闭包详解

97 阅读4分钟

JavaScript 作用域与闭包详解

作用域和闭包是 JavaScript 中最重要的概念之一,理解它们对于编写高质量代码至关重要。

一、作用域 (Scope)

1. 作用域基本概念

作用域决定了变量和函数的可访问范围。JavaScript 有以下几种作用域:

  • 全局作用域:在函数外部声明的变量
  • 函数作用域:在函数内部声明的变量
  • 块级作用域 (ES6+): 由 letconst{} 内声明的变量
// 全局作用域
var globalVar = "I'm global";

function example() {
  // 函数作用域
  var functionVar = "I'm in function";
  
  if (true) {
    // 块级作用域
    let blockVar = "I'm in block";
    console.log(blockVar); // 可访问
  }
  
  console.log(blockVar); // 报错: blockVar is not defined
}

2. 作用域链

当访问一个变量时,JavaScript 引擎会按照以下顺序查找:

  1. 当前作用域
  2. 外层作用域
  3. 直到全局作用域
let a = 1;

function outer() {
  let b = 2;
  
  function inner() {
    let c = 3;
    console.log(a + b + c); // 6 (可以访问所有变量)
  }
  
  inner();
}

outer();

3. 变量提升 (Hoisting)

  • var 声明的变量会提升到函数/全局作用域顶部
  • letconst 也有提升,但存在"暂时性死区" (TDZ)
console.log(x); // undefined (变量提升)
var x = 5;

console.log(y); // 报错: Cannot access 'y' before initialization
let y = 10;

二、闭包 (Closure)

1. 闭包基本概念

闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。

简单来说:当一个函数可以访问并记住它被声明时所处的环境(包括变量等),即使这个函数在其他地方被调用,就形成了闭包。

闭包的形成条件

  1. 函数嵌套:一个函数内部定义了另一个函数
  2. 内部函数引用外部函数的变量
  3. 内部函数被外部使用(返回、传递给其他函数等)
function outer() {
  let count = 0; // 局部变量
  
  // 内部函数inner就是一个闭包
  return function inner() {
    count++; // 访问外部函数的变量
    return count;
  };
}

const counter = outer(); // outer执行完毕,按理说count应该被销毁

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

在这个例子中:

  1. inner 函数可以访问 outer 函数的 count 变量
  2. 即使 outer 已经执行完毕,count 仍然被保留
  3. 每次调用 counter() 都会修改并记住 count 的值

2.闭包的优缺点

优点:

  1. 实现数据私有化,创建私有变量
  2. 保持变量在内存中,实现状态持久化
  3. 模块化开发,避免全局污染

缺点:

  1. 过度使用可能导致内存占用过高
  2. 不合理的闭包使用可能导致内存泄漏
  3. 可能增加代码复杂度,降低可读性

3. 闭包的实际应用

1) 数据私有化
function createPerson(name) {
  let age = 0;
  
  return {
    getName: () => name,
    getAge: () => age,
    celebrateBirthday: () => {
      age++;
      return `Happy birthday, ${name}! You're now ${age}.`;
    }
  };
}

const john = createPerson("John");
console.log(john.getName()); // "John"
console.log(john.getAge()); // 0
john.celebrateBirthday();
console.log(john.getAge()); // 1
2) 模块模式
const calculator = (function() {
  let memory = 0;
  
  return {
    add: function(a, b) {
      memory = a + b;
      return memory;
    },
    subtract: function(a, b) {
      memory = a - b;
      return memory;
    },
    getMemory: function() {
      return memory;
    },
    clearMemory: function() {
      memory = 0;
    }
  };
})();

calculator.add(5, 3); // 8
console.log(calculator.getMemory()); // 8
3)函数工厂
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

4. 闭包的注意事项

  1. 内存泄漏风险:闭包会保留对外部变量的引用,可能导致内存无法释放
// 不好的实践
function createHeavyObject() {
  const bigArray = new Array(1000000).fill("data");
  
  return function() {
    console.log("I'm holding a reference to " +bigArray);
  };
}

const leak = createHeavyObject();
leak();
// bigArray 无法被垃圾回收,因为闭包还在引用它
  1. 循环中的闭包问题
// 常见问题
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 全部输出5
  }, 100);
}

// 解决方案1: 使用let (块级作用域)
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 0,1,2,3,4
  }, 100);
}

// 解决方案2: IIFE
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 0,1,2,3,4
    }, 100);
  })(i);
}

三、作用域与闭包的关系

特性作用域闭包
定义变量可访问的范围函数记住并访问其词法作用域的能力
创建时机代码编写时确定函数被定义时创建
生命周期执行上下文结束时销毁只要闭包存在,相关作用域就保持
主要用途控制变量可见性数据封装、私有变量、函数工厂等

四、最佳实践

  1. 优先使用 letconst 替代 var,避免变量提升和全局污染
  2. 合理使用闭包,避免不必要的内存占用
  3. 模块化代码,利用闭包实现封装
  4. 注意循环中的闭包,使用块级作用域或IIFE解决

五、高级应用

1. 函数柯里化 (Currying)

function multiply(a) {
  return function(b) {
    return function(c) {
      return a * b * c;
    };
  };
}

const result = multiply(2)(3)(4); // 24

2. 记忆化 (Memoization)

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const factorial = memoize(function(n) {
  return n <= 1 ? 1 : n * factorial(n - 1);
});

console.log(factorial(5)); // 120 (计算并缓存)
console.log(factorial(5)); // 120 (直接从缓存读取)

理解作用域和闭包是成为高级JavaScript开发者的关键。通过实践这些概念,你将能够编写更高效、更模块化的代码。