前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 正确反转含emoji的字符串
JavaScript数据处理

split('').reverse().join('') 反转带 emoji 的字符串,为什么会乱码?

split('') 按 UTF-16 代码单元拆字符串,一个 emoji 会被拆成两半,反转后两半顺序颠倒,就成了乱码。改成 [...str] 或 Array.from(str) 按码点拆,能修好普通 emoji;家庭 emoji、国旗、带组合重音的字母还要用 Intl.Segmenter 按字素簇拆。

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

反转之前,先想清楚「一个字符」是什么

  1. 代码单元split('') 和 length 用的单位,emoji 占两个
  2. 码点[...str] 和 Array.from 用的单位,一个 emoji 通常是一个码点
  3. 字素簇用户看到的一个字,可能是好几个码点拼成的,Intl.Segmenter 按它切

按码点反转国旗不会报错,但会把两个区域指示符重新配对,🇨🇳🇯🇵 可能变成两个完全不同的国旗,比乱码更难发现。

核心回答

先记住这个答案

JavaScript 字符串是 UTF-16 编码的,😀 这类 emoji 在内部占两个代码单元,叫高代理项和低代理项,必须按固定顺序挨在一起才能组成一个字符。split('') 按代码单元拆分,会把这一对拆开,reverse() 之后顺序变成「低、高」,已经不是合法的字符,所以显示成乱码。改用 [...str] 或 Array.from(str),它们按码点拆分,一个 emoji 是一个整体。但家庭 emoji、国旗、带组合重音的字母是多个码点拼成的,要彻底正确,得用 Intl.Segmenter 按用户看到的字符来拆。

  • split('') 按代码单元拆,会把 emoji 切成两半
  • [...str] 按码点拆,单个 emoji 不会坏
  • 国旗、家庭 emoji、组合重音要按字素簇反转
  • Intl.Segmenter 的 grapheme 粒度就是字素簇

为什么 split('') 会出乱码

面试里这道题一般是从「写一个字符串反转」开始的,你写了 str.split('').reverse().join(''),面试官接着问:如果字符串里有 emoji 呢?

按代码单元和按码点反转对比JavaScript
const text = 'A😀B'

console.log(text.split('').length) // 4:emoji 被切成了两个单位
const bad = text.split('').reverse().join('')
console.log(JSON.stringify(bad)) // 用 JSON 转义看清楚内部顺序
console.log([...text].reverse().join('')) // 按码点拆,emoji 完整
console.log(Array.from(text).reverse().join('')) // 和展开写法等价
查看输出与解释
4
"B\ude00\ud83dA"
B😀A
B😀A

😀 的编码是高代理项 \ud83d 加低代理项 \ude00。split('') 把它们拆成两个元素,反转后变成 \ude00\ud83d,低的跑到了前面。这两个代码单元单独都不代表任何字符,页面上通常显示成两个替换字符 �。 [...text] 能修好,是因为展开语法调用的是字符串的迭代器,而字符串迭代器是按码点前进的:遇到一个高代理项,会连同后面的低代理项一起作为一项产出。Array.from 走的也是同一个迭代器。

按码点反转还不够的三种情况

很多人答到 Array.from 就停了。但有不少「一个字」是由多个码点拼成的,按码点反转会把它们拆散。

家庭 emoji、国旗和组合重音JavaScript
const family = '👨\u200D👩\u200D👧' // 爸爸、妈妈、女儿用零宽连接符串起来
console.log(family.length, [...family].length) // 8 个代码单元,5 个码点

const flags = '🇨🇳🇯🇵' // 每个国旗是两个区域指示符字母
console.log([...flags].reverse().join('') === '🇵🇯🇳🇨') // 被重新配对成了别的组合

const cafe = 'cafe\u0301' // é 写成 e + 组合重音
console.log([...cafe].reverse()[0] === '\u0301') // 重音跑到了最前面
查看输出与解释
8 5
true
true

