0

0

ES6中如何用字符串的codePointAt处理Unicode

幻夢星雲

幻夢星雲

发布时间:2025-07-10 21:53:02

|

507人浏览过

|

来源于php中文网

原创

传统的charcodeat方法在处理超出bmp的unicode字符(如表情符号或某些不常见汉字)时失效,因为它们由两个码元组成的代理对表示,而charcodeat只返回单个码元的值。1.codepointat能正确获取完整字符的unicode码点;2.它通过识别代理对,确保从字符层面进行准确操作;3.结合索引步长控制,可实现按字符精确遍历和截取,避免length、slice等方法因基于码元而导致的截断问题。

ES6中如何用字符串的codePointAt处理Unicode

在ES6中,codePointAt方法提供了一种可靠的方式来获取字符串中指定位置的Unicode码点,这对于正确处理那些由多个UTF-16码元组成的字符(即代理对)至关重要。它解决了传统charCodeAt方法在处理BMP(基本多语言平面)之外字符时的局限性,确保我们能以字符为单位进行精确操作。

ES6中如何用字符串的codePointAt处理Unicode

解决方案

在JavaScript的世界里,字符串的内部表示基于UTF-16,这意味着一个我们眼中的“字符”可能由一个或两个16位的码元(code unit)组成。对于那些在基本多语言平面(BMP,U+0000到U+FFFF)内的字符,一个码元就足够了。但当遇到像表情符号、一些不常见的汉字或历史文字时,它们位于BMP之外,就需要用一对特殊的16位码元,也就是所谓的“代理对”来表示。

String.prototype.codePointAt(index)就是为了解决这个痛点而生。它接收一个索引作为参数,并返回在该索引处开始的码点的十进制值。如果该索引指向的是一个代理对的起始部分,它会正确地返回整个码点的数值,而不是像charCodeAt那样只返回第一个码元的值。如果索引指向的是代理对的第二个码元,它会返回该码元的值。如果索引超出字符串范围,则返回undefined

ES6中如何用字符串的codePointAt处理Unicode
const str = '你好世界?'; // ? 是一个代理对,U+1F30D
console.log(str.length); // 7 (5个汉字 + 2个码元组成一个地球表情)

// 传统方法的问题
console.log(str.charCodeAt(5)); // 55356 (?的第一个码元)
console.log(str.charCodeAt(6)); // 57101 (?的第二个码元)

// codePointAt 的解决方案
console.log(str.codePointAt(5)); // 127757 (?的完整Unicode码点)
console.log(str.codePointAt(6)); // 57101 (如果从第二个码元开始,它会返回该码元的值)

const anotherStr = '?'; // 一个不常见的汉字,也是代理对 U+20BB7
console.log(anotherStr.length); // 2
console.log(anotherStr.charCodeAt(0)); // 55362
console.log(anotherStr.charCodeAt(1)); // 57271
console.log(anotherStr.codePointAt(0)); // 134071 (?的完整Unicode码点)

这基本上就是它的核心功能。它不复杂,但却解决了字符串处理中一个长期存在的隐患。

为什么在处理复杂Unicode字符时,传统的charCodeAt会失效?

这其实是JavaScript字符串内部表示机制的一个历史遗留问题。JavaScript在设计之初,字符串是基于UCS-2编码的,即每个字符占用16位。然而,随着Unicode字符集的不断扩充,尤其是当字符数量超过65536个(即超过了16位所能表示的范围)时,UCS-2就无法覆盖所有字符了。为了兼容性并扩展支持所有Unicode字符,JavaScript(以及UTF-16编码本身)引入了“代理对”(surrogate pairs)的概念。

ES6中如何用字符串的codePointAt处理Unicode

简单来说,那些码点大于U+FFFF的字符,会被拆分成两个16位的码元来存储。例如,一个表情符号 U+1F600 (?) 在内存中实际是两个UTF-16码元:0xD83D0xDE00

