JavaScript的秘密生活:理解闭包

发布: (2025年12月12日 GMT+8 11:15)
8 min read
原文: Dev.to

Source: Dev.to

函数回顾

Timothy 湿漉漉地来到图书馆,雨水从他的外套上滴落。他刚在伦敦的街道上奔跑,急于向 Margaret 说明一件让他对函数工作原理产生怀疑的事。

“这不可能,”他一边甩掉头发上的水说,“我写的代码违背了我对内存和作用域的所有认识。”

Margaret 抬起头,放下手中的茶。

“给我看看。”

Timothy 拿出笔记本电脑并敲下:

function createCounter() {
  let count = 0;

  return function() {
    count++;
    console.log(count);
  };
}

const counter = createCounter();

counter(); // 1
counter(); // 2
counter(); // 3

“我不明白,”Timothy 说,“当 createCounter 执行完毕后,它的局部变量应该被垃圾回收。count 变量应该不存在了。但返回的函数仍然能访问它。这是在访问一个已经不该存在的变量。”

Margaret 微笑道。

“你发现了闭包。你感到困惑是对的——这看起来像是对编程语言工作方式的违背。不过在 JavaScript 中,这不是 bug,而是特性。”

词法作用域

Margaret 拿出笔记本。

“在解释发生了什么之前,我们需要先了解词法作用域。你已经在第 1 章学过作用域——变量的存放位置。但还有更深层的东西。”

她写下:

function outer() {
  let outerVar = "I'm in outer";

  function inner() {
    console.log(outerVar); // Can I see this?
  }

  inner();
}

outer(); // "I'm in outer"

“当 inner 去寻找 outerVar 时,它会去哪里找?” Margaret 问。

“它会去变量声明的地方,”Timothy 回答,“在外层函数里。”

“正是如此。inner 被调用的地点并不重要,重要的是它被写在哪里。这就是词法作用域:函数在它们被定义的作用域中寻找变量,而不是在它们被执行的地方寻找。”

返回内部函数

function outer() {
  let outerVar = "I'm in outer";

  function inner() {
    console.log(outerVar);
  }

  return inner; // Return the function itself
}

const myFunction = outer(); // `outer()` finishes here
myFunction(); // "I'm in outer" - but how?!

“当 outer 执行完毕后,它的局部变量应该消失,”Timothy 缓慢地说,“但 inner 仍然在访问 outerVar。这个变量怎么还能活着?”

“因为 JavaScript 不让它死去,”Margaret 回答,“当你返回 inner 时,JavaScript 会说:*‘这个函数引用了 outerVar。我还不能回收这个变量,因为函数以后可能会被调用,需要它。’*于是 JavaScript 为 inner 保持了 outerVar 在内存中的存在。”

“所以变量会持久化?”

“变量会持久化。函数记住了它的出生地。这种组合——函数加上它需要的作用域变量——被称为 闭包。”

背包类比

Margaret 向后靠了靠。

“想象每个函数在创建时都会得到一个背包。函数被定义的那一刻,JavaScript 环顾四周并说:*‘这个函数会需要哪些变量?’*然后把这些变量装进背包。”

function createGreeter(greeting) {
  // createGreeter's scope
  return function(name) {
    // This inner function needs `greeting` from createGreeter's scope
    console.log(greeting + ", " + name);
  };
}

const sayHello = createGreeter("Hello");
const sayHi    = createGreeter("Hi");

sayHello("Alice"); // "Hello, Alice"
sayHi("Bob");      // "Hi, Bob"

“当你定义内部函数时,它会环顾四周,看到 greeting,于是把 greeting 装进自己的背包。以后,无论这个函数在哪里被调用,它都会带着 `greeting”。”

Timothy 指着代码说。

“所以 sayHellosayHi 各自有不同的背包?不同的 greeting 值?”

“没错。每次 createGreeter 运行时,都会创建一个新作用域,生成一个新的 greeting 变量,返回的函数就把对应的 greeting 放进自己的背包。它们完全互相独立。”

用闭包实现数据隐私

function createAccount() {
  let balance = 100;

  return {
    deposit: function(amount) {
      balance += amount;
      console.log("Balance: " + balance);
    },

    withdraw: function(amount) {
      balance -= amount;
      console.log("Balance: " + balance);
    },

    getBalance: function() {
      return balance;
    }
  };
}

const account = createAccount();
account.deposit(50);    // Balance: 150
account.withdraw(20); // Balance: 130
account.getBalance(); // 130

“这三个方法都是对 balance 的闭包。它们看到的是同一个 balance 值,对吗?”

“是的。而且关键是,它们持有的是 实时引用,而不是副本。”

进一步演示:

account.deposit(50);    // Balance: 150
account.withdraw(20);  // Balance: 130
account.deposit(100);  // Balance: 230

// All three methods access the SAME variable.
// If one changes it, all others see the change.

直接访问私有变量会失败:

console.log(account.balance); // undefined
account.balance = 9999;       // Creates a new property; does not affect the closure variable
console.log(account.getBalance()); // Still 230

balance 变量被完全隐藏。唯一能与之交互的方式就是通过你公开的这三个方法。这才是真正的数据隐私。”

模块模式

const calculator = (function() {
  let lastResult = 0;

  return {
    add: function(a, b) {
      lastResult = a + b;
      return lastResult;
    },

    subtract: function(a, b) {
      lastResult = a - b;
      return lastResult;
    },

    getLastResult: function() {
      return lastResult;
    }
  };
})();

calculator.add(10, 5);        // 15
calculator.subtract(20, 8); // 12
calculator.getLastResult(); // 12

// `lastResult` is private—no one can access or modify it except through these methods.

“这就是模块。你使用立即调用函数表达式(IIFE)创建一个私有作用域,然后返回一个包含公共方法的对象,这些方法可以访问私有变量。在 ES6 类出现之前,模块模式是 JavaScript 中创建带有私有状态对象的主要手段。”

经典闭包陷阱

function setupButtons() {
  for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
      console.log("Button " + i + " clicked");
    }, 1000);
  }
}

“这就是第 1 章里提到的 var 问题,与闭包混在一起。使用 var 时,变量 i 属于整个 setupButtons 函数,而不是循环块。所有三个回调共享同一个 i,循环结束后它的值是 4”(循环在 i变为4` 时退出)。”

修复方法: 使用 let 进行块级作用域,或为每次迭代创建新作用域。

// Using let (ES6)
function setupButtons() {
  for (let i = 1; i <= 3; i++) {
    setTimeout(function() {
      console.log("Button " + i + " clicked");
    }, 1000);
  }
}
// Using an IIFE
function setupButtons() {
  for (var i = 1; i <= 3; i++) {
    (function(index) {
      setTimeout(function() {
        console.log("Button " + index + " clicked");
      }, 1000);
    })(i);
  }
}
Back to Blog

相关文章

阅读更多 »

Swift #12:函数

函数是由大括号 { 包围并以名称标识的代码块。不同于在循环和条件中使用的代码块……