JavaScript 闭包与递归:状态封装、回调和调用栈边界
本文目录
闭包不是一种特殊语法,而是函数携带其定义时词法环境的结果。每创建一个函数,就建立了它对外层绑定的访问关系;当外部仍能访问该函数时,函数所需的环境会继续存在。闭包本身不是“内存泄漏”,只有不再需要的引用长期留存,才会造成资源无法释放。
用闭包隔离每个按钮的状态
点赞计数适合演示闭包:外部拿不到私有计数,只能调用公开动作。每次调用工厂函数都会创建独立计数;这比把共享计数放到全局变量更容易测试和理解。
function createLikeAction(button) {
let count = 0;
return function handleClick() {
count += 1;
button.textContent = '赞 (' + count + ')';
};
}
for (const button of document.querySelectorAll('[data-like]')) {
button.addEventListener('click', createLikeAction(button));
}
如果按钮离开 DOM,长生命周期的全局集合仍保存其监听器或函数时,节点可能无法回收。组件卸载时移除监听、清空队列,并避免闭包意外捕获大型页面对象。
this 与闭包是两套规则
普通函数中的 this 通常由调用形式决定:obj.method() 中通常指向 obj,独立调用时则受严格模式和调用环境影响。闭包访问外层词法变量的规则不会把外层 this 自动带进普通回调。箭头函数没有自己的 this,它从定义处词法捕获;需要动态接收事件按钮作为 this 时,普通函数更清楚,也可直接使用 event.currentTarget。
旧式“缓存 var that = this”是历史写法之一,现代代码往往能通过箭头函数、显式参数或组件 API 表达依赖。迁移时先看原有调用点,不能为了消除一个变量就改变回调里的 this 语义。
const account = {
name: 'Haoge',
createReader() {
const prefix = this.name;
return () => prefix + ' 的阅读记录';
},
};
const readLabel = account.createReader();
readLabel(); // Haoge 的阅读记录
递归要控制输入和栈深
递归函数包含基线条件和缩小后的子问题。没有基线会无限调用;即使有基线,深度很大的输入也可能超过调用栈。对不受控输入优先使用循环、显式栈或队列,并先验证整数范围、数组大小和树形数据深度。
对十进制整数求数字和,可以按剩余数字迭代;接受文本时先决定负号、空格和前导零的业务语义,避免把身份证号、订单号等标识误当成数值转换。
function sumDigits(raw) {
const text = String(raw).trim();
if (!/^-?\d+$/.test(text)) throw new TypeError('请输入整数');
return [...text.replace(/^-/, '')].reduce((sum, digit) => sum + Number(digit), 0);
}
sumDigits('-129'); // 12
递归遍历目录或 JSON 树时还要处理循环引用、空节点、最大深度和异常;普通 JSON 文本本身不能包含对象引用环,但运行时对象可以。
检查清单
- 闭包是否只保留它真正需要的变量与 DOM 引用?
- 一个工厂函数的每个实例是否拥有独立状态?
this是调用时绑定还是词法捕获,代码是否一眼可辨?- 递归是否有明确终止条件、输入上限和可承受的最大深度?
- 不可信输入是否有大小和时间预算,是否有循环引用处理?
参考:MDN Closures、MDN this、MDN Arrow functions、MDN Recursion、MDN Event.currentTarget。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。