家庭 emoji 由三个人物 emoji 和两个零宽连接符 \u200D 组成,零宽连接符本身不显示,只负责把前后两个 emoji 粘成一个图形。按码点反转后,连接符和人物的顺序全乱了,显示成几个散开的人。 国旗更隐蔽。🇨🇳 其实是区域指示符 C 加 N,🇯🇵 是 J 加 P。四个字母整体反转后变成 P、J、N、C,浏览器会把它们两两配对显示,得到的不是 🇯🇵🇨🇳,而是两个别的组合。不报错、不乱码,但内容错了。 组合重音同理,\u0301 本应跟在 e 后面,反转后跑到了字符串开头,没有前一个字符可以叠加。

按字素簇反转:Intl.Segmenter

用户眼里的「一个字」在 Unicode 里叫字素簇(grapheme cluster),它有一套专门的切分规则。浏览器和 Node.js 都内置了 Intl.Segmenter,把 granularity 设成 'grapheme' 就是按这个规则切。

真正按用户看到的字符反转JavaScript
const family = '👨\u200D👩\u200D👧'
const segmenter = new Intl.Segmenter('zh', { granularity: 'grapheme' })

function reverseText(str) {
  // segment() 返回可迭代对象,每一项的 segment 属性是一个字素簇
  return Array.from(segmenter.segment(str), (s) => s.segment).reverse().join('')
}

console.log(reverseText('ab' + family) === family + 'ba') // 家庭 emoji 保持完整
console.log(reverseText('🇨🇳🇯🇵')) // 两个国旗整体交换位置
console.log(reverseText('cafe\u0301') === 'e\u0301fac') // 重音仍跟着 e
查看输出与解释
true
🇯🇵🇨🇳
true

segmenter 可以在函数外创建一次重复使用,每次调用都 new 一个没有必要。Intl.Segmenter 在主流浏览器的新版本里都已经支持,如果项目要兼容很老的环境,可以用遵循 Unicode 字素切分规则的第三方库兜底。

回答前,多想一步

容易答错的地方

以为 Array.from 就能处理所有 emoji
Array.from 只保证码点不被拆开。带肤色的手势、家庭、情侣、国旗这些 emoji 都是多码点组合,按码点反转照样会坏。
用 str.length 去截断或统计字数
反转的问题和截断、计数是同一个根源。按 length 截取昵称前 10 个字,可能刚好从一个 emoji 中间切开。统计「用户输入了几个字」也应该用字素簇的数量,而不是 length。
以为给正则加 u 可以解决
/./gu 按码点匹配,和 Array.from 的效果一样,对组合 emoji 无能为力。正则的 v 标志配合 \p{RGI_Emoji} 能匹配完整 emoji,但只管 emoji,不管组合重音,通用做法还是 Intl.Segmenter。
试着用自己的话回答

面试官还会怎么问?

乱码显示成 �,这个字符是什么?

它是替换字符 U+FFFD。当渲染器或解码器遇到不合法的编码,比如一个落单的代理项,就用它来占位。所以看到 �,基本可以判断是编码被从中间截断或者拆坏了。

不用 reverse(),手写一个反转怎么写?

可以从尾到头遍历拼接,但如果按下标从 str.length - 1 往前走,遇到的仍然是代码单元,问题一样。正确做法是先得到码点数组或字素簇数组,再倒着遍历。本质上,问题不在反转算法,而在「按什么单位拆」。

反转阿拉伯文或希伯来文有意义吗?

这类从右往左书写的文字,显示顺序由 Unicode 双向算法决定,存储顺序和显示顺序本来就不一样。简单地倒序字素簇,得到的文本在界面上未必是「反过来」的样子。实际业务里很少需要反转自然语言文本,面试官真正想确认的是你清不清楚字符串的编码单位。

从一道题,走向一组知识

把知识连起来

数据处理

为什么一个 emoji 的 length 是 2?怎么正确遍历字符串?

反转乱码的根源是 emoji 占两个代码单元,那一题讲清了代理对是怎么编码出来的。

函数与作用域

严格模式下为什么不能用 arguments.callee?递归该怎么写?

手写递归版字符串反转时,递归函数怎么引用自己,正好是 arguments.callee 被禁用后要解决的问题。

参考资料

  • JavaScript:数据处理参考资料
  • Intl.Segmenter

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

本题目录
  1. 先记住这个答案
  2. 为什么 split('') 会出乱码
  3. 按码点反转还不够的三种情况
  4. 按字素簇反转:Intl.Segmenter
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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