你很可能遇到过这个 bug:从文档页复制一条命令,粘到终端里执行,结果失败了。命令看上去完全正确,你用手再敲一遍,却成功了。 问题不在你的操作,而在于你复制的命令里混进了一个隐形字符:U+00A0 NO-BREAK SPACE。它看起来和普通空格一样,但 shell 把它当成另一个 token。你的眼睛分辨不出差别,程序却严格区分。 这不是孤例,而是一整族以“看不见”为特征的 bug。于是很多人会想到写一个“清洁器”,把所有不可见字符删掉。这个方向,恰恰是最容易出错的地方。 最常见的实现是黑名单式清理: ```js const clean = s => s.replace(/[\u200B-\u200D\u2060\uFEFF\u00AD]/g, ""); ``` 这段代码看起来干净利落,但跑一下这个字符串: ```js clean("\u{1F468}\u200D\u{1F469}\u200D\u{1F466}"); // 输入: 一个家庭 emoji // 输出: 三个单独的人,两个连接符都被删了 ``` U+200D ZERO WIDTH JOINER 在那里并不是垃圾,它恰恰是 emoji 序列得以组成一个家庭的机制。把它删掉,你就在用户看不见的地方改写了内容。 更严重的问题发生在真实语言里。波斯语、阿拉伯语、以及多种印度文字中,U+200C 和 U+200D 承担着实际的拼写功能。比如波斯语表示“我读”的单词: ```js const persian = "\u0645\u06CC\u200C\u062E\u0648\u0627\u0646\u0645"; clean(persian); // 删除 U+200C 后,拼写从“میخوانم”变成了另一种写法 ``` 在梵文天城体里,U+0915、U+094D、U+200D、U+0937 组合出来的连字,和去掉连接符后的内容是两个不同的字形。一个删除零宽字符的“清洁器”不是在清理,是在毁坏内容。最可怕的是,因为这些字符不可见,没人会立刻发现,直到问题在很远的地方爆出来。 正确的修复思路,不是把某个码点位看作“本质上的垃圾”,也不是让一份黑名单替所有语言做决定。真正的做法是,理解你正在处理的内容是什么:如果它是 shell 命令,就去检查是否有非法空白;如果它是文本或 emoji,就不要用粗暴的字符清理解析它。必要时按上下文判断,而不是在不知道字符用途的情况下,删掉所有“看不见的东西”。 隐形字符不是洪水猛兽,真正危险的是我们以为它们毫无意义。
热门跟贴