charCodeAt(index) 方法的设计初衷是返回指定索引处的单个16位码元的值。所以,当你对一个由代理对组成的字符使用 charCodeAt 时,它会分别返回这两个码元的值,而不是你期望的单个完整字符的码点。这就像你试图用两个独立的数字来表示一个完整的手机号码,而没有把它们看作一个整体。这在进行字符判断、比较或者迭代时,都会带来巨大的困扰。你无法直接判断 str.charCodeAt(i) 是否是一个完整的字符,因为一个“字符”可能需要两个 charCodeAt 的结果才能构成。这就是 charCodeAt 在处理复杂Unicode字符时“失效”的根本原因。它并没有错,只是它的设计目标和我们对“字符”的直观理解产生了偏差。

如何利用codePointAt实现对完整Unicode字符的精确遍历?

传统的 for 循环加上 charCodeAt 遍历字符串时,往往是按码元(code unit)进行的。这意味着如果你有一个包含代理对的字符串,你的循环可能会在代理对的中间断开,或者把一个字符当成两个来处理。

const text = '你好世界?,很高兴认识你!';

// 错误的遍历方式 (按码元遍历)
for (let i = 0; i < text.length; i++) {
    console.log(`索引 ${i}: 字符 "${text[i]}" (charCodeAt: ${text.charCodeAt(i)})`);
}
// 输出会显示?被拆成了两个字符和两个charCodeAt值。

// 使用 codePointAt 进行精确遍历
for (let i = 0; i < text.length; ) {
    const codePoint = text.codePointAt(i);
    // 将码点转换为字符,或者进行其他操作
    const char = String.fromCodePoint(codePoint);

    console.log(`索引 ${i}: 完整字符 "${char}" (codePoint: ${codePoint})`);

    // 根据码点是否为代理对,调整索引步长
    // 如果码点大于0xFFFF,说明是代理对,需要跳过两个码元
    // 否则,跳过一个码元
    i += (codePoint > 0xFFFF ? 2 : 1);
}

通过这种方式,我们确保每次迭代都获取并处理一个完整的Unicode字符,无论它是一个码元还是一个代理对。当然,ES6也引入了 for...of 循环,它在遍历字符串时,默认就是按Unicode码点(即完整字符)进行遍历的,这在很多情况下会更简洁方便:

星辰Agent
星辰Agent

科大讯飞推出的智能体Agent开发平台,助力开发者快速搭建生产级智能体

下载
// 更简洁的按字符遍历方式 (ES6 for...of)
for (const char of text) {
    console.log(`字符: "${char}"`);
    // 如果需要码点,可以再调用 codePointAt
    // console.log(`字符: "${char}" (codePoint: ${char.codePointAt(0)})`);
}

尽管 for...of 看起来更“傻瓜式”,但理解 codePointAt 的工作原理仍然至关重要。因为它在你需要精确控制索引、或者需要获取特定位置的码点进行数学运算、或者在实现某些低层字符串处理逻辑时,提供了不可替代的精确性。比如,当你需要手动实现一个字符串截取函数,但又要求它不能截断代理对时,codePointAt 就是你检查和调整截取边界的关键工具

在处理字符串长度或子串时,codePointAt如何帮助避免常见的陷阱?

这是另一个 codePointAt 真正闪光的地方,也是我们这些写代码的人经常踩坑的地方。JavaScript的 String.prototype.length 属性返回的是字符串中的UTF-16码元数量,而不是我们直观理解的“字符”数量。同样,String.prototype.slice()substring()substr() 等方法也是基于码元索引进行操作的。这意味着如果你有一个包含代理对的字符串,这些方法可能会在代理对的中间“切开”一个字符,导致得到一个损坏的字符或不正确的长度。

举个例子:

const emojiString = 'Hello?World';
console.log(emojiString.length); // 12 (Hello:5 + ?:2 + World:5)
// 直观上我们可能认为有11个字符,但length是12

// 尝试截取最后一个字符
console.log(emojiString.slice(10)); // "ld" (?被截断了,因为?在索引5和6,而索引10和11是ld)
console.log(emojiString.slice(5, 7)); // "?" (这个碰巧是完整的,因为从5开始,正好包含两个码元)
console.log(emojiString.slice(5, 6)); // "�" 或乱码 (截断了?的第一个码元)

