Skip to content

第七章练习答案 ​

先完成正文中的预测,再查看答案。下面解释的是语言与运行环境行为,不要求背诵某种固定写法。

实时绑定与依赖循环 ​

第 1 题。 导入标识符连接到导出绑定,导入方不能重新赋值;如果绑定里保存的是对象引用,对象本身仍可能可变。可把对象保留在模块内部,只导出查询与操作函数。若返回对象快照,注意浅复制仍共享嵌套对象;应根据契约决定是否复制嵌套结构或冻结。

js
// counter.mjs
let count = 0;
export const read = () => count;
export const increment = () => { count += 1; };

使用方只能调用 increment 改变内部计数,不能取得内部变量后直接重写。

第 2 题。 将共同基准放入第三个模块,两边独立计算:

js
// base.mjs
export const base = 1;
js
// a.mjs
import { base } from './base.mjs';
export const a = base + 1;
js
// b.mjs
import { base } from './base.mjs';
export const b = base + 2;

以上注释分隔的是三个独立文件。依赖从 a/b 都指向 base,不再互相等待未初始化的 const。它重新定义了合理的数据关系,并不是凭空求解原本相互矛盾的 a=b+1、b=a+1。

第 3 题。 静态import参与模块结构,依赖图及绑定先于入口正文建立。普通同步无环依赖先求值,再运行入口。不存在导出名属于链接失败;导出名能连接但正文throw属于求值失败。两者都不能靠把console挪到import上面绕过。

Node 测试 ​

第 1 题。 添加 assert.throws(() => total('125', 3), RangeError)。原函数契约要求非负安全整数,字符串应拒绝;若产品需要转换,应该在输入边界明确转换,再调用计算函数,不要让隐式转换决定契约。

第 2 题。 async 函数抛错变成拒绝的 Promise,而 throws 验证同步抛出。使用 await assert.rejects(async () => total(-1, 2), RangeError),测试函数自身也声明 async,确保运行器等待断言完成。

第 3 题。 now返回1000且deadline为1000时,约定应关闭,改为<=会得到true,这条边界断言失败。传入时钟函数使输入可控;真实等待会受调度和运行时机影响,也没有增加对比较边界的证明力。

加载与兼容 ​

第 1 题。 async 依据加载就绪时间执行,不保证定义工具的文件先执行。对于解析器插入的经典外部脚本可使用按文档顺序执行的 defer,或改为 ESM 显式导入依赖。只调整文件书写顺序不能约束 async 的完成顺序。

第 2 题。 polyfill 补运行时 fetch,无法让旧解析器认识可选链。整个文件可能在执行前就语法失败。需对目标环境转译语法并提供相应运行时能力,再在目标环境验证;两类兼容工作分别解决不同层次的问题。

第 3 题。 defer保持解析器插入的经典外部脚本的文档执行顺序,因此show先下载完仍等待前面的dictionary。async不保证这一顺序,就绪后可能先执行;某次缓存命中下得到正确结果不构成依赖保证。

第 4 题。 三个文件分别如下,保留已有 loading.html 的段落:

js
// dictionary.js
export const chapterLabel = '模块';
js
// show.js
import { chapterLabel } from './dictionary.js';
document.querySelector('#message').textContent = chapterLabel;
console.log(Object.hasOwn(window, 'chapterLabel')); // false(独立实验页)
html
<!doctype html>
<meta charset="utf-8">
<title>显式模块依赖</title>
<script type="module" src="./show.js"></script>
<p id="message">等待脚本</p>

入口的静态导入建立依赖,词典先于入口求值;没有 async 的模块入口按解析后的时机执行。不要保留旧经典脚本标签,否则它们仍可能写入全局对象。

用心记录每一步成长