ECMAScript闭包
闭包是函数与其词法环境绑定后形成的可访问组合。
#type / concept
#status / growing
#resource / javascript
#resource / ecmascript
[!info] related notes
ECMAScript闭包
闭包可以先记成一句话:函数把定义时可访问的词法环境一起带走了。
最短定义
- 闭包不是单独一种语法,而是函数和其外层词法环境的组合
- 只要函数在别处执行时,仍然能访问定义位置的变量,就已经进入闭包语境
- 最常见形式是函数返回函数,但回调、事件处理器、模块工厂也都会产生闭包
它为什么成立
- ECMAScript 使用词法作用域,变量可访问性由定义位置决定
- 函数是一等对象,可以被返回、传递、保存
- 只要内部函数还被引用,相关词法环境就可能继续存活
它主要解决什么
- 保留跨调用的私有状态
- 给回调保留创建时的上下文
- 构造函数工厂、模块封装、柯里化等模式都依赖这种能力
最容易误解的点
- 闭包不等于内存泄漏;问题在于不再需要的引用没有释放
- 闭包捕获的是变量绑定,不一定是当时的值快照
- 只背“return 一个内部函数”不够,重点是词法环境没有丢
一个最小例子
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = makeCounter();
next(); // 1
next(); // 2
makeCounter() 已经执行结束,但返回的函数仍然能访问 count,这就是闭包最典型的表现。
这篇和相邻笔记怎么分工
- 看函数定义与调用方式:ecmascript-functions
- 看词法环境、提升、调用栈:ecmascript-execution-context
- 看异步回调里的实际表现:ecmascript异步
面试要点
来自 curry-interview-question 的面试视角整理。
一句话回答
闭包就是函数和其定义时可访问的词法环境形成的组合,因此函数即使在别处执行,仍然能访问外层变量。
面试回答主线
它为什么会出现
因为 JavaScript 使用词法作用域,而函数又能被返回、传递和保存。
它解决什么问题
- 保存私有状态
- 给回调保留上下文
- 实现函数工厂、柯里化、模块封装
常见应用
- 计数器
- 防抖节流
- 事件处理器
- 模块私有变量
常见误区
闭包不等于内存泄漏
闭包本身不是问题,真正的问题是本该释放的引用一直被保留。
闭包捕获的是变量绑定
不是简单的值拷贝,这也是很多循环和异步题的关键。
最短记忆方式
闭包 = 函数把定义时的可访问环境一起带走了。
一句话回答
柯里化是把接收多个参数的函数,转换成多次接收部分参数的函数调用,直到收集齐所有参数后才执行。它依赖闭包保存历史参数。
面试回答主线
它解决什么问题
- 参数预填:提前固定部分参数,生成更专用的函数
- 延迟执行:参数没收集齐之前不执行,适合配置链式调用
- 类型推导:在 TS 里可以更精确地约束每步参数类型
手写实现
最简版(固定两参数):
function add(a: number) {
return function (b: number) {
return a + b;
};
}
console.log(add(2)(3)); // 5
通用版(任意参数,toString 触发求值):
function add(...args: number[]) {
const sum = args.reduce((a, b) => a + b, 0);
const fn = function (...more: number[]) {
return add(...args, ...more);
};
fn.valueOf = () => sum;
return fn;
}
console.log(+add(1)(2)(3)); // 6
和闭包的关系
柯里化内部必须用闭包保存已经接收的参数。每次返回的新函数都携带了之前的参数上下文。
实际应用
- 函数组合:
compose/pipe里常用柯里化让函数更容易串联 - 事件处理:
const handleClick = (id) => () => { ... } - 配置函数:先传配置,再传数据,最后执行
- React/Vue 场景:生成带预设参数的回调处理器
常见误区
柯里化不等于部分应用
- 柯里化:严格地每次只接收一个参数,直到收集完
- 部分应用:可以一次接收多个参数,返回剩余参数的函数
面试里如果题目是 add(1)(2)(3),通常期望的是柯里化风格。
不要只写代码不说思路
手写时先说”用闭包保存参数,判断参数是否收齐,收齐就执行,没收齐就返回新函数”,再写代码。
最短记忆方式
柯里化 = 分步收参数 + 闭包存历史 + 收齐再执行