前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库JavaScript 字符串长度 Unicode 编码
JavaScript值与类型

为什么一个表情符号的 length 是 2,家庭表情甚至是 8?

length 数的是 UTF-16 码元的个数。常用汉字一个码元,所以 '中文'.length 是 2;😀 这类表情要两个码元,length 是 2;带肤色、家庭这类组合表情由多个码点拼成,length 可以到 4、8 甚至更多。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#值与类型
先看核心答案读代码示例
理解线索

字符串长度的三层单位

  1. 码元UTF-16 的 16 位存储单元,length 和下标都按它算
  2. 码点Unicode 给每个字符的编号,超过 U+FFFF 的要两个码元
  3. 字素簇用户眼里的一个字,可能由多个码点组合而成

生僻字也可能占两个码元。像「𠮷」这类扩展区汉字码点超过 U+FFFF,length 是 2,只处理表情不处理汉字的截断逻辑会在人名上出问题。

核心回答

先记住这个答案

JavaScript 字符串在内部按 UTF-16 编码存储,length 返回的是 16 位码元的个数,不是用户看到的字数。英文字母和常用汉字都在基本平面里,一个字一个码元;😀 这类表情的码点超过 U+FFFF,要用两个码元组成的代理对来存,所以 length 是 2。带肤色的点赞、家庭表情是好几个码点用连接符拼起来的,length 会是 4、8 甚至更多。

  • length 数码元,不数用户看到的字
  • 常用汉字 1 个码元,大部分表情 2 个
  • [...str].length 数码点,组合表情仍会多算
  • 按看到的字数统计用 Intl.Segmenter

同一个「字」,三种数法

把几类字符放在一起,分别用 length、展开运算符、Intl.Segmenter 数一遍,差别一目了然。

码元、码点、字素簇对照JavaScript
const list = [['英文', 'abc'], ['汉字', '中文'], ['表情', '😀'],
  ['带肤色', '👍🏽'], ['家庭', '👨‍👩‍👧'], ['组合重音', 'e\u0301']]
const seg = new Intl.Segmenter('zh', { granularity: 'grapheme' })
for (const [name, s] of list) {
  const codePoints = [...s].length              // 按码点数
  const graphemes = [...seg.segment(s)].length  // 按用户看到的字数
  console.log(name, s.length, codePoints, graphemes)
}
查看输出与解释
英文 3 3 3
汉字 2 2 2
表情 2 1 1
带肤色 4 2 1
家庭 8 5 1
组合重音 2 2 1

每行三个数依次是 length、码点数、字素簇数。只有最后一列和用户的感受一致。 带肤色的 👍🏽 是「点赞」加「肤色修饰符」两个码点,每个都占两个码元,所以 length 是 4。家庭表情是三个人物表情中间夹两个零宽连接符(ZWJ),一共 5 个码点、8 个码元,屏幕上却只显示一个图案。

为什么是 UTF-16

JavaScript 诞生时,Unicode 还以为 65536 个字符就够用,16 位一个字符正好。后来字符数量不够,Unicode 扩展到了 U+10FFFF,UTF-16 用「代理对」来表示超出部分:一个高位代理加一个低位代理,两个码元合起来表示一个码点。

为了兼容,length、下标、charCodeAt、slice 这些老 API 都保持按码元工作。ES2015 之后新增的 for...of、展开运算符、codePointAt、String.fromCodePoint 才开始按码点处理。

组合字符是另一回事。「é」可以是一个预组合码点,也可以是字母 e 加一个组合重音符,后者是两个码点,length 为 2。这种情况可以用 normalize('NFC') 合并成一个码点。

项目里怎么数字数

输入框限制「最多 20 个字」,后端存储限制「最多 50 字节」,这是两个不同的需求。前者按用户看到的字数,用 Intl.Segmenter 最准确,不支持的环境退而用 [...str].length;后者要按 UTF-8 字节数算,可以用 new TextEncoder().encode(str).length,一个汉字 3 字节,一个表情 4 字节。

按规范,HTML 的 maxlength 属性也是按码元计数的,限制 10 的输入框大约只能输 5 个表情。做表单校验时,前端提示的字数口径要和这个限制对齐。

回答前,多想一步

容易答错的地方

以为中文字符 length 都是 2
常用汉字都在基本多文种平面内,一个字就是一个码元,'中文'.length 是 2 而不是 4。只有扩展区的生僻字才会占两个码元。说「中文算两个」是把 UTF-16 和 GBK 双字节的说法混在一起了。
以为 [...str].length 就是真实字数
展开运算符按码点拆分,能正确处理单个表情,但组合表情、带肤色表情、组合重音仍然会被拆成多个。要和用户看到的一致,只能按字素簇拆。
用 slice 按 length 截断昵称
nickname.slice(0, 10) 按码元截,刚好截在代理对中间,末尾会留下半个表情,显示成一个问号方块。截断要先按字素簇拆成数组,再取前几项拼回去。
试着用自己的话回答

面试官还会怎么问?

怎么判断一个字符串里有没有表情?

可以用带 u 标志的正则配合 Unicode 属性转义,比如 /\p{Extended_Pictographic}/u.test(str)。不加 u 标志时,正则也按码元匹配,. 会只匹配半个表情。

charCodeAt 和 codePointAt 有什么区别?

charCodeAt(i) 返回第 i 个码元的值,遇到表情只拿到高位代理。codePointAt(i) 在 i 位置是高位代理时,会把后面的低位代理一起读出来,返回完整码点,比如 😀 得到 0x1F600。

反转含表情的字符串为什么会乱码?

str.split('').reverse().join('') 按码元拆分,代理对的高低位被颠倒,变成两个无效码元。改成 [...str].reverse().join('') 能处理单个表情,组合表情仍然需要按字素簇拆分后再反转。

从一道题,走向一组知识

把知识连起来

值与类型

两个看起来一模一样的字符串,为什么用 === 比较却不相等?

组合重音字符的 length 为 2,normalize('NFC') 后能变回 1。

值与类型

slice、substring、substr 遇到负数和起止颠倒时有什么区别?

slice 和 substring 也按码元截取,用 length 算位置截表情会切出半个字符。

参考资料

  • JavaScript:值与类型参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 同一个「字」,三种数法
  3. 为什么是 UTF-16
  4. 项目里怎么数字数
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