JavaScript中字符串的字节长度

184

在我的 JavaScript 代码中,我需要按照以下格式向服务器组合一个消息:

<size in bytes>CRLF
<data>CRLF

示例:

3
foo

数据可能包含Unicode字符。我需要将它们以UTF-8格式发送。

我正在寻找JavaScript中计算字符串字节长度最跨浏览器的方法。

我已尝试使用以下内容组成有效载荷:

return unescape(encodeURIComponent(str)).length + "\n" + str + "\n"

但它不能为旧浏览器提供准确的结果(或者,也许是那些浏览器中的字符串采用UTF-16编码?)。

有什么线索吗?

更新:

例如:在UTF-8中,字符串ЭЭХ! Naïve?的字节数为15个字节,但某些浏览器报告23个字节。


1
可能是重复的问题?https://dev59.com/JnE95IYBdhLWcg3wp_kg - Eli
@Eli:你链接的问题中没有一个答案适用于我。 - Alexander Gladysh
当你谈论“ЭЭХ!Naïve?”时,你是否将其放入特定的正常形式中?http://unicode.org/reports/tr15/ - Mike Samuel
@Mike:我在随机文本编辑器中(以UTF-8模式)输入并保存了它,就像我的库的任何用户所做的那样。然而,我似乎已经找出了问题所在——请看我的回答。 - Alexander Gladysh
18个回答

243

岁月流逝,如今您可以以本地方式完成它

(new TextEncoder().encode('foo')).length

请注意,它不受 IE 支持(您可以使用polyfill替代)。

MDN 文档

标准规范


8
多么棒的现代方法,谢谢! - Con Antonakos
请注意,根据MDN文档,Safari(WebKit)尚不支持TextEncoder。 - Maor
自Chrome 53版本起,TextEncode仅支持utf-8 - Jehong Ahn
3
如果你只需要长度,那么分配一个新字符串、进行实际转换、获取长度,然后丢弃字符串可能会过度杀伤。请参见我上面的答案,其中提供了一种仅以高效方式计算长度的函数。 - lovasoa

107

在JavaScript中本地没有方法可以实现这一点。(参见Riccardo Galli的回答,了解现代方法。)


对于历史记录或文本编码器API仍然不可用的情况下。

如果您知道字符编码,可以自己计算。

encodeURIComponent假定UTF-8为字符编码,所以如果您需要该编码,则可以执行以下操作:

function lengthInUtf8Bytes(str) {
  // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence.
  var m = encodeURIComponent(str).match(/%[89ABab]/g);
  return str.length + (m ? m.length : 0);
}

这应该是可行的,因为UTF-8编码多字节序列的方式。第一个编码字节始终以高位为零的单字节序列,或以C、D、E或F作为第一个十六进制数字的字节开始。第二个及后续字节是首两位为10的额外字节,这些字节是你想要在UTF-8中计数的。

维基百科中的表格更加清晰明了。

Bits        Last code point Byte 1          Byte 2          Byte 3
  7         U+007F          0xxxxxxx
 11         U+07FF          110xxxxx        10xxxxxx
 16         U+FFFF          1110xxxx        10xxxxxx        10xxxxxx
...

如果你需要了解页面编码,可以使用以下技巧:

function lengthInPageEncoding(s) {
  var a = document.createElement('A');
  a.href = '#' + s;
  var sEncoded = a.href;
  sEncoded = sEncoded.substring(sEncoded.indexOf('#') + 1);
  var m = sEncoded.match(/%[0-9a-f]{2}/g);
  return sEncoded.length - (m ? m.length * 2 : 0);
}

