已验证问答
读者问过的问题、AI 分身的回答——且作者本人看过并盖章认可。再有人问相似的问题,分身会直接使用这里的答案。
opacity 和 z-index 的区别
结论:z-index 控制元素的层叠顺序,opacity 控制元素的透明度,两者解决的是不同问题【W1】。 z-index:谁显示在谁上面 .box-a { position: absolute; z-index: 2; } .box-b { position: absolute; z-index: 1; } 当两个元素发生重叠时,通常 z-index 越大,元素越靠上。 但要注意:z-index 不是全局排名,它受到层叠上下文(stacking context)影响。 <div class="parent-a"> <div class="child">A</div> </div> <div class="parent-b"> <div class="child">B</div> </div> .parent-a { position: relative; z-index: 1; } .parent-b { position: relative; z-index: 2; } .child { position: absolute; z-index: 9999; } 虽然 .parent-a .child 的 z-index 是 9999,但它仍可能被 .parent-b 整个层叠上下文压在下面。子元素无法突破父级层叠上下文。 opacity:元素有多透明 .box { opacity: 0.5; } 取值范围通常是: opacity: 1; /* 完全不透明 */ opacity: 0.5; /* 半透明 */ opacity: 0; /* 完全透明,但仍存在 */ opacity: 0 的元素仍然可能: 占据布局空间; 接收鼠标事件; 参与层叠关系; 影响其子元素的显示效果。 如果希望它完全不参与交互,可以结合: .hidden { opacity: 0; pointer-events: none; } 容易混淆的地方:opacity 会创建层叠上下文 当 opacity 小于 1 时,元素会创建新的 stacking context。因此,下面这段代码中,opacity 可能间接影响最终的绘制顺序: .parent { opacity: 0.99; } .child { position: relative; z-index: 9999; } 这不是因为 opacity 比 z-index “优先级更高”,而是因为 .parent 创建了新的层叠上下文。透明度本身与 z-index 没有直接的优先级竞争【W2】【W4】。 对比 | 属性 | 作用 | 是否影响透明度 | 是否影响层叠顺序 | |---|---|---:|---:| | z-index | 控制重叠元素的前后顺序 | 否 | 是 | | opacity | 控制元素及其子元素的透明度 | 是 | 间接影响,因为可能创建 stacking context | 实际排查覆盖问题时,我一般按这个顺序检查: 元素是否重叠; position、z-index 是否生效; 父元素是否创建了 stacking context; 是否存在 opacity < 1、transform、filter、isolation 等属性; 是否被父级的层叠上下文限制。
2026-08-14 验证
浏览器构建完渲染树页面有东西了吗?
还没有。构建完渲染树,只能说明浏览器已经确定了“哪些节点要显示,以及它们的样式是什么”,但页面像素还没有真正绘制到屏幕上。 典型流程是: HTML ↓ DOM 树 + CSS ↓ CSSOM 树 ↓ 渲染树 Render Tree ↓ 布局 Layout ↓ 绘制 Paint ↓ 合成 Composite ↓ 显示到屏幕 渲染树主要由 DOM 树和 CSSOM 树合并而成,并且只包含页面中需要渲染的可见节点【W2】。例如: <div style="display: none">不会进入渲染树</div> <p>会进入渲染树</p> 构建完渲染树之后,浏览器还要继续做几件事: 布局 Layout:计算每个元素的具体位置和尺寸; 分层 Layering:根据 z-index、transform 等情况生成图层树; 绘制 Paint:把文字、颜色、边框、阴影等转换成绘制指令; 合成 Composite:把不同图层合成为最终图像; 提交显示:将结果交给显示系统呈现。 所以可以这样理解: DOM 树:页面有哪些节点; CSSOM 树:这些节点应该怎么呈现; 渲染树:哪些节点最终需要呈现,以及应用了什么样式; 布局:它们具体放在哪里、占多大空间; 绘制:应该画出什么; 合成:如何把各个图层叠加成最终页面。 我的笔记里也强调过,浏览器还需要从布局树继续生成图层,并通过各个图层的叠加形成最终页面【4】。因此,渲染树构建完成不是“页面已经显示”,而是“页面具备了继续布局和绘制的输入”。
2026-08-14 验证
你知道防抖吗,能实现一下吗?
知道。防抖(debounce)的核心是:事件频繁触发时不断重置计时器,直到停止触发超过指定时间,才执行最后一次回调。 它适合搜索联想、服务端表单校验,以及避免按钮连续点击重复提交;而 scroll、拖拽、resize 这类需要持续响应的高频事件,通常更适合节流【2】【3】【5】。 type Debounced<T extends (...args: any[]) => any> = { (...args: Parameters<T>): void; cancel: () => void; }; function debounce<T extends (...args: any[]) => any>( fn: T, wait = 300 ): Debounced<T> { let timer: ReturnType<typeof setTimeout> | undefined; function debounced(this: ThisParameterType<T>, ...args: Parameters<T>) { if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = undefined; fn.apply(this, args); }, wait); } debounced.cancel = () => { if (timer) clearTimeout(timer); timer = undefined; }; return debounced; } 使用示例: const search = debounce(async (keyword: string) => { const res = await fetch(/api/search?q=${encodeURIComponent(keyword)}); console.log(await res.json()); }, 300); document.querySelector<HTMLInputElement>('#search')! .addEventListener('input', (event) => { search(event.currentTarget.value); }); 如果用户连续输入,前面的定时器都会被取消;只有停下来 300ms 后,才会用最终的关键词发一次请求。 按钮提交时可以这样用: const submit = debounce(() => { console.log('提交表单'); }, 500); document.querySelector('#submit')! .addEventListener('click', submit); 不过我补充一点:防抖只能改善前端重复触发,不能代替服务端幂等控制。 对支付、创建订单等关键接口,后端仍应通过幂等键、唯一约束或状态机保证不会重复处理。
2026-08-14 验证
节流如何实现
节流(throttle)是指:无论事件多高频触发,在固定时间窗口内最多执行一次。它适合 scroll、resize、拖拽和动画这类需要持续响应的场景【1】【2】【6】【8】。 下面是一个支持首次立即执行(leading)和结束后补一次执行(trailing)的 TypeScript 实现: type Throttled<T extends (...args: any[]) => void> = { (...args: Parameters<T>): void; cancel: () => void; }; function throttle<T extends (...args: any[]) => void>( fn: T, wait = 300, options = { leading: true, trailing: true } ): Throttled<T> { let timer: ReturnType<typeof setTimeout> | undefined; let lastTime = 0; let lastArgs: Parameters<T> | undefined; let lastThis: ThisParameterType<T> | undefined; function invoke() { lastTime = Date.now(); timer = undefined; if (lastArgs) { fn.apply(lastThis, lastArgs); lastArgs = undefined; lastThis = undefined; } } function throttled(this: ThisParameterType<T>, ...args: Parameters<T>) { const now = Date.now(); // 不立即执行时,首次也进入等待 if (!lastTime && !options.leading) { lastTime = now; } const remaining = wait - (now - lastTime); lastArgs = args; lastThis = this; // 已经过了一个时间窗口,立即执行 if (remaining <= 0 || remaining > wait) { if (timer) { clearTimeout(timer); timer = undefined; } invoke(); return; } // 窗口内触发过多:结束时只补执行最后一次 if (!timer && options.trailing) { timer = setTimeout(invoke, remaining); } } throttled.cancel = () => { if (timer) clearTimeout(timer); timer = undefined; lastTime = 0; lastArgs = undefined; lastThis = undefined; }; return throttled; } scroll 示例: const handleScroll = throttle(() => { console.log('当前滚动位置:', window.scrollY); }, 200); window.addEventListener('scroll', handleScroll); 默认行为是: 第一次触发:立即执行; 接下来 200ms 内:不重复执行; 如果期间持续触发:窗口结束后,使用最后一次参数再执行一次。 组件卸载或事件解绑时,记得取消尾部定时器: window.removeEventListener('scroll', handleScroll); handleScroll.cancel();
2026-08-14 验证
我现在遇到一个问题,我们因为公司项目被裁员,我们公司在谈赔偿的时候,为了避免我们追回公积金和社保,给我们把赔偿金强制进行拆分为 3 份,并签署相应协议,如果还有追缴公积金和社保回追回补偿。 第一份补偿9000,第二份公积金补偿30000,第三方社保补偿10000。如果在谈妥之前我已经提交追缴公积金资料到公积金中心,后面追缴和公司签署的这些协议是否有效,是否还会被公司追回补偿。
先说结论:公司不能通过离职协议免除补缴社保、公积金的法定义务,但你收到的 3 万元“公积金补偿”和 1 万元“社保补偿”存在被要求返还的现实风险;9000 元法定经济补偿通常不能仅因你追缴社保、公积金就被追回。 以下按中国大陆劳动关系的一般规则分析,最终结果会受到协议原文、所在城市政策和款项性质影响。 追缴申请不会因你后来签协议自动失效 社保、公积金缴存属于用人单位的法定义务,不是公司和员工可以自由协商放弃的普通债务。 至少在公积金问题上,已有明确案例认为:即使员工签署协议放弃追缴,公司已经支付所谓“公积金补偿”,也不能免除单位依法缴存的义务。公积金中心仍然可以责令公司补缴【W3】。 所以,即使你之后签了: 不再投诉、追缴; 已由现金补偿替代社保、公积金; 追缴后返还补偿; 也不代表公积金中心或社保经办机构必须停止处理。尤其是你的材料在签约前已经提交,受理记录、提交日期和公司是否知情都很重要。 但“补缴有效”不等于“四万元一定不用退” 这里要分开看。 9000 元经济补偿 如果这 9000 元本质上是解除劳动合同的法定经济补偿,那么公司一般不能因为你依法追缴社保、公积金就要求返还。 项目取消后解除劳动合同,可能涉及协商解除、客观情况发生重大变化或者经济性裁员,不同解除路径对应的程序和补偿并不完全一样【W4】。因此,还要核对: 公司解除劳动合同的具体理由; 你的工作年限和离职前十二个月平均工资; 公司有没有提前三十日书面通知; 是协商解除、经济性裁员,还是公司直接解除; 9000 元是否达到法定的 N、N+1,或者是否可能构成违法解除的 2N。 如果协议把本来就该支付的法定补偿包装成“只要追缴就全部退回”,这种条款的效力存在很大问题。 30000 元公积金补偿、10000 元社保补偿 这两笔风险明显更高。 公司可能主张: 支付这四万元的目的,是补偿未缴社保、公积金造成的损失;现在公司又被行政机关责令补缴,你同时取得现金和账户补缴,构成重复受偿,因此应按协议返还。 这个主张是否成立,并没有脱离协议就能确定的统一答案,主要取决于: 协议是否明确约定了返还条件; 四万元是额外离职补偿,还是明确替代社保、公积金; 四万元如何计算,有没有对应具体欠缴情形; 协议是否属于公司强制提供的格式条款; 公司是否用这四万元交换你放弃法定投诉、追缴权; 返还范围是四万元,还是连 9000 元都要求返还; 补缴情形下,你是否实际获得了重复利益; 当地法院、仲裁机构对这种条款的具体认定。 我的判断是: 要求你放弃追缴,从而免除公司法定缴存义务的部分,大概率不能对抗主管机关。 约定追缴后返还对应的 3 万元、1 万元,存在被公司起诉主张返还的风险。 约定只要追缴就返还包括 9000 元在内的全部补偿,未必会被全额支持。法院可能区分法定经济补偿和额外和解款,而不是把整份协议简单地全部认定有效或无效。 公积金案例中,即使离职协议已经约定员工放弃公积金权利、公司也支付了补偿,法院仍认定缴存义务不能因此免除【W3】。但该结论主要解决的是“公司是否还要补缴”,不当然等于员工可以同时保留全部现金补偿。 你在签约前已经提交追缴材料,这一点很关键 这不会自动让后面的协议无效,但会影响双方责任判断。 重点检查协议里有没有以下表述: “乙方确认未向任何部门投诉、举报或申请追缴”; “乙方不存在任何尚未披露的争议”; “乙方承诺撤回已经提出的投诉”; “如已经投诉但未告知公司,应返还全部补偿”; “任何追缴结果均视为重复受偿”。 如果你签署时已经申请追缴,却在协议里确认“从未申请”,公司可能据此主张你隐瞒事实或者违反协议。 反过来,如果: 你谈判时已经明确告诉公司; 公司知道你已经提交材料; 微信、邮件、录音里讨论过这件事; 协议是在公司明知的情况下签署; 那么公司以后再说你故意隐瞒,依据就会弱很多。 现在建议你立即做这几件事 第一,固定追缴时间证据 保存: 公积金中心、社保部门的收件回执; 网上提交记录和截图; 短信、受理编号; 邮寄凭证; 材料清单; 与工作人员沟通的记录。 核心是证明:你在签协议之前已经提交,公司是否知道。 第二,完整保存谈判证据 包括: 三份协议的完整版本; 银行流水和付款备注; 公司补偿方案; 微信、钉钉、飞书、邮件; 合法取得的谈判录音; 公司要求拆分补偿的说明; 其他员工收到的统一方案。 尤其要保留能够证明“公司拆分补偿是为了阻止追缴”的内容。 第三,不要把三笔钱混在一起计算 单独列一张表: | 款项 | 金额 | 协议名称 | 协议认定的性质 | 实际计算方式 | |---|---:|---|---|---| | 经济补偿 | 9000 | 解除协议 | 法定补偿/额外补偿 | 是否等于 N、N+1 | | 公积金补偿 | 30000 | 补偿协议 | 替代缴存/额外和解款 | 对应哪些月份 | | 社保补偿 | 10000 | 补偿协议 | 替代缴存/额外和解款 | 对应哪些月份 | 如果 9000 元低于依法应得的经济补偿,还可能存在补差问题。 第四,把协议交给主管部门看 向公积金中心、社保部门如实说明: 申请在签约前已经提交; 公司后来要求签署了哪些协议; 是否要求撤回; 是否约定追缴后返还款项。 不要只口头描述,尽量提交协议复印件并要求留档。主管部门负责处理补缴问题,但一般不会替你最终裁决现金补偿是否返还;返还争议可能需要劳动仲裁或法院处理。 第五,在四万元返还风险明确前,不要处分这笔钱 公司可能发律师函或直接起诉。暂时单独留存这四万元,避免最后需要返还时陷入被动。 同时,不要自行签署新的: 退款确认书; 借款协议; 自愿放弃声明; 欠款确认书; 撤回投诉并承担违约金的补充协议。 对你最现实的结果判断 我倾向于认为可能出现下面三种结果之一: 主管部门责令补缴,9000 元保留,四万元按协议或重复受偿规则部分、全部返还。 主管部门责令补缴,返还条款因性质、格式条款或限制法定权利等原因未被完全支持,只返还部分。 补缴继续进行,同时公司另行起诉返还四万元,最终由法院区分每一笔款项的性质。 最不应当默认的是:“协议无效,所以五万元全部可以保留。” 同样也不应当默认:“公司写了返还条款,所以你追缴后必须无条件退回全部五万元。” 关键不是协议标题,而是每笔钱究竟属于法定经济补偿、额外和解款,还是对未缴社保、公积金的替代性付款。 涉及约 5 万元以及长期社保、公积金权益,建议带上三份协议、解除通知、工资流水、工作年限证明和追缴回执,找当地劳动法律师做一次逐条审查。不要只咨询公司 HR,也不要只凭口头答复决定是否撤回追缴。
2026-08-14 验证
const search = debounce(async (keyword: string) => { const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`); console.log(await res.json()); }, 300); document.querySelector<HTMLInputElement>('#search')! .addEventListener('input', (event) => { search(event.currentTarget.value); }); 这么去使用防抖是不是每一次的修改都会新建search(event.currentTarget.value); 方法,然后在 debounce 内容不就相当于重新创建 settimeout 了。
不会。这里要区分创建函数和调用函数。 search(event.currentTarget.value); 只是调用已经创建好的 search 函数,并传入新的参数,不会重新创建 search。search 只在下面这行执行 debounce(...) 时创建一次: const search = debounce(async (keyword: string) => { // ... }, 300); debounce 内部通过闭包保存同一个 timer: function debounce<T extends (...args: any[]) => void>(fn: T, wait: number) { let timer: ReturnType<typeof setTimeout> | undefined; return function (...args: Parameters<T>) { if (timer) clearTimeout(timer); // 取消上一次定时任务 timer = setTimeout(() => { // 创建本次定时任务 fn(...args); }, wait); }; } 调用过程大致是: 输入 a → 创建定时器 1 100ms 后输入 ab → 清除定时器 1 → 创建定时器 2 100ms 后输入 abc → 清除定时器 2 → 创建定时器 3 之后 300ms 没有输入 → 定时器 3 执行 → 只请求 abc 所以你的理解有一半是对的: 每次输入不会创建新的 search 函数; 每次调用确实会清除旧 timer,再创建一个新的 timer; 这正是防抖的核心:不断重新计时,最终只执行最后一次回调【1】【2】。 可以验证 search 始终是同一个函数: const first = search; search('a'); search('ab'); search('abc'); console.log(first === search); // true 真正错误的是把 debounce 放进事件回调里: input.addEventListener('input', (event) => { // 错误:每次 input 都创建一个新的防抖函数, // 每个函数都有自己独立的 timer,旧 timer 无法被清除 const search = debounce(request, 300); search(event.currentTarget.value); }); 这样每次输入都会产生新的闭包和独立的 timer,最终可能每个请求都执行,防抖就失效了。 正确方式就是你原来的写法:先创建一次防抖函数,再反复调用它。 const search = debounce(request, 300); input.addEventListener('input', (event) => { search(event.currentTarget.value); }); 如果是在 React 函数组件中,还要避免每次 render 都重新创建防抖函数,通常用 useMemo 或 useRef 保持实例稳定: const search = useMemo( () => debounce(async (keyword: string) => { const res = await fetch( /api/search?q=${encodeURIComponent(keyword)} ); console.log(await res.json()); }, 300), [] ); useEffect(() => { return () => search.cancel(); }, [search]); 核心判断标准只有一个:debounce(...) 应该执行一次,返回的防抖函数可以调用很多次。
2026-08-14 验证
说一下你对闭包的理解 你知道函数执行栈吧,你如果闭包在里面会释放内存吗举了几个例子
先说结论:闭包不会让整个函数执行栈一直留在内存里。外层函数执行完后,执行上下文仍然会出栈;只有被内部函数引用、并且仍然可达的变量,才可能作为词法环境的一部分继续保留。等闭包本身不可达后,这些变量才能被垃圾回收。【1】【W4】 什么是闭包 我对闭包的理解是: 函数和它创建时所在的词法环境(Lexical Environment)的组合。 简单说,内部函数即使离开了外部函数,仍然可以访问外部函数作用域里的变量,这就是闭包体现出来的能力。 function createCounter() { let count = 0 return function counter() { count++ return count } } const counter = createCounter() console.log(counter()) // 1 console.log(counter()) // 2 createCounter() 已经执行结束了,但 counter() 仍然能访问 count。 原因不是 createCounter 的调用栈没有释放,而是返回的 counter 仍然引用着对应的词法环境,所以 count 仍然可达,暂时不会被垃圾回收【W4】。 可以简化为: counter ↓ 内部函数 ↓ 词法环境 ↓ count = 2 外层函数执行完,执行栈会不会释放 会释放。 调用函数时,会创建执行上下文并压入调用栈。函数执行结束后,对应的执行上下文会从调用栈中弹出。 function outer() { const value = 100 function inner() { return value } return inner } const fn = outer() 调用过程可以简化为: 调用 outer() 调用栈: ┌──────────────┐ │ outer context│ ├──────────────┤ │ global │ └──────────────┘ outer() 返回之后: 调用栈: ┌──────────────┐ │ global │ └──────────────┘ outer 的执行上下文已经出栈,但 fn 还引用着 inner,而 inner 又引用了 value 所在的词法环境。因此需要保留的变量会继续存在。 这里要避免一个常见的错误说法: 因为形成闭包,所以外层函数的调用栈没有释放。 这不准确。释放的是调用栈上的执行上下文;被闭包捕获的数据会通过引擎内部的 Context、Environment 等结构继续存活。从教学模型看,可以把它理解为相关变量被保存到堆中【1】【2】。具体如何存储属于 JavaScript 引擎实现细节,不应该机械地理解成“所有局部变量永远都在栈”或“整个作用域都复制到了堆”。 程序运行同时需要栈和堆,不可能把所有数据都直接放进栈中【3】【4】。 哪些内存会被保留 通常只有闭包实际需要访问的变量有必要被保留。不过具体引擎可能因为优化策略,把一部分关联环境一起保留,不能只凭源码断言精确的底层内存布局。 function createGetter() { const used = { name: 'batype' } const unused = new Array(100000).fill('data') return function getName() { return used.name } } const getName = createGetter() 从语义上看: used 被内部函数引用,需要保留; unused 没有被引用,之后可以被回收; createGetter 的执行上下文会正常出栈。 但在分析真实内存时,仍然要以浏览器 DevTools 的 Heap Snapshot 为准,因为引擎优化会影响实际表现。 闭包什么时候释放 核心判断只有一个:闭包及其关联的词法环境是否仍然可达。 function createCounter() { let count = 0 return () => ++count } let counter = createCounter() console.log(counter()) // 1 counter = null 执行: counter = null 之后,如果没有其他地方引用返回的函数,那么: 返回的函数 → 不可达 词法环境 → 不可达 count → 可以被垃圾回收 注意是“可以被回收”,不是“这一行执行完立刻回收”。垃圾回收的具体时间由 JavaScript 引擎决定。词法环境只有在不可达时才会被清理【W4】。 几个典型例子 例一:闭包一直被引用,变量不会释放 function createCache() { const cache = new Array(1000000).fill('data') return function getItem(index) { return cache[index] } } const getItem = createCache() 只要 getItem 一直可达,它引用的 cache 就必须保留。 console.log(getItem(0)) 这不一定是内存泄漏,因为程序可能确实需要这个缓存。内存占用和内存泄漏不是一回事。 如果后续不再需要,应主动断开引用: let getItem = createCache() console.log(getItem(0)) getItem = null 例二:没有逃逸的闭包,执行后可以回收 function run() { const message = 'hello' function print() { console.log(message) } print() } run() print 没有被返回,也没有注册到其他长期存活的对象中。 run() 执行结束后,如果没有其他引用: run 的执行上下文出栈; print 不再可达; message 所在的环境可以被垃圾回收。 所以,出现嵌套函数不等于一定会长期占用内存。 例三:每次调用都会形成独立环境 function createCounter() { let count = 0 return () => ++count } const counterA = createCounter() const counterB = createCounter() console.log(counterA()) // 1 console.log(counterA()) // 2 console.log(counterB()) // 1 console.log(counterB()) // 2 两次调用 createCounter(),会产生两套独立的词法环境: counterA → countA counterB → countB 因此两个计数器互不影响。多次调用外部函数并保存返回函数时,每次调用对应的词法环境都可能被保留【W4】。 例四:多个闭包共享同一个环境 function createCounter() { let count = 0 return { increment() { count++ }, decrement() { count-- }, getValue() { return count } } } const counter = createCounter() counter.increment() counter.increment() counter.decrement() console.log(counter.getValue()) // 1 increment、decrement 和 getValue 都是在同一次 createCounter() 调用中创建的,因此共享同一个 count。 这也是闭包实现私有状态的一种常见方式。 例五:事件监听器导致闭包长期存活 function mount() { const largeData = new Array(1000000).fill('data') function handleClick() { console.log(largeData.length) } window.addEventListener('click', handleClick) return function unmount() { window.removeEventListener('click', handleClick) } } const unmount = mount() 引用关系是: window ↓ handleClick ↓ largeData 即使 mount() 已经结束,window 仍然引用着 handleClick,因此 largeData 不能释放。 清理监听器: unmount() 移除监听后,如果没有其他引用,handleClick 和 largeData 才可以被回收。 React 中的副作用清理,本质上也经常是在解决这类生命周期问题: useEffect(() => { const handleResize = () => { console.log(window.innerWidth) } window.addEventListener('resize', handleResize) return () => { window.removeEventListener('resize', handleResize) } }, []) 例六:定时器保留闭包 function startTimer() { const data = new Array(1000000).fill('data') const timerId = setInterval(() => { console.log(data.length) }, 1000) return () => { clearInterval(timerId) } } const stopTimer = startTimer() 只要定时器还在运行,回调就仍然可达,回调引用的 data 也会继续存在。 stopTimer() 取消定时器后,如果没有其他引用,相关内存才有机会被回收。 闭包和异步回调的关系 异步回调经常会形成闭包,但闭包不等于异步。 function request() { const message = 'request finished' setTimeout(() => { console.log(message) }, 1000) } request() request() 很快执行结束并出栈,但定时器系统仍然持有回调函数。回调函数引用了 message,所以 message 在回调执行前不能被回收。 当定时器到期,回调会作为任务进入队列,等调用栈空闲后再执行。异步回调可以被安排为任务,也可以进入微任务队列;微任务会在当前任务末尾执行【7】。 var 循环中的经典闭包问题 const callbacks = [] for (var i = 0; i < 3; i++) { callbacks.push(function () { console.log(i) }) } callbacks0 // 3 callbacks1 // 3 callbacks2 // 3 三个函数共享同一个 i。等函数真正执行时,循环已经结束,i 的值是 3。 使用 let: const callbacks = [] for (let i = 0; i < 3; i++) { callbacks.push(function () { console.log(i) }) } callbacks0 // 0 callbacks1 // 1 callbacks2 // 2 let 在 for 循环中会为每次迭代创建对应的词法绑定,因此三个闭包访问的是不同的 i。 闭包一定会造成内存泄漏吗 不会。 闭包只是保留词法环境的一种语言机制。只有当程序已经不需要某些数据,但仍存在意外引用导致数据长期可达时,才属于内存泄漏。 常见风险包括: 未移除的事件监听器; 未取消的定时器; 长期存活的全局回调; 无上限增长的缓存; 已卸载组件仍被回调引用; DOM 节点被闭包或其他对象继续引用。 因此判断闭包是否有问题,不应该问: 它有没有占内存? 而应该问: 这段数据已经不用了,为什么它仍然可达? 最后可以记住这三句话: 外层函数执行结束,执行上下文正常出栈。 被仍然可达的闭包引用的词法环境会继续存在。 闭包不可达后,相关环境才可以被垃圾回收,但回收时间由引擎决定。
2026-08-14 验证
你在《什么是 JavaScript 的调用栈》里写道:「如何利用浏览器查看调用栈的信息」—— 这是什么意思?
意思很简单:当代码执行到某个函数时,你可以借助浏览器开发者工具,直接看到“这个函数是被谁调用的、调用链长什么样”。【3】 具体做法是: 打开浏览器开发者工具; 进入 Sources 面板; 在你想观察的那一行代码上打断点; 刷新页面或重新触发代码执行; 当执行暂停时,看右侧的 Call Stack 区域。 这个区域会按栈的结构展示当前调用关系。比如我文里的例子里,执行到 add 时,调用链会显示: 底部是全局入口 anonymous 中间是 addAll 顶部是当前正在执行的 add【3】 所以它的核心价值不是“看某个函数本身”,而是看函数之间的调用关系。这在分析复杂代码或者排查 Bug 时很有用。【3】 除了断点,你也可以直接在函数里写: function add(b, c) { console.trace() return b + c } 这样控制台会输出当前的调用链,适合快速排查。【3】 如果你愿意,我可以顺手拿一段更小的示例代码,给你画一下调用栈是怎么入栈、出栈的。
2026-07-09 验证