统计字数的几种方法
一般情况下,统计的时候,Emoji 是算成一个数,但 ES6 之前的 api 是会把这种字符算成多个。
1. 最简单但有明显问题的 String.prototype.length
1 2 3 4 5 6 7 8 9
| "中文".length; "𠮷".length; "💖".length; "🙋♂️".length;
console.log("\u4E2D\u6587"); console.log("\uD83D\uDC96"); console.log("\uD842\uDFB7"); console.log("\uD83D\uDE4B\u200D\u2642\uFE0F");
|
最后一个 🙋♂️ 的长度有 5 个字符,中间有一个零宽字符 \u200D
,能把两个 Emoji 合并成一个。 最后有一个 \uFE0F
是一个变体选择器。 可以看文末参考链接。
下面是一个黑色皮肤的举手 Emoji:
1
| console.log("\uD83D\uDE4B\uD83C\uDFFF\u200D\u2642\uFE0F");
|
这里面的 \uD83C\uDFFF
代表的是黑色皮肤,如果是 \uD83C\uDFFB
代表的是白色皮肤,可以看参考文末链接。
2. 使用 Array.from
1 2 3 4
| Array.from("中文").length; Array.from("𠮷").length; Array.from("💖").length; Array.from("🙋♂️").length;
|
这里会把 🙋♂️ 分拆成四个部分 ["\uD83D\uDE4B", "\u200D", "\u2642", "\uFE0F"]
1 2 3 4
| console.log("\uD83D\uDE4B"); console.log("\u200D"); console.log("\u2642"); console.log("\uFE0F");
|
所以使用 Array.from
也不是一个比较好的统计字数的方法。
3. 使用正则表达式
ES6 之后给 REGEX FLAGS 添加了 u
标识位,对 Unicode 更加友好的支持。比如:
1 2
| '𠮷'.match(/\S/g).length // 2 '𠮷'.match(/\S/gu).length // 1
|
但在处理组合的 Emoji 的时候,还是没有比较好的处理:
1
| '🙋♂️'.match(/\S/gu).length // 4
|
后来发现一个字数统计的正则:
1 2 3 4 5
| const ASTRAL_REGEX = /\ud83c[\udffb-\udfff](?=\ud83c[\udffb-\udfff])|(?:[^\ud800-\udfff][\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]?|[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*/g;
"𠮷".match(ASTRAL_REGEX).length; "🙋♂️".match(ASTRAL_REGEX).length;
|
Benchmark
写了一个脚本分别测试原生 length
、Array.from
、/\S/gu
、REGEX_UNICODE_CHARACTER 测试判断的长度时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| const UNICODE_WORD_REGEX = /[\u00ff-\uffff]|\S+/g; const UNICODE_CHARACTER_REGEX = /\S/gmu; const UNICODE_CHARACTER2_REGEX = /\ud83c[\udffb-\udfff](?=\ud83c[\udffb-\udfff])|(?:[^\ud800-\udfff][\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]?|[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*/g; const TIMES = 100000;
const str = Array.from(new Array(10)).reduce( (s) => s + "中文𠮷?précédantお客様の💖🙋♂️", "" );
let count1 = 0; console.time("length"); for (let i = 0; i < TIMES; ++i) { count1 += str.length; } console.timeEnd("length"); console.log("count1 length:", count1);
let count2 = 0; console.time("array from"); for (let i = 0; i < TIMES; ++i) { count2 += Array.from(str).length; } console.timeEnd("array from"); console.log("count2 length:", count2);
let count3 = 0; console.time("regex unicode"); for (let i = 0; i < TIMES; ++i) { count3 += str.match(UNICODE_CHARACTER_REGEX).length; } console.timeEnd("regex unicode"); console.log("count3 length:", count3);
let count4 = 0; console.time("regex2"); for (let i = 0; i < TIMES; ++i) { count4 += str.match(UNICODE_CHARACTER2_REGEX).length; } console.timeEnd("regex2"); console.log("count4 length:", count4);
|
通过测试 190 个字,循环 100000 遍,得到执行结果如下,使用 UNICODE_CHARACTER2_REGEX
是一个折衷方案中最好的方法。
1 2 3 4 5
| 使用方法 | 计算长度 | 耗费时间(ms) String.prototype.length | 25000000 | 3.134 Array.from | 22000000 | 2635.567 UNICODE_CHARACTER_REGEX | 22000000 | 916.429 UNICODE_CHARACTER2_REGEX | 19000000 | 748.209
|
UNICODE_CHARACTER2_REGEX
统计出的 19000000 才是正确的。
参考

这里的 FITZ-(1~5) 就是不同的五种肤色,从 \u1F3FB – \u1F3FF 共五个。
常见的有:
- “变量选择器-15”(VARIATION SELECTOR-15, 简写 VS-15): \uFE0E, 作用是让基础 Emoji 变成更接近文本样式(text-style) (e.g. ☹︎);
- “变量选择器-16”(VARIATION SELECTOR-16, 简写 VS-16): \uFE0F, 作用则是让基础 Emoji 变成更接近 Emoji 样式(emoji-style) (e.g. ☹️).