JavaScript 闭包 × C++ 类比:彻底搞懂闭包

JavaScript 闭包 × C++ 类比:彻底搞懂闭包 🎯

如果你学过 C++,那恭喜你,理解闭包会快得多。因为闭包本质上就是 C++ 里一个带有成员变量的对象


一、最核心的类比 🔑

先看一个 JavaScript 闭包:

javascript 复制代码
function createCounter() {
    let count = 0;          // "被记住"的变量

    return function() {     // 返回一个函数
        count++;
        console.log(count);
    };
}

let counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

用 C++ 怎么写出完全一样的效果?

cpp 复制代码
#include <iostream>

class Counter {
private:
    int count;              // "被记住"的变量 → 成员变量

public:
    Counter() : count(0) {} // 构造函数 → 相当于外部函数初始化变量

    void operator()() {     // 重载 () 运算符 → 相当于返回的内部函数
        count++;
        std::cout << count << std::endl;
    }
};

int main() {
    Counter counter;        // 相当于 let counter = createCounter()
    counter(); // 1
    counter(); // 2
    counter(); // 3
}

看到了吗?

JavaScript 闭包 C++ 对应物
外部函数 createCounter 类的构造函数
被捕获的变量 count 类的 private 成员变量
返回的内部函数 重载 operator()仿函数对象
调用 counter() 调用对象的 operator()

💡 一句话总结:JavaScript 的闭包 ≈ C++ 中一个携带了私有数据的可调用对象。


二、C++11 的 Lambda ------ 最像闭包的东西 🎯

C++11 引入了 lambda 表达式,它和 JavaScript 闭包几乎是一模一样的概念。

JavaScript 闭包

javascript 复制代码
function createAdder(x) {
    return function(y) {    // 捕获了外部的 x
        return x + y;
    };
}

let add10 = createAdder(10);
console.log(add10(5));  // 15
console.log(add10(20)); // 30

C++ Lambda(几乎一样的写法!)

cpp 复制代码
#include <iostream>
#include <functional>

std::function<int(int)> createAdder(int x) {
    return [x](int y) {    // [x] 捕获了外部的 x ------ 这就是闭包!
        return x + y;
    };
}

int main() {
    auto add10 = createAdder(10);
    std::cout << add10(5)  << std::endl; // 15
    std::cout << add10(20) << std::endl; // 30
}

关键对比:捕获列表 []

C++ lambda 的方括号 [] 就是在显式声明"我要记住哪些外部变量"。

而 JavaScript 是自动捕获的,不需要你声明。

cpp 复制代码
int a = 1, b = 2, c = 3;

// C++ ------ 你必须明确说"我要捕获谁"
auto f1 = [a, b]()  { return a + b; };     // 只捕获 a, b(值拷贝)
auto f2 = [&a, &b]() { return a + b; };    // 捕获 a, b 的引用
auto f3 = [=]()      { return a + b + c; }; // 捕获所有(值拷贝)
auto f4 = [&]()      { return a + b + c; }; // 捕获所有(引用)
javascript 复制代码
// JavaScript ------ 自动捕获,不需要声明
let a = 1, b = 2, c = 3;

let f = function() { return a + b + c; }; // 自动"看到"外面的一切

C++ 是"显式闭包",JavaScript 是"隐式闭包"。


三、值捕获 vs 引用捕获 ------ 这是最大的区别! ⚠️

C++ 中你可以选择

cpp 复制代码
int x = 10;

auto byValue = [x]() mutable {   // 值捕获:拷贝了一份 x
    x++;                           // 修改的是副本
    std::cout << "lambda内: " << x << std::endl;
};

byValue();                              // lambda内: 11
std::cout << "lambda外: " << x << std::endl; // lambda外: 10 ← 原始 x 没变!
cpp 复制代码
int x = 10;

auto byRef = [&x]() {            // 引用捕获:直接操作原始 x
    x++;
    std::cout << "lambda内: " << x << std::endl;
};