@Alexander,当您向服务器发送消息时,是否通过HTTP头指定消息正文的内容编码? - Mike Samuel
1
@Alexander,很棒。如果你正在建立一个协议,强制使用UTF-8作为文本交换的编码方式是一个好主意。这样可以减少导致不匹配的变量。UTF-8应该是字符编码的网络字节顺序。 - Mike Samuel
4
lengthInUtf8Bytes函数对于非BMP字符返回5,而这些字符在str.length中返回2。我会写一个修改后的函数到答案部分。 - Lauri Oherd
@MikeSamuel,代理对由两个代码单元(''.length == 2)组成,即使只有一个Unicode字符。这些单独的代理半部分被公开,就好像它们是字符一样:'' == '\uD834\uDF06'来源 - Lauri Oherd
4
这个解决方案很棒,但是它没有考虑utf8mb4。例如,encodeURIComponent('') 的结果是 '%F0%9F%8D%80' - albert
显示剩余24条评论

89

这是一个更快的版本,它不使用正则表达式,也不使用encodeURIComponent()函数:

function byteLength(str) {
  // returns the byte length of an utf8 string
  var s = str.length;
  for (var i=str.length-1; i>=0; i--) {
    var code = str.charCodeAt(i);
    if (code > 0x7f && code <= 0x7ff) s++;
    else if (code > 0x7ff && code <= 0xffff) s+=2;
    if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
  }
  return s;
}

这里是一个性能比较

它只是计算由charCodeAt()返回的每个Unicode代码点的UTF8长度(基于维基百科对UTF8和UTF16代理字符的描述)。

它遵循RFC3629(其中UTF-8字符最多为4字节长)。


87

对于简单的UTF-8编码,使用Blob会比TextEncoder兼容性稍好一些。但在非常旧的浏览器中可能无法正常工作。

对于简单的UTF-8编码,使用Blob会比TextEncoder兼容性稍好一些。但在非常旧的浏览器中可能无法正常工作。

new Blob([""]).size; // -> 4  

1
这比TextEncoder还要好,是真正的答案。不需要任何polyfill。 - jprado
与下面的“Buffer”方法相同,它的缺点是无法同时在浏览器和Node中使用。因此,在可能在任一地方使用的库代码中,TextEncoder解决方案更可取。(尽管我看到Node现在对Blob有实验性支持:https://nodejs.org/api/all.html#all_buffer_class-blob) - Darren Cook
适用于Node 17。也适用于Web Workers。此外,它接受一个 File - vhs

40

使用 Buffer 的另一种非常简单的方法(仅适用于 NodeJS):

Buffer.byteLength(string, 'utf8')

Buffer.from(string).length

3
你可以跳过使用 Buffer.byteLength(string, 'utf8') 创建缓冲区。 - Joe
1
@Joe 谢谢你的建议,我刚刚进行了编辑以包含它。 - Iván Pérez

31

这个函数将返回你传入的任何UTF-8字符串的字节大小。

function byteCount(s) {
    return encodeURI(s).split(/%..|./).length - 1;
}

来源


它无法处理字符串 'ユーザーコード',期望长度为14,但实际长度为21。 - May'Habit
3
你错了,@MayWeatherVN。ユーザーコード的字节长度始终为21,我在不同的工具上进行了测试;请在发表评论时更加友善;) - Capitex
我记得在 PHP 上测试过的这个字符串是14。 - May'Habit
@May'Habit 这个包含7个字符的字符串在UCS-2中将会占用14个字节,但在UTF-8中则占用21个字节。 - Darren Cook

11

我在Firefox中比较了一些建议的方法以测试速度。

我使用的字符串包含以下字符:œ´®†¥¨ˆøπ¬˚∆˙©ƒ∂ßåΩ≈ç√∫˜µ≤

所有结果均为3次运行的平均值,时间以毫秒为单位。请注意,所有URI编码方法的表现都类似,并且结果极端,因此我只包括其中一个。

尽管根据字符串大小存在一些波动,但charCode方法(lovasoa和fuweichin)的性能都相似且最快,其中fuweichin的charCode方法是最快的。Blob和TextEncoder方法的表现也相似。 总体而言,charCode方法大约比Blob和TextEncoder方法快75%。URI编码方法基本上是不可接受的。