这种行为在处理用户输入、限制文本长度、或者在UI上展示字符串时,会造成视觉上的混乱和数据上的错误。codePointAt 无法直接改变 lengthslice 的行为,但它提供了一种工具,让我们能够构建出正确处理这些情况的逻辑。

例如,要获取真正的“字符”数量,你需要结合 codePointAt 进行遍历:

function getTrueCharLength(str) {
    let count = 0;
    for (let i = 0; i < str.length; ) {
        str.codePointAt(i); // 只是为了触发跳过代理对的逻辑
        count++;
        i += (str.codePointAt(i) > 0xFFFF ? 2 : 1);
    }
    return count;
}

console.log(getTrueCharLength(emojiString)); // 11

或者更简单地,利用 Array.from()[...str] 将字符串转换为码点数组:

console.log([...emojiString].length); // 11

对于子字符串的截取,如果需要按“字符”而不是“码元”来截取,你同样需要手动构建逻辑,或者利用更高级的API。codePointAt 让你能够判断在哪个索引位置开始或结束截取,以确保不会截断代理对。例如,如果你想从字符串中提取前N个字符(而不是前N个码元),你不能简单地用 slice(0, N)。你需要遍历,直到收集到N个完整的字符,并记录下最终的码元索引:

function sliceByChars(str, startCharIdx, endCharIdx) {
    let currentCodeUnitIdx = 0;
    let currentCharIdx = 0;
    let startIndex = -1;
    let endIndex = -1;

    for (let i = 0; i < str.length; ) {
        const codePoint = str.codePointAt(i);

        if (currentCharIdx === startCharIdx) {
            startIndex = i;
        }
        if (currentCharIdx === endCharIdx) {
            endIndex = i;
            break; // 找到结束位置就停止
        }

        i += (codePoint > 0xFFFF ? 2 : 1);
        currentCharIdx++;
    }

    if (startIndex === -1) startIndex = 0; // 如果startCharIdx超出了,就从头开始
    if (endIndex === -1) endIndex = str.length; // 如果endCharIdx超出了,就到字符串末尾

    return str.substring(startIndex, endIndex);
}

console.log(sliceByChars(emojiString, 0, 6)); // "Hello?" (前6个字符)
console.log(sliceByChars(emojiString, 6, 11)); // "World" (从第6个字符开始到结束)

虽然这个 sliceByChars 函数看起来有点复杂,但它正是 codePointAt 在实际应用中提供精确控制的体现。它帮助我们从“码元”的思维模式中解脱出来,真正以“字符”为单位来思考和操作字符串,从而避免那些因为Unicode复杂性而产生的常见陷阱。在ES2018中引入的 Intl.Segmenter API,则提供了更强大和语义化的方式来处理字符串的各种分割(如按字符、按词、按句子),但其底层原理依然离不开对Unicode码点的正确识别。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

557

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

394

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

755

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

478

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

454

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1051

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

554

2023.09.20

Golang 性能分析与pprof调优实战
Golang 性能分析与pprof调优实战

本专题系统讲解 Golang 应用的性能分析与调优方法,重点覆盖 pprof 的使用方式,包括 CPU、内存、阻塞与 goroutine 分析,火焰图解读,常见性能瓶颈定位思路,以及在真实项目中进行针对性优化的实践技巧。通过案例讲解,帮助开发者掌握 用数据驱动的方式持续提升 Go 程序性能与稳定性。

6

2026.01.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
ECMAScript6 / ES6---十天技能课堂
ECMAScript6 / ES6---十天技能课堂

共25课时 | 1.9万人学习

HTML5/CSS3/JavaScript/ES6入门课程
HTML5/CSS3/JavaScript/ES6入门课程

共102课时 | 6.8万人学习

HTML+CSS基础与实战
HTML+CSS基础与实战

共132课时 | 9.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号