byRef();                                // lambda内: 11
std::cout << "lambda外: " << x << std::endl; // lambda外: 11 ← 原始 x 变了!

JavaScript 永远是"引用捕获"

javascript 复制代码
let x = 10;

let fn = function() {
    x++;                    // 直接修改外部的 x,没得选
    console.log("函数内:", x);
};

fn();                       // 函数内: 11
console.log("函数外:", x);   // 函数外: 11 ← 原始 x 变了!

JavaScript 的闭包相当于 C++ lambda 中永远使用 [&](引用捕获全部)。

这就是为什么 JavaScript 有那个经典的循环陷阱:

javascript 复制代码
// JavaScript 的坑
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);     // 全部输出 3,因为"引用捕获"了同一个 i
    }, 1000);
}

如果用 C++ 思维理解,就是:

cpp 复制代码
// C++ 等价理解
int i;
std::vector<std::function<void()>> tasks;

for (i = 0; i < 3; i++) {
    tasks.push_back([&i]() {       // 引用捕获 → 全部指向同一个 i
        std::cout << i << std::endl;
    });
}
// 此时 i = 3
for (auto& task : tasks) {
    task();  // 全部输出 3!和 JavaScript 一模一样的问题
}

修复方式也一样 ------ 改为"值捕获":

cpp 复制代码
// C++ 修复:改为值捕获
for (int i = 0; i < 3; i++) {
    tasks.push_back([i]() {        // 值捕获 → 每个 lambda 有自己的 i 副本
        std::cout << i << std::endl;
    });
}
// 输出:0, 1, 2 ✅
javascript 复制代码
// JavaScript 修复方案1:用 IIFE 创建值副本
for (var i = 0; i < 3; i++) {
    (function(j) {                  // j 是 i 的副本,相当于"值捕获"
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })(i);
}
// 输出:0, 1, 2 ✅

