JAVASCRIPT与TYPESCRIPT · KNOWLEDGE CHAPTER
K08

作用域、函数与生命周期

函数需要同时追踪三种关系:名字通过词法环境找到哪个绑定,this 通过调用形式或词法外层取得哪个接收者,以及谁还持有这个函数使相关对象继续存活。闭包、回调和泄漏不是三个独立口诀,而是一张可以沿引用和时间顺序解释的关系图。

JavaScript与TypeScript2 道章节练习5 道主练关联

核心模型

函数需要同时追踪三种关系:名字通过词法环境找到哪个绑定,this 通过调用形式或词法外层取得哪个接收者,以及谁还持有这个函数使相关对象继续存活。闭包、回调和泄漏不是三个独立口诀,而是一张可以沿引用和时间顺序解释的关系图。

原理与机制

闭包保留可访问的绑定,不自动拍快照

函数创建时关联词法环境,之后调用会读取绑定当前值。for(var i...) 中多个回调共享函数作用域的 i,结束后都可能看到最终值;for(let i...) 为迭代建立相应绑定,各回调可保留各轮值。必须用 var 时,每轮调用工厂或 IIFE,通过独立参数绑定保存编号。如果参数是同一个对象,各次复制的是引用,仍会看到后续对象修改;需要快照时必须明确复制范围,而不是把创建闭包当成克隆。

作用域存在与绑定初始化是两回事

let、const、class 的绑定在作用域进入后就影响名字解析,但初始化前处于暂时性死区。typeof 未声明名字返回字符串 undefined,是专门规则;typeof 内层未初始化 let 仍抛 ReferenceError,外层同名值也被遮蔽。var 常先初始化为 undefined,提升不意味着引擎物理移动代码。默认参数从左向右初始化,f(a=b,b=1) 在 a 使用默认值时会读取尚未初始化的 b;传 null 不触发默认值,传 undefined 会。switch 的 case 默认共享块,可用独立块限定声明。

this 取决于调用契约

普通函数通过 obj.method() 调用时接收者是 obj;取出成 f() 后严格模式下 this 通常为 undefined。call/apply 显式给接收者,bind 创建固定接收者的新函数。箭头函数没有自身 this,只沿词法外层读取,不能再被 call/bind 改写,也不能 new。对象字面量不是独立 this 作用域,所以里面的箭头函数不会自动绑定对象。绑定原方法与包装 ()=>obj.method() 还不同:前者保留原函数,后者每次重新查当前方法。

函数身份连接订阅与清理

每次 bind 都产生新函数,注册和移除分别 bind 会不匹配。可构造时绑定一次保存,或用实例箭头字段;原型方法可共享,但两种绑定方式通常都增加每实例函数。removeEventListener 关键匹配事件类型、同一函数引用与 capture,不要求 passive/once 完全复写;capture 两种注册要分别移除。支持 signal 的监听可用同一 AbortController 集中终止,但必须实际 abort,不能只创建控制器。

内存回收看可达性,资源释放看责任

长期事件源、定时器或全局 Map 若持有回调,回调就可能继续保留编辑器实例及大数组。不可达循环可以回收,无环但一直可达的缓存也会无限增长。用可重复打开关闭场景,比较堆快照、保留路径与 detached DOM,排除正常缓存、延迟回收和工具自身引用。生命周期结束时幂等 dispose,退订、清计时器、关闭连接或 worker;WeakMap 不延长键寿命,但不能替代及时清理,FinalizationRegistry 的调用时机甚至是否发生都不可靠。

最小示例

javascript
javascript
"use strict";
const fs = [];
for (let i = 0; i < 3; i++) fs.push(() => i);
console.log(fs.map(f => f()));
const obj = { n: 1, read() { return this.n; } };
const bound = obj.read.bind(obj);
const wrapped = () => obj.read();
obj.read = () => 2;
console.log(bound(), wrapped());
console.log(obj.read.bind(obj) === obj.read.bind(obj));

预期输出 [0,1,2]、1 2、false。把循环 let 改为 var 后输出会变成 [3,3,3];bound 保留旧方法和接收者,wrapped 查到新方法;最后两次 bind 身份不同,即使目标相同也不能互相作为监听清理引用。此片段无 DOM,事件清理仍需浏览器例程验证。

环境:现代 Node.js;可保存为 .cjs 后执行。

核验:未执行教学示例;说明中的结果为机制推演,不是本轮测试通过记录。

边界与取舍

边界

  • const 禁止重新绑定,不会深冻结对象;把局部变量设 null 也不清除其他持有者。
  • 闭包不是泄漏证据,循环引用也不是;需要证明不再使用的数据仍由长寿命根保留。
  • 箭头函数解决词法 this,不自动处理监听退出、实例数量或资源生命周期。

取舍

  • 实例箭头字段传回调方便,但每实例有独立函数;原型方法更便于共享与继承,需明确绑定策略。
  • 缓存和闭包可减少重复计算,也会延长数据存活;容量、失效和 dispose 应成为接口契约。

口述示范

我解释闭包时先画绑定与调用时间,解释 this 时看实际调用式,解释泄漏时沿根对象找保留路径。回调需要稳定身份和对称清理,不能靠再 bind 一次、设 null 或等待垃圾回收释放必须及时关闭的资源。

章节练习

练习 1

编辑器退出后 window 上的监听仍触发,注册与清理都写了 bind,如何修?

展开检查点
  • 保存一次绑定的引用,匹配 capture,或在销毁时 abort 监听信号。
  • 重复打开关闭检查监听数量与保留路径稳定,dispose 可重复调用。
练习 2

解释循环回调和 typeof 的两个意外结果,并改出可预测版本。

展开检查点
  • var 共享绑定;let 每轮绑定或工厂参数隔离,对象引用仍可能共享。
  • 未声明与 TDZ 不同,默认参数顺序也可产生 TDZ。

参考来源与核验边界

核验日期 2026-10-04。严格模式是 this 示例前提;垃圾回收与 finalizer 不承诺即时执行。DOM 监听的 signal 选项需依目标环境确认。 本章新增示例未在本轮执行,不新增测试通过声明。

内容版本:2026-10-04.2 · 题目和来源 ID 保持原样,本站不将面经标签解释为企业官方出题或高频保证。