以下是我的结果:

大小为6.4 * 10 ^ 6字节:

Lauri Oherd  URIEncoding:     6400000    et: 796
lovasoa  charCode:            6400000    et: 15
fuweichin  charCode2:         6400000    et: 16
simap  Blob:                  6400000    et: 26
Riccardo Galli  TextEncoder:  6400000    et: 23

大小为19.2 * 10^6字节: Blob在这里做了一些奇怪的事情。

Lauri Oherd  URIEncoding:     19200000    et: 2322
lovasoa  charCode:            19200000    et: 42
fuweichin  charCode2:         19200000    et: 45
simap  Blob:                  19200000    et: 169
Riccardo Galli  TextEncoder:  19200000    et: 70

大小为64 * 10^6字节:

Lauri Oherd  URIEncoding:     64000000    et: 12565
lovasoa  charCode:            64000000    et: 138
fuweichin  charCode2:         64000000    et: 133
simap  Blob:                  64000000    et: 231
Riccardo Galli  TextEncoder:  64000000    et: 211

文件大小为192 * 10^6字节: 使用URI编码方法会导致浏览器卡在这个点。

lovasoa  charCode:            192000000    et: 754
fuweichin  charCode2:         192000000    et: 480
simap  Blob:                  192000000    et: 701
Riccardo Galli  TextEncoder:  192000000    et: 654

文件大小为 640 * 10^6 字节:

lovasoa  charCode:            640000000    et: 2417
fuweichin  charCode2:         640000000    et: 1602
simap  Blob:                  640000000    et: 2492
Riccardo Galli  TextEncoder:  640000000    et: 2338

大小为1280 * 10^6字节: Blob和TextEncoder方法在这里开始受阻。

lovasoa  charCode:            1280000000    et: 4780
fuweichin  charCode2:         1280000000    et: 3177
simap  Blob:                  1280000000    et: 6588
Riccardo Galli  TextEncoder:  1280000000    et: 5074

大小为 1920 * 10^6 字节:

lovasoa  charCode:            1920000000    et: 7465
fuweichin  charCode2:         1920000000    et: 4968
JavaScript error: file:///Users/xxx/Desktop/test.html, line 74: NS_ERROR_OUT_OF_MEMORY:

这是代码:

function byteLengthURIEncoding(str) {
  return encodeURI(str).split(/%..|./).length - 1;
}

function byteLengthCharCode(str) {
  // returns the byte length of an utf8 string
  var s = str.length;
  for (var i=str.length-1; i>=0; i--) {
    var code = str.charCodeAt(i);
    if (code > 0x7f && code <= 0x7ff) s++;
    else if (code > 0x7ff && code <= 0xffff) s+=2;
    if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
  }
  return s;
}

function byteLengthCharCode2(s){
  //assuming the String is UCS-2(aka UTF-16) encoded
  var n=0;
  for(var i=0,l=s.length; i<l; i++){
    var hi=s.charCodeAt(i);
    if(hi<0x0080){ //[0x0000, 0x007F]
      n+=1;
    }else if(hi<0x0800){ //[0x0080, 0x07FF]
      n+=2;
    }else if(hi<0xD800){ //[0x0800, 0xD7FF]
      n+=3;
    }else if(hi<0xDC00){ //[0xD800, 0xDBFF]
      var lo=s.charCodeAt(++i);
      if(i<l&&lo>=0xDC00&&lo<=0xDFFF){ //followed by [0xDC00, 0xDFFF]
        n+=4;
      }else{
        throw new Error("UCS-2 String malformed");
      }
    }else if(hi<0xE000){ //[0xDC00, 0xDFFF]
      throw new Error("UCS-2 String malformed");
    }else{ //[0xE000, 0xFFFF]
      n+=3;
    }
  }
  return n;
}

function byteLengthBlob(str) {
  return new Blob([str]).size;
}

function byteLengthTE(str) {
  return (new TextEncoder().encode(str)).length;
}

