历史的坑
JS 的字符串长度是个奇怪的设定,很多编程语言,获取字符串的长度是得到 字节长度,比如一个正常的汉字是两个字节,但在 js 中,'汉'.length 是 1 。看上去很方便,殊不知,这个特性埋下的坑。
比如:
😀
:
'😀'.length
得到的是
2
𠮷
:
'𠮷''.length
得到的也是
2
JS 内部字符以
UTF-16
的格式储存 (一种兼容
utf-8
,但可储存编码比
utf-8
多
8
位),简单点说,就是最多存 16 位的编码,但中华文化博大精深,字太多了,有些字排到后面,都不只 16 位编码了,于是,在 js 中只能用 2 个 utf-16 来储存,于是,这个
𠮷
这就被 js 当成长度为 2 了。
生辟字很少见的,但
emoji
不少见啊,还很流行,关键的是,大多都是 编码超过 16 位,也就是说 js 中长度不止为 1 可能有的 emoji 长度为 2 都不止。
比如这个 👨👩👧👧 ,运行
'👨👩👧👧'.length
在 js 中 答案为 11 , 这里不去深究 Emoji 的历史了,有兴趣可以去了解下。
解决办法
那在需求中,比如评论输入框中
限定 20 的字符
, 很明显 直接用
length
判断长度 ,用户两个 emoji 就不够了,显然不能满足需求
其他一些
for ... of
和
codePointAt
方法在一些情况下是可以解决长度问题,但在 组合 emoji 中就无能为力了
弊端的实现
function length(s) {
let len = 0;
for (let i = 0; i < s.length; i++) {
len++;
// 0xffff 是16进制,一个f相当4位二进制,4个f就是16位
if (s.codePointAt(i) > 0xffff) i++;
return len;
}
length("😀"); //1
length("𠮷"); //1
这个方法一些情况可以获取真实长度,但是在 组合 emoji 比如 👨👩👧👧 就会有问题。
length("👨👩👧👧"); // 7 很明显不对
究极解决
这里需要引入 lodash 的
toArray
方法 转成数组 再获取数组的长度
有兴趣的可以去
https://github.com/lodash/lodash/blob/master/toArray.js
源码了解它的实现 这里先不去深究了
function trueLength(str) {
return _.toArray(str).length;
}
不想引入loadsh 我这边根据源码重新写了一个
const countStrLen = (string) => {
const reUnicode = /\ud83c[\udffb-\udfff](?=\ud83c[\udffb-\udfff])|(?:[^\ud800-\udfff][\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]?|[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]|\ud83c[\udffb-\udfff])?)*/g;
return (string.match(reUnicode) || []).length
countStrLen('👨👩👧👧') //1
或者直接挂载在原型链上
String.prototype.countLengtn = function() {
const reUnicode = /\ud83c[\udffb-\udfff](?=\ud83c[\udffb-\udfff])|(?:[^\ud800-\udfff][\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]?|[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff\u1ab0-\u1aff\u1dc0-\u1dff]|\ud83c[\udffb-\udfff])?)*/g;
return (this.toString().match(reUnicode) || []).length
"𠮷👨👩👧👧👨👩👧👧😀𠮷".countLengtn() // 5