# 混淆后的代码保持哪些不变（语义契约）

AfterPack 的输出保留了你的程序的哪些行为，每个引擎版本如何用真实代码检验，以及简短的有意差异清单。

Source: https://www.afterpack.dev/zh/docs/semantic-contract

AfterPack 改变的是代码读起来的样子，保留的是代码做的事。混淆后的构建与你交给它的构建返回相同的值，抛出相同类型的错误，写出相同的输出，对页面做出相同的改动。本页说明引擎 0.2.3 及以后版本的这份契约：哪些保持不变，每个版本如何检验，以及输出有意不同的少数几处。

## 哪些保持不变

在每一个[预设](https://www.afterpack.dev/zh/docs/presets)下、使用任何[种子](https://www.afterpack.dev/zh/docs/config#seed)，混淆后的构建都会保持：

- **结果与错误。** 函数返回相同的值。原来会抛错的代码仍然抛出同一类型的错误。
- **副作用及其顺序。** 控制台输出、DOM 改动、网络请求和存储写入以相同的顺序、相同的值发生。
- **异步顺序。** `async` 函数、Promise 和定时器按相同的顺序完成。
- **全局接口。** 经典脚本定义与之前相同的全局变量，因此页面上的其他脚本仍然可以调用它的函数、读取它的变量。ES 模块不会向 `window` 添加任何东西，也不会覆盖任何全局变量。含有 `import` 或 `export` 的文件按模块读取；对于不含它们的文件，由框架插件、CLI 或 [`sourceType`](https://www.afterpack.dev/zh/docs/config#sourceType) 告诉引擎。
- **你的代码所依赖的语言规则。** 严格模式下的错误、`this` 与调用接收者、getter 与 setter、类字段、`BigInt` 运算，以及 `eval` 和 `with` 的作用域，都按原样运行。
- **你的代码读取的名称。** 在读取 `this.constructor.name` 或 `new.target.name` 的文件中（自定义错误类就会这样做），类和构造函数保留你写下的名称，例外情况见[有意差异](#deliberate-differences)。

使用不同种子的两次构建形态不同，但行为永远相同。

## 哪些会按设计改变

- **代码的文本。** `fn.toString()` 的返回值、行号和列号，以及调用栈中的名称都会改变。要读取生产环境的调用栈，请私下保留一份 [source map](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled)。
- **体积与速度。** 输出更大，从 `medium` 起每个操作要做更多工作。实测数据见[性能](https://www.afterpack.dev/zh/docs/performance)。
- **检查自身的代码。** 当引擎发现代码在运行时读取自身结构，例如 `fn.toString()`，它会停止构建并指出该模式。用 [`reflection.allow`](https://www.afterpack.dev/zh/docs/config#reflection-allow) 确认该模式，或用 [`paths.exclude`](https://www.afterpack.dev/zh/docs/config#paths-exclude) 让该文件原样交付。

## 每个版本如何检验

从引擎 0.2.3 起，每个引擎版本在发布前都要通过一道语义关卡。它把每个程序运行两次，一次按原样，一次经过混淆，然后比较两次运行做了什么：返回值、抛出的错误、控制台输出，以及异步工作的顺序。任何差异都会让该版本无法发布，除非它属于下面列出的[有意差异](#deliberate-differences)。

这道关卡运行：

- **小而有针对性的程序**，每个都瞄准一条代码变换工具可能弄错的语言规则：类与私有字段、闭包与作用域、生成器、`async` 顺序、模板字面量、`BigInt`、`eval` 与 `with`、严格模式、标签与 `switch`。覆盖每一个预设、大量种子，以及原生引擎和 WebAssembly 引擎。
- **真实的库**，固定版本，各由一个小脚本驱动：处理日期的 dayjs、Moment.js 和 Luxon，大量使用 `BigInt` 的密码学库 viem、ethers 和 @noble/curves，以及 Comlink、lodash 的 `_.template` 和 get-intrinsic。
- **Chromium 中的页面**，按顺序加载多个经典脚本、带或不带 import 的 ES 模块、内联事件处理器和自定义元素，以及一个单 chunk 的 Vue 应用。每个页面都必须表现相同，并且让 `window` 与原始版本完全一致：没有新增的全局变量，也没有被覆盖的全局变量。

## 有意差异

这些差异，加上表格后面的类名差异，是这道关卡唯一允许的差异。表中每一行都涉及替换内置对象、把实例包进 `Proxy`、不用 `new` 调用类，或读取 JavaScript 引擎错误消息的代码，因此大多数应用永远不会遇到。它们只出现在引擎重写的类中，从 `light` 起生效；`minify` 会让类保持原样。

| 你的代码 | 原生行为 | 混淆后的输出 |
|---|---|---|
| 在一个本身是 `Proxy`（由基类构造函数返回）或原型链中有 `Proxy` 的实例上定义公有类字段 | 只会触发代理的 `defineProperty` 陷阱 | 每个字段还会触发 `has` 陷阱；对于对象上不存在的字段，还会触发 `set` 陷阱及其转发到的一切 |
| 不用 `new` 调用类的构造函数 | 在求值任何参数默认值或解构之前就抛出 `TypeError` | 先求值参数默认值和解构，然后抛出同样的 `TypeError` |
| 在替换了 `Function.prototype.call` 之后，或在带有自有 `call` 属性的方法上，调用私有方法或 `super.method()` | 直接调用该方法 | 经由被替换的 `call` 调用 |
| 读取由私有字段检查、私有字段重复初始化或不用 `new` 调用类所产生的 `TypeError` 的消息 | JavaScript 引擎自己的消息 | AfterPack 自己的消息，错误类型相同 |
| 在文件加载之前替换 `TypeError`、`Object`、`Object.defineProperty`、`Reflect.construct`、`Reflect.ownKeys`、`WeakMap` 或 `WeakSet` | 类仍然使用原始的内置对象 | 被重写的类使用替换后的版本 |

类名和构造函数名在少数情况下会不同，在每个预设下都是如此。只以其他方式读取名称的文件，例如 `err.constructor.name` 或 `const { name } = new.target`，得到的是改名后的名称。在确实读取 `this.constructor.name` 或 `new.target.name` 的文件中，以下情况仍会显示改名后的名称：以 `new (class X {})()` 构造的类、由默认参数命名的匿名类、在声明它的 `switch` case 之前的某个 case 中被构造的函数，以及只通过其他引用构造的函数，例如 `new ns.F()`、别名或 `Reflect.construct`。

## 哪些保持你写的样子

只要无法低成本地忠实重写，以下结构就保持原样。它们周围的代码仍会被重命名和编码，`with` 和直接 `eval` 周围除外。

- `async` 函数和 `await`。
- `delete`，以及作用于单个名称的 `typeof`。
- `catch (e)` 块中声明了 `var e` 的函数。
- 直接在 `switch` 分支中声明的 `let` 或 `const`。
- 被调用者带括号或是 `super` 成员的可选调用或标签：`(o?.m)?.()`、`` (o?.m)`x` ``、`super.m?.()`。
- 给自身名称赋值的类、以可选调用、标签或带括号的被调用者形式调用 `super` 成员的类，或派生构造函数中顶层 `super()` 调用不恰好为一次的类。
- `minify` 和 `light` 下的循环。
- `with` 语句或直接 `eval` 周围的作用域。从它向外直到文件顶层的每一层作用域中，名称都保留原来的写法，`with` 语句体内的全局变量也保持原样。构建会用 [`DIAG_DYNAMIC_SCOPE_NATIVE`](https://www.afterpack.dev/zh/docs/diagnostics#complete-reference) 报告这一点。如果被求值的代码不需要局部变量，就用间接方式调用 `eval`，即 `(0, eval)(code)`，或者把需要 `eval` 或 `with` 的代码移到单独的文件中。

## 引擎 0.2.3 之前

引擎 0.2.2 及更早版本在关卡现在覆盖的一些情况下没有满足这份契约：与其他脚本共享的经典脚本顶层名称、不含 import 的 ES 模块、模板字面量中的日期等对象、被当作构造函数使用的函数、覆盖继承访问器的类字段、引用自身的函数、`medium` 及以上的 `BigInt` 运算、`eval` 和 `with`、用 `var` 重新声明自身变量的 `catch` 块、`async` 顺序、带括号的可选调用、常量 `**` 表达式，以及在运行时读取的类名。请用引擎 0.2.3 或更高版本重新构建；[更新日志](https://www.afterpack.dev/zh/changelog)列出了每一项修复。

## 报告差异

本页未列出的差异就是一个 bug。请在 [github.com/afterpack-dev/afterpack/issues](https://github.com/afterpack-dev/afterpack/issues) 提交 issue，并附上一个小的复现。在修复之前，[`paths.exclude`](https://www.afterpack.dev/zh/docs/config#paths-exclude) 可以让受影响的文件原样交付。

## 下一步

- [诊断](https://www.afterpack.dev/zh/docs/diagnostics)：构建可能报告的每个代码，以及如何处理。
- [AfterPack 的工作原理](https://www.afterpack.dev/zh/docs/concepts)：各种变换对你的代码做了什么。
- [预设](https://www.afterpack.dev/zh/docs/presets)：每个级别应用了什么。
- [配置](https://www.afterpack.dev/zh/docs/config)：[`reflection.allow`](https://www.afterpack.dev/zh/docs/config#reflection-allow)、[`paths.exclude`](https://www.afterpack.dev/zh/docs/config#paths-exclude)，以及本页链接到的其他键。