var sample = "œ´®†¥¨ˆøπ¬˚∆˙©ƒ∂ßåΩ≈ç√∫˜µ≤i";
var string = "";

// Adjust multiplier to change length of string.
let mult = 1000000;

for (var i = 0; i < mult; i++) {
  string += sample;
}

let t0;

try {
  t0 = Date.now();
  console.log("Lauri Oherd – URIEncoding:   " + byteLengthURIEncoding(string) + "    et: " + (Date.now() - t0));
} catch(e) {}

t0 = Date.now();
console.log("lovasoa – charCode:            " + byteLengthCharCode(string) + "    et: " + (Date.now() - t0));

t0 = Date.now();
console.log("fuweichin – charCode2:         " + byteLengthCharCode2(string) + "    et: " + (Date.now() - t0));

t0 = Date.now();
console.log("simap – Blob:                  " + byteLengthBlob(string) + "    et: " + (Date.now() - t0));

t0 = Date.now();
console.log("Riccardo Galli – TextEncoder:  " + byteLengthTE(string) + "    et: " + (Date.now() - t0));


7
花了一些时间才找到一个关于 React Native 的解决方案,我在这里分享一下:
首先安装 buffer 包:
npm install --save buffer

然后使用节点方法:
const { Buffer } = require('buffer');
const length = Buffer.byteLength(string, 'utf-8');

5
实际上,我已经找出问题所在。为了使代码工作,页面的标签应该包含以下内容:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
或者,如评论中所建议的那样,如果服务器发送HTTP Content-Encoding头,则也应该可以工作。 这样,不同浏览器的结果就是一致的。 以下是一个示例:
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
  <title>mini string length test</title>
</head>
<body>

<script type="text/javascript">
document.write('<div style="font-size:100px">' 
    + (unescape(encodeURIComponent("ЭЭХ! Naïve?")).length) + '</div>'
  );
</script>
</body>
</html>

注意:我怀疑指定任何(准确的)编码都可以解决编码问题。这只是我需要UTF-8的巧合。

2
JavaScript中的unescape函数不应该用于解码统一资源标识符(URI)。 - Lauri Oherd
3
“@LauriOherd unescape绝不能用于解码URI。然而,为了将文本转换为UTF-8,它工作得很好。” - T S
unescape(encodeURIComponent(...)).length 始终能够正确计算长度,无论是否使用 meta http-equiv ... utf8。如果没有编码规范,一些浏览器可能会简单地将文档的字节编码为实际的 HTML 文本,然后计算其长度,这样就会得到不同的文本。可以通过打印文本本身以及长度来轻松测试这一点。 - T S
@LauriOherd 是的,在这个例子中它不用于解码URI。 - Finesse

5

在 NodeJS 中,Buffer.byteLength 是一个专为此目的设计的方法:

let strLengthInBytes = Buffer.byteLength(str); // str is UTF-8

请注意,默认情况下该方法假定字符串采用UTF-8编码。如果需要使用不同的编码,请将其作为第二个参数传递。


仅通过知道字符串中字符的“数量”就能计算strLengthInBytes吗?例如:var text = "Hello World!"; var text_length = text.length; //将text_length作为参数传递给某个方法? 另外,关于Buffer,我刚刚看到了这个答案,它讨论了new Blob(['test string']).size和在node中的Buffer.from('test string').length。也许这些对一些人有帮助? - user1063287
1
问题在于字符数并不总是等同于字节数。例如,普遍使用的UTF-8编码是一种可变长度编码,单个字符的大小可以为1到4个字节。因此,需要使用特殊方法以及所使用的编码方式。 - Boaz
例如,一个由4个字符组成的UTF-8字符串,如果每个字符只有1个字节,则至少可能为4个字节“长”;如果每个字符为4个字节,则最多可能为16个字节“长”。请注意,在任何情况下,“字符计数”仍为4,因此它不是一种可靠的衡量“字节长度”的方法。 - Boaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接