AI 写的字数统计器为什么连 Emoji 都数不对?从 UTF-16 到 Intl.Segmenter 的 Unicode

举报
yd_213270272 发表于 2026/11/08 23:56:46 2026/11/08
【摘要】 前言现在使用 AI 编程助手开发网页小工具非常方便。例如,我们只需要提出一个需求:“帮我开发一个在线字数统计器,支持中文、英文和 Emoji。”AI 很快就能生成一个输入框,再使用 JavaScript 统计字符数量。最常见的写法就是:const text = "你好世界";console.log(text.length);结果为 4,看起来完全正确。但如果换成 Emoji 呢?conso...

前言

现在使用 AI 编程助手开发网页小工具非常方便。

例如,我们只需要提出一个需求:

“帮我开发一个在线字数统计器,支持中文、英文和 Emoji。”

AI 很快就能生成一个输入框,再使用 JavaScript 统计字符数量。

最常见的写法就是:

const text = "你好世界";
console.log(text.length);

结果为 4,看起来完全正确。

但如果换成 Emoji 呢?

console.log("😀".length);

结果竟然是:

2

明明只有一个 Emoji,为什么 JavaScript 认为它的长度是 2?

如果输入家庭 Emoji:

console.log("👨‍👩‍👧‍👦".length);

结果甚至是 11。

这并不是 JavaScript 计算错误,而是我们对“字符”的理解与 JavaScript 字符串的内部表示方式不同。

本文就从这个常见问题出发,介绍 UTF-16、Unicode 码点以及 Intl.Segmenter,并实现一个能够正确处理大多数常见 Emoji 组合的网页字符统计工具。


一、为什么 JavaScript 的 length 不等于字符数量?

JavaScript 字符串采用 UTF-16 编码单元序列表示。

简单理解,length 返回的是 UTF-16 编码单元的数量,而不是用户肉眼看到的字符数量。

对于常见汉字、英文字母:

console.log("ABC".length);    // 3
console.log("你好".length);    // 2

这些结果通常符合我们的直觉。

但是,部分 Unicode 字符需要使用两个 UTF-16 编码单元表示。

例如:

console.log("😀".length);     // 2

虽然屏幕上只有一个笑脸,但它占用了两个 UTF-16 编码单元。

更复杂的 Emoji 还可能由多个 Unicode 码点组合而成。

例如家庭 Emoji:

👨‍👩‍👧‍👦

它并不是一个单独的 Unicode 码点,而是由多个 Emoji 和零宽连接符组合形成的序列。

因此,如果直接用 length 统计用户感知的字符数量,就可能得到不符合直觉的结果。

二、解决方案一:使用 Array.from()

JavaScript 提供了一种更接近 Unicode 码点统计的方法。

例如:

const text = "😀";

console.log(Array.from(text).length);

结果是:

1

原因在于,字符串迭代器会按 Unicode 码点处理有效的代理项对,而不是简单地逐个读取 UTF-16 编码单元。

所以:

console.log(Array.from("你好").length); // 2
console.log(Array.from("😀").length);   // 1

但是,这个方法还有一个局限。

继续测试:

const family = "👨‍👩‍👧‍👦";

console.log(family.length);
console.log(Array.from(family).length);

结果分别是:

11
7

问题来了:

家庭 Emoji 明明显示成一个图案,为什么按照 Unicode 码点统计仍然有 7 个?

因为 Unicode 码点并不等于用户感知字符。

Array.from() 解决了部分问题,但并不能完整识别组合 Emoji。

三、什么是 Grapheme Cluster?

在 Unicode 文本处理中,还有一个重要概念:

Grapheme Cluster,通常称为字素簇。

可以把它理解为按照 Unicode 文本分割规则划分出的用户感知字符单元。

例如:

输入内容 UTF-16 编码单元 Unicode 码点 字素簇
A 1 1 1
你 1 1 1
😀 2 1 1
👍🏽 4 2 1
👨‍👩‍👧‍👦 11 7 1

