空白与换行:. 不匹配什么、\s 匹配什么
syntax 页里 . 和 \s 一笔带过。这页把它俩掰开看:JS 正则里 .(无 s 标志)
默认不匹配 4 个行终止符;而 \s 匹配的远不止一个空格——是 ECMAScript 的 WhiteSpace +
LineTerminator 全集,涵盖 NBSP、各种排版空格、全角空格,甚至 BOM。这些都是「看不见却真实存在」的字符,
也是一大类难以定位的 bug 的源头。
. 与四个行终止符
「行终止符」在 JS 里恰好 4 个。. 默认跳过它们;开 s(dotAll)才让 . 吃下换行;它们也都属于 \s:
U+2028(LS)/ U+2029(PS)——它们也是行终止符,. 同样不匹配。
另外 m(multiline)改的是 ^ $ 的含义,不是 .;让 . 跨行的是 s,两者别混。
\s 的完整空白清单(按宽度对比)
下面每个都被 \s 命中(✓ 实时由 /^\s$/u 测出)。绿色标尺夹着空白本体,盒子宽度 = 这个空白真实的字形宽度——能直观看出 thin / en / em / 全角的宽窄差:
U+FEFF 被 \s 命中(它在 spec 里是 ZWNBSP,算 WhiteSpace)——
所以一段开头带 BOM 的文本,\s 会把它当空白。其二 反过来,ZWSP U+200B(零宽空格)\s 不命中——
名字带 space,实则是格式字符(Cf),不在 WhiteSpace 里。U+200C(ZWNJ)/ U+200D(ZWJ)同理(见 Basic_Emoji 页)。
隐形字符扫描:把文本里的隐形字符标出来
从别处复制来的文本常混入 NBSP、全角空格、BOM——肉眼看着是普通空格,却让 ===、trim()、数字解析全失灵。粘一段进来,每个空白/不可见字符都被标出来:
\s 用的就是上表这张码点表
(parser.ts 里的 SPACE),和 ECMAScript 逐项一致;. 默认排除的也正是这 4 个行终止符,
回溯与 Pike VM 两条引擎共用同一个 isLineTerminator 判定。实用建议:清洗输入时先
str.replace(/\s/gu, ' ') 把杂空白归一成普通空格,或按需 normalize + 去 BOM(replace(/^\uFEFF/, ''))。
\p{White_Space} 全量:25 个,按 gc 小分类摊开
上面那张表是 ECMAScript \s 的清单;Unicode 自己另有一个二元属性
White_Space(码点的档案页讲过)。两份名单高度重合却不相等。
这里把 White_Space=Yes 的全部 25 个码点按 General_Category 小分类摊开,每行实时
用 /^\s$/u 测 \s 是否也认——一眼找出那个唯一的反例。
U+0085 是 White_Space=Yes,\s 却不认——
就是上面 控制与换行 段里那个唯一打「·」的行,trim() 同样不剥它(EBCDIC 移植来的换行,JS 未纳入)。
其二 反过来,BOM U+FEFF 是 White_Space=No,\s 反而认——所以它不在这张 25 个的表里,
却出现在上面的 \s 清单中。要严格按 Unicode 属性匹配(认 NEL、不认 BOM),用 /\p{White_Space}/u 而非 \s。
看不见、却 White_Space=No:零宽与隐形字符全量速查
还有一大批字符肉眼根本看不见——零宽、bidi 方向控制、变体选择符、Tag、各脚本的隐形格式控制——但它们都
White_Space=No,于是 \s / \p{White_Space} / trim()
全都视而不见,它们就能在不被察觉的情况下混入文本:homoglyph 钓鱼、隐形水印、绕过「禁止空名」校验、Trojan Source。
这里把这类字符全量列出:已分配(gc≠Cn)且 \p{Default_Ignorable_Code_Point} 或 \p{Cf}、
且 White_Space=No 的共 438 个(另纳入盲文空白 U+2800;排除了约 3769 个未分配的 DI 预留码位),
按用途分类、可搜索、可快速切换。「该被渲染器隐藏」的精确属性是
\p{Default_Ignorable_Code_Point}(DI 列,每行实时测);\s 列复看它们是否被 \s 误收。
gc=Cf 的「活动」格式符 DI=No——
如 Arabic number sign U+0600(框住后续数字)、interlinear annotation U+FFF9–FFFB、Egyptian hieroglyph 格式控制——
它们不是「隐藏自己」而是「影响排版」。盲文空白 U+2800 也 DI=No,但属于伪空白(gc=So 真字形,画出来空),
和 HANGUL FILLER 一类是绕过「禁止空名」校验的主力。
其二 BOM U+FEFF 是全表唯一被 \s 收的(承前一节 \s 的历史遗留),其余 \s 一律不认。
其三 bidi override U+202E(RLO)把后文显示顺序反转 → 源码里藏反转就是 Trojan Source(详见
特殊用意的码点页)。其四 U+180E(蒙古文元音分隔)曾经是 White_Space(Zs),
Unicode 6.3 起降级为 Cf——老代码若硬把它当空白,就和今天的 \p{White_Space} 对不上了。
结论:没有单一属性能一网打尽「看不见」,实务上多用 \p{Cf} ∪ \p{Default_Ignorable_Code_Point} 再补 U+2800 之类,或维护显式清单。