
文章目录 21
先给结论:
- 词法环境解决的是:“变量存在哪里,以及查找变量时去哪里找。”
- 闭包解决的是:“函数离开定义它的作用域后,为什么还能访问当时的外部变量。”
两者关系是:
函数在创建时会记录它定义位置对应的外层词法环境。函数与这个词法环境的关联,就是闭包的基础。
一、什么是词法作用域
JavaScript 使用词法作用域 Lexical Scope。
“词法”表示变量的作用域由代码书写位置决定,而不是由函数调用位置决定。
const name = 'global';
function outer() {
const name = 'outer';
function inner() {
console.log(name);
}
return inner;
}
const fn = outer();
function run() {
const name = 'run';
fn();
}
run();输出:
outer虽然 fn() 是在 run() 中调用的,但 inner 定义在 outer 中,所以它寻找 name 时,会沿定义位置的外层作用域查找:
inner 作用域
→ outer 作用域
→ 全局作用域它不会去调用者 run 的作用域中查找。
因此:
变量在哪里查找
取决于函数在哪里定义
不取决于函数在哪里调用二、什么是词法环境
词法环境是 JavaScript 规范中的一种抽象结构,用来记录当前作用域内的变量,以及如何找到外层作用域。
可以简化理解为:
interface LexicalEnvironment {
bindings: Record<string, unknown>;
outer: LexicalEnvironment | null;
}它主要包括两个部分:
- 当前环境中的变量绑定;
- 指向外层词法环境的引用。
例如:
const globalName = 'JavaScript';
function outer() {
const age = 10;
function inner() {
const message = 'hello';
console.log(message, age, globalName);
}
inner();
}
outer();可以抽象为:
inner 词法环境
├─ message = "hello"
└─ outer → outer 词法环境
├─ age = 10
└─ outer → 全局词法环境
├─ globalName = "JavaScript"
└─ outer → null执行 console.log(message, age, globalName) 时:
- 在
inner环境找到message; - 当前环境没有
age,沿outer找到outer环境; - 当前环境没有
globalName,继续找到全局环境; - 如果一直查找到
outer = null仍未找到,就抛出ReferenceError。
这种逐层查找关系通常称为作用域链。
三、什么时候创建词法环境
可以先粗略理解为:进入某个作用域时,会创建对应的词法环境。
全局环境
const appName = 'demo';appName 位于全局词法环境。
函数调用环境
function add(a, b) {
const result = a + b;
return result;
}
add(1, 2);
add(3, 4);每调用一次 add(),都会创建一个独立的函数执行环境:
第一次调用:
a = 1
b = 2
result = 3
第二次调用:
a = 3
b = 4
result = 7因此,两次调用中的局部变量互不影响。
块级环境
let 和 const 具有块级作用域:
{
const value = 1;
let count = 2;
}
console.log(value); // ReferenceError这个代码块会形成相应的块级词法环境。
var 没有普通块级作用域:
{
var value = 1;
}
console.log(value); // 1四、什么是闭包
闭包可以定义为:
函数以及该函数创建时所关联的词法环境。
更通俗地说:
内层函数记住了它定义时的外层变量,即使外层函数已经执行结束,它仍然可以访问这些变量。
经典例子:
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3第一次执行:
const counter = createCounter();会发生:
- 调用
createCounter(); - 创建本次调用的词法环境;
- 创建
count,初始值为0; - 创建
increment函数; increment记录自己定义位置的外层词法环境;- 返回
increment; counter保存对increment的引用。
引用关系可以表示为:
counter
↓
increment 函数
↓
createCounter 本次调用的词法环境
↓
count = 0虽然 createCounter() 已经执行完毕,但返回的函数仍然引用它的词法环境。因此该环境仍然可达,count 不会被垃圾回收。
每次执行 counter(),访问的都是同一个 count。
五、为什么两个闭包互不影响
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1每次调用 createCounter(),都会创建一个新的词法环境:
counterA
↓
increment A
↓
词法环境 A
└─ count = 2
counterB
↓
increment B
↓
词法环境 B
└─ count = 1它们不是共享一个 count,而是分别捕获了不同调用产生的词法环境。
所以准确说法是:
两次调用创建了两个独立的词法环境,两个闭包分别引用各自环境中的
count。
六、闭包什么时候可以被回收
let counter = createCounter();
counter(); // 1
counter = null;赋值之前:
counter → increment → 词法环境 → count赋值之后:
counter → null如果没有其他地方引用 increment 函数,那么:
increment变得不可达;- 它关联的词法环境变得不可达;
count也可以被垃圾回收。
注意,是“可以被回收”,不代表立即回收。真正的回收时间由 JavaScript 引擎决定。
七、闭包的常见使用场景
1. 保存私有状态
function createWallet(initialMoney) {
let money = initialMoney;
return {
deposit(amount) {
money += amount;
},
getBalance() {
return money;
},
};
}
const wallet = createWallet(100);
wallet.deposit(50);
console.log(wallet.getBalance()); // 150外部无法直接访问 money,只能通过暴露的方法操作它。
2. 函数柯里化和参数复用
function multiplyBy(factor) {
return function (value) {
return factor * value;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 153. 事件处理器
function bindButton(button, message) {
button.addEventListener('click', () => {
console.log(message);
});
}事件触发时,外层函数可能早已执行完,但回调仍然能够访问 message。
4. 防抖
function debounce<T extends (...args: never[]) => void>(
fn: T,
delay: number,
) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => {
if (timer !== undefined) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}返回函数通过闭包保存 timer,因此多次调用可以操作同一个定时器。
八、闭包可能导致内存问题吗
闭包本身不是内存泄漏。只有不再需要的数据仍然被可达对象引用时,才可能造成内存长期占用。
function createHandler() {
const largeData = new Array(1_000_000).fill('data');
return function handler() {
console.log(largeData.length);
};
}
const handler = createHandler();
window.addEventListener('click', handler);只要事件监听器仍然引用 handler,handler 关联的词法环境和 largeData 就可能一直存在。
不再使用时应解除引用:
window.removeEventListener('click', handler);常见风险来源包括:
- 未移除的事件监听器;
- 未清理的定时器;
- 长期存在的回调;
- 缓存中保存了不再需要的闭包;
- 闭包捕获了大型对象。
九、常见误区
误区一:只有返回内部函数才叫闭包
不是。函数只要引用了外层词法环境中的变量,就体现了闭包机制。返回内部函数只是最容易观察的场景。
function outer() {
const value = 1;
setTimeout(() => {
console.log(value);
}, 1000);
}这里的定时器回调也形成了闭包。
误区二:闭包会复制外部变量的值
闭包通常保留的是对变量绑定的访问,不是简单复制当时的值:
function outer() {
let value = 1;
const getValue = () => value;
value = 2;
return getValue;
}
console.log(outer()()); // 2误区三:闭包中的变量永远不会释放
只有闭包函数仍然可达时,相关环境才需要保留。闭包失去所有引用后,对应数据仍然可以被垃圾回收。
误区四:闭包和作用域是同一个概念
不是:
- 作用域描述变量在哪些代码区域可访问;
- 词法环境是规范用于管理变量绑定和外层引用的抽象结构;
- 闭包是函数与其定义时词法环境的关联。
十、面试标准回答
可以这样回答:
JavaScript 使用词法作用域,变量查找由代码定义位置决定。词法环境用于记录当前作用域中的变量绑定,并通过外层引用形成作用域链。函数创建时会记录其定义位置对应的外层词法环境,因此函数即使在外层函数执行结束后被调用,仍然能够访问外层变量,这种函数与词法环境的关联称为闭包。闭包常用于保存私有状态、事件回调、函数柯里化和防抖,但如果长期存在的闭包捕获了大型对象,也需要注意内存占用。
最简记忆方式:
词法环境:变量放在哪里、沿哪里查找
闭包:函数记住并访问定义时的外层环境
作用域链:当前环境一路指向外层环境的查找路径相关文章
- 基础知识
幂等键 幂等性 幂等请求这些代表什么
这三个概念紧密相关,是构建健壮分布式系统的基石。
- 面试题-基础
Vue 3 响应式解构:为什么替换 user 后出现“李四 / 王五”
通过一道 Vue 3 代码题,区分 reactive 对象解构、嵌套属性修改与对象替换的行为,并用 toRef 保持源属性关联。文章保留首次作答及追问补答,说明从误判到纠正的过程。
- 面试题-AI相关
从 Embedding 到带出处的知识库问答:最小 RAG 链路与面试复盘
围绕企业知识库问答,复盘从工具调用思路到理解文档分块、向量检索与正文关联的学习过程。结合真实作答与四轮点评,补全 Embedding、相似度、Prompt 组装、引用展示和无资料兜底的基础概念。
评论
0 条评论会在审核后展示,邮箱不会公开。