可以看到:

当我们希望统计用户视觉上认为的一个完整 Emoji 时,字素簇通常更符合需求。

不过,字素簇是基于 Unicode 分割规则的文本单位,并不保证与所有字体、浏览器中的实际字形显示始终一一对应。

四、解决方案二:使用 Intl.Segmenter

JavaScript 提供了 Intl.Segmenter API,可以按指定粒度分割文本。

其中一个非常实用的配置就是:

granularity: "grapheme"

例如:

const segmenter = new Intl.Segmenter("zh-CN", {
  granularity: "grapheme"
});

const text = "👨‍👩‍👧‍👦";

const characters = [
  ...segmenter.segment(text)
];

console.log(characters.length);

结果为:

1

这次,家庭 Emoji 被识别为一个字素簇。

如果我们想要统计整个字符串,可以封装为一个函数:

function countCharacters(text) {
  const segmenter = new Intl.Segmenter("zh-CN", {
    granularity: "grapheme"
  });

  return [...segmenter.segment(text)].length;
}

测试:

console.log(countCharacters("你好世界"));  // 4
console.log(countCharacters("😀"));        // 1
console.log(countCharacters("👍🏽"));       // 1
console.log(countCharacters("👨‍👩‍👧‍👦")); // 1

这样就能处理许多普通字符串统计方法无法正确识别的组合字符。

五、实战:开发一个 Unicode 字符统计网页

接下来实现一个简单的网页工具。

功能包括:

  1. 实时统计 UTF-16 编码单元数量。
  2. 统计 Unicode 码点数量。
  3. 统计字素簇数量。
  4. 显示 UTF-8 字节数。

整个项目使用浏览器原生 API,不需要后端服务器,也不需要第三方依赖。

创建 index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, initial-scale=1">
  <title>Unicode 字符统计器</title>

  <style>
    body {
      font-family: system-ui, sans-serif;
      max-width: 650px;
      margin: 40px auto;
      padding: 20px;
      line-height: 1.6;
    }

    textarea {
      box-sizing: border-box;
      width: 100%;
      min-height: 160px;
      padding: 12px;
      font: inherit;
    }

    .result {
      background: #f1f5f9;
      padding: 16px;
      margin-top: 16px;
      border-radius: 10px;
    }
  </style>
</head>

<body>
  <h1>Unicode 字符统计器</h1>

  <label for="input">
    输入需要统计的文本
  </label>

  <textarea id="input"
    placeholder="输入中文、英文或 Emoji"></textarea>

  <div class="result">
    <p>UTF-16 编码单元:<strong id="units">0</strong></p>
    <p>Unicode 码点:<strong id="points">0</strong></p>
    <p>字素簇:<strong id="graphemes">0</strong></p>
    <p>UTF-8 字节数:<strong id="bytes">0</strong></p>
  </div>

  <p id="notice" role="status"></p>

  <script>
    const input = document.getElementById("input");

    const segmenter =
      typeof Intl.Segmenter === "function"
        ? new Intl.Segmenter("zh-CN", {
            granularity: "grapheme"
          })
        : null;

    function updateCount() {
      const text = input.value;

      const units = text.length;
      const points = Array.from(text).length;
      const bytes = new TextEncoder()
        .encode(text).length;

      const graphemes = segmenter
        ? Array.from(segmenter.segment(text)).length
        : null;

      document.getElementById("units")
        .textContent = units;

      document.getElementById("points")
        .textContent = points;

      document.getElementById("bytes")
        .textContent = bytes;

      document.getElementById("graphemes")
        .textContent = graphemes ?? "不支持";

      document.getElementById("notice")
        .textContent = segmenter
          ? ""
          : "当前浏览器不支持 Intl.Segmenter";
    }

    input.addEventListener("input", updateCount);

    updateCount();
  </script>
</body>
</html>

