零宽字符在开发中的应用与注意事项
零宽字符(Zero-Width Characters)是一类不可见但实际存在的Unicode字符,它们在开发中有多种用途但也可能带来问题。
常见的零宽字符
-
零宽度空格 (U+200B)
- 最常见的零宽字符
- HTML中可用
​表示

-
零宽度非连接符 (U+200C)
- 用于某些语言的排版
(例如,"A\u200CB"与"AB"在程序中被视为不相等)
- 用于某些语言的排版
-
零宽度连接符 (U+200D)
- 用于表情符号的组合
-
左至右标记 (U+200E) 和 右至左标记 (U+200F)
- 控制文本方向
-
单词连接符 (U+2060)
- 防止换行时断开单词
开发中的用途
-
水印与隐藏信息
javascript
// 在文本中嵌入隐藏信息 function embedHiddenText(visibleText, hiddenText) { const zeroWidthSpace = '\u200B'; const zeroWidthNonJoiner = '\u200C'; let binaryHidden = hiddenText.split('').map(c => c.charCodeAt(0).toString(2).padStart(8, '0') ).join(''); let steganographed = visibleText; binaryHidden.split('').forEach(bit => { steganographed += bit === '0' ? zeroWidthSpace : zeroWidthNonJoiner; }); return steganographed; } -
防止恶意爬取
- 在关键数据中插入零宽字符,干扰爬虫解析
-
文本格式标记
- 在不影响显示的情况下标记文本段落
潜在问题与解决方案
-
调试困难
javascript
// 检测字符串中的零宽字符 function hasZeroWidthChars(str) { const zeroWidthRegex = /[\u200B-\u200F\u2060\uFEFF]/g; return zeroWidthRegex.test(str); } // 移除所有零宽字符 function removeZeroWidthChars(str) { return str.replace(/[\u200B-\u200F\u2060\uFEFF]/g, ''); } -
数据验证问题
javascript
// 严格的用户名验证(排除零宽字符) function isValidUsername(username) { return !/[\u200B-\u200F\u2060\uFEFF]/.test(username) && /^[a-zA-Z0-9_\-]{3,20}$/.test(username); } -
数据库存储问题
- MySQL:
SET NAMES utf8mb4 COLLATE utf8mb4_bin - PostgreSQL:
TEXT COLLATE "C"
- MySQL:
最佳实践
- 明确文档记录何时使用零宽字符
- 添加注释说明使用原因
- 输入过滤关键字段应过滤这类字符
- 输出转义在显示前处理可能有害的零宽字符
实现说明
-
编码过程:
encodeToInvisible函数首先将输入字符串的每个字符转换为16位的二进制表示(确保统一长度)- 然后将每个二进制数字替换为对应的零宽度Unicode字符:
1→\u200b(零宽度空格)0→\u200c(零宽度非连接符)- →
\u200d(零宽度连接符)
- 最后在前面添加一个
\ufeff作为标记
-
解码过程:
decodeFromInvisible函数反向操作,将零宽度字符转换回二进制数字和空格- 然后将每16位二进制数转换回原始的Unicode字符
-
特点:
- 完全不可见的隐写术技术
- Unicode兼容性好,可以在大多数现代系统中使用
- JS内置支持,无需额外库
注意事项
- 安全性:这不是加密,只是隐写术。任何知道这个方法的人都可以解码内容。
- 长度限制:编码后的长度会显著增加(大约16倍)。
- 兼容性:某些系统可能会过滤或修改这些特殊Unicode字符。
- 应用场景:适用于水印、元数据隐藏等场景。
javascript
// 编码函数:将普通字符串转为隐形字符
function encodeToInvisible(text) {
// 将字符串转换为二进制表示
let binaryString = '';
for (let i = 0; i < text.length; i++) {
// 获取字符的Unicode码点,然后转换为16位二进制字符串
const charCode = text.charCodeAt(i);
binaryString += charCode.toString(2).padStart(16, '0') + ' ';
}
// 移除最后一个多余的空格
binaryString = binaryString.trim();
// 替换二进制数字为不可见字符
let invisibleString = '';
for (const char of binaryString) {
switch (char) {
case '1':
invisibleString += '\u200b'; // Zero Width Space
break;
case '0':
invisibleString += '\u200c'; // Zero Width Non-Joiner
break;
case ' ':
invisibleString += '\u200d'; // Zero Width Joiner
break;
}
}
// 使用零宽度非断空格符作为分隔符(虽然这里我们没有多个部分要分隔)
return '\ufeff' + invisibleString;
}
// 解码函数:将隐形字符转回普通字符串
function decodeFromInvisible(invisibleText) {
// 移除开头的\ufeff分隔符(如果有)
if (invisibleText.startsWith('\ufeff')) {
invisibleText = invisibleText.substring(1);
}
// 将不可见字符转换回二进制数字和空格
let binaryString = '';
for (const char of invisibleText) {
switch (char) {
case '\u200b':
binaryString += '1';
break;
case '\u200c':
binaryString += '0';
break;
case '\u200d':
binaryString += ' ';
break;
}
}
// 分割二进制字符串为各个字符的二进制表示
const binaryParts = binaryString.split(' ');
// 将每个16位二进制转换回Unicode字符
let originalText = '';
for (const part of binaryParts) {
if (part.length === 16) { // 确保是16位二进制数
const charCode = parseInt(part, 2);
originalText += String.fromCharCode(charCode);
}
}
return originalText;
}
// 使用示例
const originalText = "Hello, World!";
console.log("原始文本:", originalText);
// 编码为隐形字符
const encodedText = encodeToInvisible(originalText);
console.log("编码后的隐形文本:", encodedText);
console.log("编码后长度:", encodedText.length);
console.log("看起来是空的:", encodedText === "" ? "是" : "否");
// 解码回原始文本
const decodedText = decodeFromInvisible(encodedText);
console.log("解码后的文本:", decodedText);
// 验证是否一致
console.log("是否匹配原始文本:", decodedText === originalText ? "是" : "否");
// Vue/React组件示例:安全显示可能包含零宽字符的文本 function SafeText({ children }) { const cleanText = children.replace(/[\u200B-\u200F\u2060\uFEFF]/g, ''); return <span>{cleanText}</span>; }
在实际开发中,合理利用零宽字符可以解决特定问题,但也要注意它们可能带来的副作用。