// JavaScript 修复方案2:用 let(每轮循环自动创建新变量)
for (let i = 0; i < 3; i++) {     // let 每轮都创建新的 i,类似值捕获
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
// 输出:0, 1, 2 ✅

四、私有变量的对比 🔒

C++ 用 private

cpp 复制代码
class BankAccount {
private:
    double balance;              // 私有,外部无法直接访问

public:
    BankAccount(double init) : balance(init) {}

    void deposit(double amount) {
        if (amount > 0) balance += amount;
    }

    void withdraw(double amount) {
        if (amount <= balance) balance -= amount;
    }

    double getBalance() const {
        return balance;
    }
};

int main() {
    BankAccount account(500);
    account.deposit(200);
    // account.balance = 999999;  // ❌ 编译错误!private!
    std::cout << account.getBalance(); // 700
}

JavaScript 用闭包实现同样的效果

javascript 复制代码
function createBankAccount(init) {
    let balance = init;          // 闭包变量 = 私有成员

    return {
        deposit(amount) {
            if (amount > 0) balance += amount;
        },
        withdraw(amount) {
            if (amount <= balance) balance -= amount;
        },
        getBalance() {
            return balance;
        }
    };
}

let account = createBankAccount(500);
account.deposit(200);
// account.balance = 999999;    // 无效!balance 根本不是对象的属性
console.log(account.getBalance()); // 700

💡 C++ 用 class + private 保护数据,JavaScript 用闭包保护数据。效果完全一样。


五、内存管理的区别 ⚙️

这也是 C++ 程序员最关心的部分。

C++ ------ 你必须小心生命周期

cpp 复制代码
// ❌ 危险!引用捕获了局部变量!
std::function<int()> createBad() {
    int x = 42;
    return [&x]() {       // 引用捕获 x
        return x;          // x 已经被销毁了!未定义行为(悬垂引用)!
    };
}

// ✅ 安全:值捕获
std::function<int()> createGood() {
    int x = 42;
    return [x]() {        // 值捕获:x 被复制到 lambda 内部
        return x;          // 安全!lambda 拥有自己的 x 副本
    };
}

JavaScript ------ 垃圾回收器帮你管

javascript 复制代码
function create() {
    let x = 42;
    return function() {
        return x;          // 永远安全!
    };
    // x 不会被销毁,因为垃圾回收器发现还有函数引用着它
}

let fn = create(); // x 活着
fn = null;         // 现在没人引用内部函数了 → x 终于可以被回收了

C++ 需要程序员自己管理闭包变量的生命周期,JavaScript 的垃圾回收器自动搞定。

这也意味着 JavaScript 更容易出现内存泄漏 (变量一直不被回收),而 C++ 更容易出现悬垂引用(变量已经没了但还在用)。


六、完整对照表 📊

概念 C++ JavaScript
闭包的载体 lambda / 仿函数对象 普通函数
捕获方式 显式:[x] [&x] [=] [&] 隐式自动捕获(类似 [&]
值捕获 [x] 需要手动用 IIFE 或 let 模拟
引用捕获 [&x] 默认行为
私有变量 class + private 闭包变量
内存管理 手动(注意悬垂引用) 自动 GC(注意内存泄漏)
闭包的本质 带状态的可调用对象 带状态的可调用对象

七、一个"神级"类比 🧠

如果你真正理解了 C++,那么:

复制代码
JavaScript 闭包  ===  C++ 编译器自动生成的一个匿名类的实例

其中:
  - 被捕获的外部变量  →  自动生成的 private 成员变量
  - 内部函数的逻辑    →  自动生成的 operator()
  - 调用外部函数       →  自动调用构造函数,初始化成员变量
  - 返回内部函数       →  返回这个匿名类的实例

实际上,C++ 编译器处理 lambda 时,真的就是这么做的

cpp 复制代码
// 你写的:
auto fn = [x, &y](int z) { return x + y + z; };

// 编译器在背后生成的(大致等价):
class __anonymous_lambda_01 {
private:
    int x;      // 值捕获
    int& y;     // 引用捕获
public:
    __anonymous_lambda_01(int _x, int& _y) : x(_x), y(_y) {}
    int operator()(int z) const {
        return x + y + z;
    }
};
auto fn = __anonymous_lambda_01(x, y);

JavaScript 的闭包也是同样的原理,只不过 JavaScript 引擎帮你把这一切都藏起来了。


八、最后总结 🎬

如果你是 C++ 程序员,记住这三句话就够了:

  1. JavaScript 闭包 = C++ 仿函数 / lambda,本质都是"函数 + 它记住的数据"
  2. JavaScript 默认引用捕获 [&],C++ 可以自由选择值/引用捕获
  3. C++ 用 class + private 做封装,JavaScript 用闭包做封装,殊途同归

闭包不神秘,它就是一个随身携带了数据的函数。C++ 程序员天天在用,只是换了种写法而已。 😄


后记

2026年4月16日15点00分于上海,在opus 4.6辅助下完成。

相关推荐
6Hzlia1 小时前
【Hot 100 刷题计划】 LeetCode 72. 编辑距离 | C++ 经典 DP 增删改状态转移
c++·算法·leetcode
赵优秀一一1 小时前
SQLAlchemy学习记录
开发语言·数据库·python
无限进步_2 小时前
【C++】寻找字符串中第一个只出现一次的字符
开发语言·c++·ide·windows·git·github·visual studio
孬甭_2 小时前
字符函数及字符串函数
c语言·开发语言
摇滚侠2 小时前
Java 进阶教程,全面剖析 Java 多线程编程
java·开发语言
KevinCyao2 小时前
php彩信接口代码示例:PHP使用cURL调用彩信网关发送图文消息
android·开发语言·php
装疯迷窍_A2 小时前
以举证方位线生成工具为例,分享如何在Arcgis中创建Python工具箱(含源码)
开发语言·python·arcgis·变更调查·举证照片
楼田莉子2 小时前
Linux网络:IP协议
linux·服务器·网络·c++·学习·tcp/ip
网域小星球2 小时前
C 语言从 0 入门(二十五)|位运算与位段:底层开发、嵌入式核心
c语言·开发语言