直接使用支持 Intl.Segmenter 的现代浏览器打开,就可以体验不同字符统计方式的区别。

例如输入:

你好😀👨‍👩‍👧‍👦

在支持上述 Unicode 分割行为的环境中,预期统计结果是:

指标 结果
UTF-16 编码单元 15
Unicode 码点 10
字素簇 4
UTF-8 字节数 35

其中,字素簇统计最接近用户在这个例子中看到的四个完整字符单位。

六、为什么字数限制也可能出现 Bug?

这个问题在真实业务中很常见。

例如,一个网站要求用户昵称最多输入 10 个字符。

开发者可能写出:

if (nickname.length > 10) {
  alert("昵称不能超过10个字符");
}

但如果用户输入多个 Emoji,这种限制就可能与用户预期不一致。

例如,五个普通笑脸 Emoji:

😀😀😀😀😀

它们的 length 为 10。

用户明明只输入了五个 Emoji,却已经用完了十个 UTF-16 编码单元。

如果产品需求真正要求限制用户感知字符数量,就应该考虑字素簇统计。

但需要注意:

并不是所有场景都应该改用字素簇。

例如:

  • 数据库字段可能以字节长度限制。
  • 某些 API 规定按 Unicode 码点统计。
  • 文件协议可能规定具体编码长度。
  • 短信计费可能使用其他编码和分段规则。

因此,开发者首先要弄清楚业务要求中的“字符”究竟指什么。

不能认为某一种统计方式适合所有场景。

七、还有一个容易忽略的问题:Unicode 规范化

在 Unicode 中,一些看起来相同的文字可能存在不同的编码表示。

例如,带重音的字母 é 可以使用一个预组合字符表示,也可以通过字母 e 加组合重音符号表示。

const a = "\u00E9";
const b = "e\u0301";

console.log(a === b); // false

虽然它们可能显示得很相似,但实际字符串并不相同。

JavaScript 提供了:

normalize()

用于 Unicode 规范化。

例如:

console.log(
  a.normalize("NFC") === b.normalize("NFC")
);

结果是:

true

这对于文本比较、搜索和去重工具非常有用。

不过,规范化也会改变字符串表示,因此不能在所有场景中无条件使用。

尤其是密码、数字签名、文件哈希等依赖原始字节内容的场景,不能随意规范化后再处理。

八、AI 编程助手为什么容易忽略 Unicode?

因为大多数简单测试只包含:

Hello
你好
12345

这些输入通常不会暴露 UTF-16 编码单元与用户感知字符之间的区别。

如果测试数据没有 Emoji、组合字符或特殊 Unicode 序列,使用 length 可能一直看起来正确。

因此,在开发以下功能时,应该主动增加 Unicode 测试:

  • 字数统计器。
  • 昵称长度限制。
  • 文本截取。
  • 字符逐个显示动画。
  • 搜索和文本去重。
  • 表单输入验证。

例如,可以在给 AI 编程助手的提示词中增加:

文本处理功能必须考虑 Unicode。涉及用户感知字符数量时,优先使用 Intl.Segmenter,并测试中文、Emoji、组合字符和空字符串。不要简单地用 JavaScript 的 length 作为所有字符数量的判断依据。

这会比单纯要求“支持中文和 Emoji”更加明确。

九、总结

通过本文的实践,我们可以看到:

JavaScript 的字符串长度并不是一个看起来那么简单的问题。

length 统计的是 UTF-16 编码单元。

Array.from() 可以按 Unicode 码点遍历。

Intl.Segmenter 则能够按照 Unicode 字素簇规则识别完整的字符单元。

对于普通英文和常见汉字,这几种统计方式可能得到相同结果。

但当文本包含 Emoji、组合符号和复杂 Unicode 序列时,结果就可能明显不同。

我认为,AI 自动开发网页时,除了关注按钮是否正常、布局是否美观,也应该关注这些隐藏在常见 API 背后的边界情况。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。