第九章练习答案
这些题的重点是区分“现在看起来正常”和“行为有明确保证”。
错误与回归
第 1 题。 finally 的 return 形成新的完成结果,覆盖 try 中原本要传播的异常。清理可以留在 finally,但避免返回业务结果;若清理自身可能失败,还要设计是否保留原始错误,不能无意覆盖。
第 2 题。 使用 assert.throws(() => parseReading('{}'), ReadingError)。{} 是合法 JSON,因此解析完成后才在数据结构校验中失败;'{' 在 JSON 解析阶段失败,包装后的 cause 是 SyntaxError。两者提示应帮助定位不同层次的问题。
第 3 题。 错误发生在第二次+=把10与字符串5连接时,Number只会把错误字符串变成错误数字105。应在加法前检查total、row.price及typeof,找到第一个违反数字输入契约的字段,再在输入边界转换校验或拒绝。
安全与性能
第 1 题。 HTML 中不止 script 可以产生危险行为,事件属性等也需要考虑,简单正则无法完整模拟 HTML 解析与各种上下文。普通文本使用 textContent;确需富文本时采用明确允许列表和经过维护的清理方案,避免自行拼凑过滤器。
第 2 题。 一次结果受运行环境、输入规模、预热、GC 和计时精度影响,也可能没有测量等价工作。应先确认实际瓶颈,再对代表性输入进行重复测量并查看分布,结论限定在被测环境和任务内。
第 3 题。 Object.assign触发目标继承的__proto__ setter,改变的是该局部目标的原型;theme来自那条原型链,所以不是自有属性。空对象theme仍undefined说明此实验未修改全局Object.prototype,不能把目标原型变化与全局污染混为一谈。
第 4 题。 在 .mjs 中可运行以下函数与输入:
function sameOriginAddress(raw, base) {
const trusted = new URL(base);
const target = new URL(raw, trusted);
if (!['http:', 'https:'].includes(target.protocol) || target.origin !== trusted.origin) {
throw new TypeError('只允许站内 HTTP/HTTPS 地址');
}
return target.href;
}
const base = 'https://example.test/reader/';
for (const value of ['/notes', 'https://example.test/notes',
'https://example.test.evil.invalid/notes', 'javascript:alert(1)']) {
try { console.log(sameOriginAddress(value, base)); }
catch { console.log('拒绝'); }
}前两项都得到 https://example.test/notes,后两项拒绝。base 必须来自程序可信配置;若也由用户任意指定,检查便失去可信基准。此函数约束导航范围,不能证明站内每个页面内容安全,也不能替服务端授权。
内存与资源
第 1 题。 window 仍持有监听器,监听器闭包又引用节点,DOM 移除只切断树中的一条边。应在不再使用时移除监听器或 abort 对应 signal,并检查定时器、缓存等其他强引用。
第 2 题。 FinalizationRegistry 不保证及时甚至必然执行,进程或页面结束时可能没有回调机会。草稿保存必须由明确的编辑、定期保存或页面生命周期策略触发,并处理失败;finalizer 最多承担不影响正确性的辅助工作。
第 3 题。 监听移除后不再执行是EventTarget可观察契约,可用重复派发和断言验证;GC时机由实现决定,不保证在一秒内发生。先证明资源生命周期行为正确,再用堆快照研究仍存在的引用。
第 4 题。 保留 subscribe 定义,执行以下确定性断言:
const bus = new EventTarget();
const messages = [];
const old = subscribe(bus, '旧', x => messages.push(x));
const current = subscribe(bus, '新', x => messages.push(x));
function expect(text) {
messages.length = 0;
bus.dispatchEvent(new Event('refresh'));
if (messages.join(',') !== text) throw new Error('订阅清理失败');
}
expect('旧,新');
old(); old();
expect('新');
current();
expect('');登记实验中使用 registry.register(third, '第三项', otherToken);先取消原 token,再取消 otherToken,二者首次应各返回 true,第二次各返回 false。保留 third 等目标强引用直到实验结束,测试的是登记匹配与取消,不依赖垃圾回收时机。