core-js 中的 Well-formed Unicode Strings 提案实现:isWellFormed与toWellFormed深度解析
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
本篇文章以 core-js 仓库中 well-formed-unicode-strings.md 文档为骨架,系统讲解 TC39 的 Well-formed Unicode strings 提案在 core-js 中的完整落地:从String.prototype.isWellFormed与String.prototype.toWellFormed的 API 签名与入口点,到packages/core-js/modules下的逐位级源码实现、Safari 兼容补丁、QUnit 测试用例,以及各发行层级(es / stable / actual / full / virtual)的引入方式。读完本文,你将掌握如何在不支持该特性的 JavaScript 引擎上引入这两个方法,并理解其底层 UTF-16 判定原理。
提案背景:为什么需要 "Well-formed" 字符串
JavaScript 的字符串以 UTF-16 编码单元(code unit)存储。绝大多数 Unicode 字符可以映射为单个 UTF-16 编码单元,但 BMP 之外的字符(如 emoji💩,码点 U+1F4A9)需要由**一对代理项(surrogate pair)**表示:
- 高代理项(leading/high surrogate):范围
\uD800~\uDBFF - 低代理项(trailing/low surrogate):范围
\uDC00~\uDFFF
当字符串中只出现高代理项或只出现低代理项、或两者顺序颠倒时,就产生了未配对的代理项(unpaired surrogate / lone surrogate),这样的字符串被定义为 "ill-formed"(格式不佳)。这类字符串在传统处理中容易引发问题:例如encodeURI会抛出URIError,URL解析可能失败,传给 Web API 或写入存储时可能产生无效数据。
TC39 的 "Well-formed Unicode strings" 提案为此给String.prototype增加了两个零参数方法,用于检测与修复这种状态,随后该特性随 ES2024 正式标准化。在 core-js 中,它被收录为 stage 4 提案并默认随稳定入口提供。
内置签名与核心 API
原文档给出的类型签名如下:
class String { isWellFormed(): boolean; toWellFormed(): string; }String.prototype.isWellFormed(): boolean:返回当前字符串是否为 well-formed,即是否包含未配对的代理项。不修改原字符串,无参数。String.prototype.toWellFormed(): string:返回一个新的字符串,将其中所有未配对的代理项替换为 Unicode 替换字符 U+FFFD(�);若字符串本身已 well-formed,则原样返回(但始终返回新字符串对象)。
两者的行为都由ToString语义先对this做强制转换,因此1会被当作字符串'1'处理,但null、undefined会抛TypeError,Symbol上下文同样抛错(详见下文测试用例)。
入口点(Entry points)
原文档标注的入口为:
core-js/proposals/well-formed-unicode-strings在仓库中对应 packages/core-js/proposals/well-formed-unicode-strings.js,其内容十分简洁——它只是两个底层模块的聚合入口:
'use strict'; // https://github.com/tc39/proposal-is-usv-string require('../modules/esnext.string.is-well-formed'); require('../modules/esnext.string.to-well-formed');实际引入方式(在安装 core-js 包后,于应用入口处引入即可):
import 'core-js/proposals/well-formed-unicode-strings'; // 或按需只引入其中一个 import 'core-js/proposals/well-formed-unicode-strings';esnext.string.*两个模块目前仅是过渡别名(esnext.string.is-well-formed.js 内部仅转发到es.string.is-well-formed),源码注释中标注了TODO: Remove from core-js@4——这从侧面印证该提案已完成标准化,正式实现统一收敛到es命名空间。
源码级实现:isWellFormed的位运算判定
核心实现位于 packages/core-js/modules/es.string.is-well-formed.js:
var charCodeAt = uncurryThis(''.charCodeAt); $({ target: 'String', proto: true }, { isWellFormed: function isWellFormed() { var S = toString(requireObjectCoercible(this)); var length = S.length; for (var i = 0; i < length; i++) { var charCode = charCodeAt(S, i); // single UTF-16 code unit if ((charCode & 0xF800) !== 0xD800) continue; // unpaired surrogate if (charCode >= 0xDC00 || ++i >= length || (charCodeAt(S, i) & 0xFC00) !== 0xDC00) return false; } return true; } });算法的关键点:
- 快速跳过普通单元:
charCode & 0xF800 !== 0xD800利用位掩码判断当前编码单元是否落在代理项区间(0xD800~0xDFFF)。0xF800是高 5 位掩码,代理项区间的特征是0xD800到0xDFFF,其高 5 位恰好为11011(0xD800 = 1101 1000 0000 0000)。非代理项单元直接continue,这是最常见的分支,性能最优。 - 低代理项单独出现:
charCode >= 0xDC00说明当前是低代理项却"孤身"出现,立即返回false。 - 高代理项检查后继:
++i >= length说明高代理项位于字符串末尾、没有后继;(charCodeAt(S, i) & 0xFC00) !== 0xDC00用0xFC00掩码校验后继单元是否为低代理项。任一条件成立即判定 ill-formed。
该实现与规范String.prototype.isWellFormed(ECMA-262 §22.1.3.19)的判定逻辑一一对应,复杂度为 O(n),且只读遍历不产生额外分配。
源码级实现:toWellFormed与 Safari 兼容补丁
修复逻辑位于 packages/core-js/modules/es.string.to-well-formed.js,其中包含一个值得注意的引擎 bug 检测:
var $toWellFormed = ''.toWellFormed; var REPLACEMENT_CHARACTER = '\uFFFD'; // Safari bug var TO_STRING_CONVERSION_BUG = $toWellFormed && fails(function () { return call($toWellFormed, 1) !== '1'; }); $({ target: 'String', proto: true, forced: TO_STRING_CONVERSION_BUG }, { toWellFormed: function toWellFormed() { var S = toString(requireObjectCoercible(this)); if (TO_STRING_CONVERSION_BUG) return call($toWellFormed, S); // ... } });这里的fails是 core-js 内部的特性检测辅助:若宿主原生toWellFormed无法正确处理数字参数(Safari 旧版本把1错误转换而非'1'),则TO_STRING_CONVERSION_BUG为真,polyfill 会被forced强制启用,并退化为"先自行ToString、再调用原生方法"的修补路径,保证toWellFormed.call(1) === '1'。
polyfill 主路径的修复逻辑与isWellFormed同构,只是把未配对的代理项替换为\uFFFD:
var result = $Array(length); for (var i = 0; i < length; i++) { var charCode = charCodeAt(S, i); if ((charCode & 0xF800) !== 0xD800) result[i] = charAt(S, i); // 普通单元原样拷贝 else if (charCode >= 0xDC00 || i + 1 >= length || (charCodeAt(S, i + 1) & 0xFC00) !== 0xDC00) result[i] = REPLACEMENT_CHARACTER; // 未配对代理项 → U+FFFD else { // 合法代理对 result[i] = charAt(S, i); result[++i] = charAt(S, i); } } return join(result, '');实现细节上:先按长度预分配数组、逐单元填充、最后join拼接,避免字符串拼接的反复重分配;对合法代理对一次性处理两个编码单元并同步前进索引,确保不重复遍历。
行为验证:QUnit 测试用例
仓库在 tests/unit-global/es.string.is-well-formed.js 与 tests/unit-global/es.string.to-well-formed.js 中提供了完整的 QUnit 断言,unit-pure 目录下还有针对 pure 版本(不污染全局原型)的对应测试。测试覆盖了:
正常(well-formed)输入:
assert.true(isWellFormed.call('a')); assert.true(isWellFormed.call('💩')); // 合法代理对 assert.true(isWellFormed.call('a💩b')); assert.same(toWellFormed.call('a💩b'), 'a💩b'); // 已 well-formed 时原样返回ill-formed 输入(孤立/错序代理项):
assert.true(!isWellFormed.call('\uD83D')); // 孤高代理项 assert.true(!isWellFormed.call('\uDCA9')); // 孤低代理项 assert.true(!isWellFormed.call('\uDCA9\uD83D')); // 顺序颠倒 assert.true(!isWellFormed.call('a\uD83Da')); // 嵌入字符串中间 assert.same(toWellFormed.call('\uD83D'), '\uFFFD'); assert.same(toWellFormed.call('\uDCA9\uD83D'), '\uFFFD\uFFFD'); assert.same(toWellFormed.call('a\uD83Da'), 'a\uFFFDa');类型转换与边界:
// 对象通过 toString 参与强制转换 assert.true(isWellFormed.call({ toString() { return 'abc'; } })); assert.same(toWellFormed.call(1), '1'); // 数字 → 字符串 // strict 模式下 null / undefined 抛 TypeError // Symbol 上下文抛错 assert.throws(() => isWellFormed.call(Symbol('test')));测试还断言了两个方法的arity === 0、函数名正确、looksNative(在原生存在时不做无谓覆盖)以及原型属性不可枚举。
在 core-js 各层级中的分布与引入方式
该特性已随 ES2024 标准化,因此完整贯穿 core-js 的所有稳定入口层级:
- es 层:es/string/is-well-formed.js 与 es/string/to-well-formed.js,直接
require对应modules/es.string.*实现; - stable / actual / full 层:逐层转发(stable → actual → full),可直接
import 'core-js/full/string/is-well-formed';; - virtual 层(不污染原型,返回可调用函数):es/string/virtual/is-well-formed.js、full/string/virtual 等,供 core-js-pure 风格调用;
- instance 层(以实例方法形式导出):es/instance/is-well-formed.js → stable/instance → actual/instance。
此外,packages/core-js/stage/4.js 中通过require('../proposals/well-formed-unicode-strings')将本特性纳入 stage 4 聚合入口,因此直接引入core-js/stage/4也会一并包含这两个方法。
姊妹提案:Well-formedJSON.stringify
与字符串 well-formed 直接相关的还有姊妹提案 "Well-formedJSON.stringify",其文档见 well-formed-jsonstringify.md,入口为core-js/proposals/well-formed-stringify(对应 proposals/well-formed-stringify.js,仅引入 es.json.stringify)。
两者的区别在于:String.prototype.toWellFormed作用于 JS 字符串本身;而 well-formedJSON.stringify解决的是JSON.stringify输出 ill-formed JSON(旧引擎会把孤立代理项原样输出为裸字符,产生非法的 UTF-8 序列)。在 es.json.stringify.js 中可以看到对应的特性检测与修复逻辑:
// https://github.com/tc39/proposal-well-formed-stringify var ILL_FORMED_UNICODE = fails(function () { return $stringify('\uDF06\uD834') !== '"\\udf06\\ud834"' || $stringify('\uDEAD') !== '"\\udead"'; });当原生实现不合格时,polyfill 会以正则surrogates = /[\uD800-\uDFFF]/g扫描序列化结果,并通过fixIllFormedJSON把未配对的代理项转义为\udXXX形式的合法 JSON 转义序列,而合法的代理对则保持原样。两者协同,可以在旧引擎上同时保证字符串与 JSON 输出的 well-formed。
实战建议
- 何时需要 polyfill:如果你的目标运行时尚未实现 ES2024 的这两个方法(如较旧的 Safari、Node < 20 等),通过
core-js/proposals/well-formed-unicode-strings一次性补齐;如果只需其中之一,可按需引入core-js/stable/string/is-well-formed或to-well-formed更细粒度入口。 - 与现有方案的对比:此前处理孤立代理项通常依赖手写正则
/(\uD800-\uDBFF)|((?<![\uD800-\uDBFF])[\uDC00-\uDFFF])/g之类的方式,性能与可读性都不如规范化的isWellFormed/toWellFormed;core-js 的实现采用位运算单遍扫描,O(n) 复杂度且无正则回溯风险。 - 安全边界:两者都遵循
ToString语义——null/undefined会抛TypeError,Symbol会抛错,因此对不确定的输入(如 Web API 返回值)建议先做类型判断或String()包裹,这与源码中的requireObjectCoercible行为一致。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考