在 HarmonyOS(ArkTS)开发里,字符串这个基础类型,真的是又爱又恨。表面上看,谁不会写let str: string = "hello",可真到上手做项目,从 TextInput 输入校验、网络参数拼接、JSON 解析,到列表搜索、中文排序、emoji 截断,几乎每一个能让你加班到深夜的 bug,背后都站着字符串处理。说它是“全栈”级别的基本功,一点都不夸张,因为 UI 层、逻辑层、数据层,处处都有它的影子。
这篇手册是我在自己的 HarmonyOS 应用开发过程中沉淀下来的字符串实战笔记,整理成了一套可以直接照着用的方案:先讲高频 API 的调用姿势和坑,再把转换、格式化、校验、序列化这些常见场景串起来,最后附上性能优化和排查问题的经验。适合刚开始接触 ArkTS 的开发者,也适合从 Web/小程序转过来、被字符串 API 差异坑过的人。你不需要一次性读完,把它当成工具书,遇到问题回来翻对应章节就行。
1. 别小看字符串:HarmonyOS 里它比你想的更“全栈”
我一开始做 HarmonyOS 开发时,总觉得字符串就是length、substring、indexOf那三板斧,和别的语言没区别。直到我接手一个业务模块,要同时处理输入框内容、本地缓存、网络请求参数和列表展示,才发现字符串这层基本功如果不够扎实,会在每一个环节拖你后腿。
举几个实际场景:
- TextInput 组件拿到的永远是字符串,哪怕你只想要数字,也得先过一层转换和校验。
- 网络请求体里的 JSON、表单数据,本质上是把对象、数组、布尔值“拼接”成字符串,再在另一端“拆开”。这个过程中只要有一处转义没处理好,整个接口就挂了。
- 列表搜索、关键词高亮、敏感信息脱敏,你输入的是字符串,输出的是新字符串,中间还要考虑正则、编码、大小写。
- 日志打印、错误提示、埋点上报,看起来不起眼,但字符串切分或拼接出错,直接导致线上问题无法定位。
所以我把字符串称为“全栈”,不是说你要用字符串去写后端,而是说字符串贯穿了一款 App 的完整链路:展示层靠它,逻辑层靠它,数据交互也靠它。尤其是 ArkTS 这种基于 TypeScript 语言、带严格类型约束的开发环境,字符串的应用姿势和纯 JavaScript 或者传统 Java 都有差异,如果不提前摸清规律,后面排查问题会很痛苦。
我这篇手册不会只罗列 API,而是会把每一个操作放在真实的业务场景里讲,告诉你为什么这么写、有什么副作用、替代方案是什么。读完你再回来看项目里的字符串处理代码,应该会有一种“原来这里可以这么写”的顿悟感。
2. 高频 API 盘点:这些方法我天天用,但踩坑也最多
2.1 长度、截取、拼接的基础认知
字符串求长度,第一反应是str.length。在 ArkTS 里,这个属性确实好用,但要注意它数的是 UTF-16 编码单元,而不是“用户看到的字符个数”。如果你处理的文本里有 emoji、生僻汉字或者特殊符号,length可能会比你预期的大。
let emoji = "😀"; console.log(emoji.length); // 输出 2,因为它用两个 UTF-16 编码单元表示 let chinese = "汉"; console.log(chinese.length); // 输出 1,常见汉字一个编码单元就够了截取字符串时,我推荐优先用substring和slice。它俩的差别很细,但在实战中容易踩坑:
substring(start, end):如果start大于end,会自动交换这两个参数。它不接受负数,负数会被当成 0。slice(start, end):不会交换参数,但支持负数,表示从末尾往前数。
let text = "HarmonyOS"; console.log(text.substring(1, 3)); // "ar" console.log(text.slice(1, 3)); // "ar" console.log(text.substring(3, 1)); // "ar",参数被交换了 console.log(text.slice(3, 1)); // "",参数不交换,范围为空 console.log(text.slice(-3)); // "iOS",从末尾取三位拼接字符串,我见过不少str = str + "xxx"的写法,简单场景没问题,但高频循环里不建议这么干。原因放到第 5 节细说,这里先记住一个原则:少量拼接随便用,批量拼接时考虑数组join或者模板字符串。
模板字符串是 ArkTS/TypeScript 里我很推荐的方式,它最大的优势是可读性。比起一堆加号和引号,模板字符串能把变量和静态文本混排得明明白白:
let name = "HarmonyOS"; let version = 5; let desc = `当前系统:${name},版本:${version}`;2.2 大小写转换、去空格、比较:三个最容易想当然的操作
字符串大小写转换没什么好说的,toUpperCase()和toLowerCase()是基础。但要注意,有些语言环境下的特殊字符转换结果可能和你预期不一样。比如土耳其语环境下,"I".toLowerCase()的结果不是"i"而是"ı"(不带点的 i)。HarmonyOS 应用面向全球用户时,这个细节会引发隐藏的 bug,尤其是做搜索功能时,大小写归一化一定要谨慎。
去空格这块,ArkTS 提供了trim()、trimStart()、trimEnd()。trim()去掉开头和结尾的空白字符,不会去掉字符串内部的空格。我遇到过新手把trim()当成“去掉所有空格”来用,结果数据里中间的空格纹丝不动。想去掉内部所有空格,得用replaceAll(" ", ""),注意它只会消除半角空格,全角空格\u3000要用正则/\s/g才能覆盖。
let input = " HarmonyOS 开发 "; console.log(input.trim()); // "HarmonyOS 开发" console.log(input.replaceAll(" ", "")); // "HarmonyOS开发" console.log(input.replace(/\s/g, "")); // "HarmonyOS开发",包含全角空格场景更保险比较字符串是否相等,新手最容易直接写==。这在 ArkTS 里对字符串类型是有效的,因为它是值类型比较,不像某些语言里对象引用比较会有坑。但如果你要比较的字符串可能来自不同来源,比如一个是string类型,另一个是String对象,那还是建议用===严格相等,避免类型转换引发意外。
如果要忽略大小写比较,别自己写str1.toLowerCase() === str2.toLowerCase(),性能不好,而且一旦某个字符toLowerCase()有特殊行为就容易出错。更稳的做法是用toLocaleLowerCase()配合localeCompare,或者干脆在输入阶段就统一把所有内容trim()+toLowerCase(),后续比较就简单了。
2.3 分割与遍历:从 split 到 for...of
split应该是使用频率极高的 API,把字符串按分隔符拆成数组。注意两点:
第一,分隔符如果是字符串字面量,它按完全匹配切割;如果是正则,可以按一组字符切割。
let csv = "apple,orange;banana"; console.log(csv.split(/[,;]/)); // ["apple", "orange", "banana"]第二,split("")会把字符串按 UTF-16 编码单元拆开,对于 emoji 这类双编码单元字符,拆完就变成乱码了。想按“用户感知字符”拆,更稳的方式是用Array.from(str)或者for...of循环:
let emojiStr = "a😀b"; console.log(emojiStr.split("")); // ["a", "\uD83D", "\uDE00", "b"],乱码了 console.log(Array.from(emojiStr)); // ["a", "😀", "b"],正确遍历字符串,我推荐for...of,它能正确处理四字节字符。下标遍历for (let i = 0; i < str.length; i++)遇到 emoji 会读到无效的半个字符,做回文判断、字符统计这类操作时,结果天差地别。
2.4 查找与替换:indexOf、includes、replaceAll 的选择
判断一个字符串是否包含另一个,includes()最直观。需要知道位置时用indexOf()。需要统计出现次数时,可以配合split的奇妙用法:
let sentence = "HarmonyOS is great, HarmonyOS is powerful"; let count = sentence.split("HarmonyOS").length - 1; console.log(count); // 2替换字符串时,replace()默认只替换第一个匹配项;如果要替换所有,用replaceAll(),或者给replace()传正则并加g标志。这是老掉牙的坑了,但我几乎每周都在代码评审里看到:
let text = "2024-01-01 2024-02-01"; console.log(text.replace("-", "/")); // "2024/01-01 2024/02-01" console.log(text.replaceAll("-", "/")); // "2024/01/01 2024/02/01" console.log(text.replace(/-/g, "/")); // "2024/01/01 2024/02/01"正则还牵扯到转义问题。比如你要把[123]这个字符串里的方括号替换成圆括号,如果直接写replace("[", "(")没问题,但一旦你用的是replace(/[/g, "("),会直接报错,因为/在正则里是分隔符,方括号是元字符。更常见的坑是用户输入里本来就含正则元字符,你想做“把用户输入的 abc 替换成 def”,就需要先把用户输入转义成安全文本。
3. 转换全家桶:数字、数组、JSON 一张表说清
3.1 字符串转数字:光有 Number 还不够
从输入框拿到"123",要变成数字 123,你有几个选择:
Number("123"):最常用,空字符串转成 0,带非法字符返回NaN。parseInt("123abc"):解析到第一个非法字符为止,返回 123。注意它会把"0x10"识别成十六进制,但不同环境下行为可能不一致,建议总是显式传进制参数parseInt(str, 10)。parseFloat("3.14abc"):解析浮点数,到第一个非法字符为止。- 一元加号
+"123":效果类似Number(),适合写紧凑代码。
我给出的建议是:表单输入验证场景,先用trim()清理空白,再用正则校验格式,最后才转数字,这样能避免很多NaN进入计算流程。
let userInput = " 42 "; let trimmed = userInput.trim(); if (/^\d+$/.test(trimmed)) { let num = Number(trimmed); console.log(num); // 42 }3.2 数组与字符串互相转换:split 和 join 的完美对称
数组转字符串用join,字符串转数组用split。这组操作是我做标签系统、批量筛选、日志埋点时最常用的。
let tags = ["HarmonyOS", "ArkTS", "实战"]; let tagStr = tags.join(","); // "HarmonyOS,ArkTS,实战" let tagList = tagStr.split(","); // ["HarmonyOS", "ArkTS", "实战"]join还可以指定空字符串,直接把数组拼成一个连续字符串。这在生成随机验证码、拼接图片 URL 列表时非常有用:
let chars = ["A", "B", "C", "D"]; let code = chars.join(""); // "ABCD"3.3 对象与 JSON 序列化:别把 JSON.stringify 当万能胶
HarmonyOS 应用和服务器通信,JSON 基本是标配。JSON.stringify可以把对象转成 JSON 字符串,JSON.parse可以把字符串转回对象。但有几个细节,非踩坑不能体会:
第一,JSON.stringify会忽略值为undefined的函数字段、Symbol 字段,遇到循环引用会直接抛错。如果你序列化的对象里有Map、Set、Date,默认结果可能不是你想要的,需要先给它一个toJSON方法或做预处理。
第二,JSON.parse对非法 JSON 字符串会直接抛异常。从网络返回的字符串,务必放在try...catch里解析,否则一个后端多余的空格或转义逗号,就能让你的 App 白屏。
let raw = '{"name":"HarmonyOS","version":5}'; try { let obj = JSON.parse(raw); console.log(obj.name); } catch (e) { console.error("解析失败", e); }第三,对象转 JSON 字符串时,中文会被默认原样保留,不会做 Unicode 转义。这没问题,网络传输和服务器都能正常处理。但如果你要把它放进 URL 参数或某些老旧网关,就需要注意encodeURIComponent的配合使用。
3.4 URL 编码、Base64、Unicode 转义:容易被忽视的一层
在 HarmonyOS 开发中,网络请求地址、WebView 参数、分享链接都经常需要做 URL 编码。encodeURIComponent和decodeURIComponent是最常用的,但要注意它和encodeURI的区别:
encodeURI不会编码:/?#[]@等 URL 结构符,适合直接编码整个 URL。encodeURIComponent会编码几乎所有非字母数字的字符,适合编码参数值。
let keyword = "HarmonyOS 开发"; let url = `https://example.com/search?q=${encodeURIComponent(keyword)}`; console.log(url); // q=HarmonyOS%20%E5%BC%80%E5%8F%91Base64 编码在图片上传、扫码、签名场景里很常见。HarmonyOS 里有util.Base64Helper可以处理,具体 API 版本有差异,使用时记得查一下当前 SDK 的包路径。编码后的字符串可能包含+、/、=等字符,放进 URL 时同样要编码,不然会被截断或转义。
Unicode 转义在写国际化资源时很常见。\uXXXX表示一个 UTF-16 编码单元,String.fromCharCode和charCodeAt可以互相转换。遇到需要把中文变成\u5f00\u53d1这样的格式时,可以用循环实现,但注意四字节字符要拆成两个代理对处理。
4. 实战案例:一个注册页面的字符串全流程
理论讲多了容易飘,我来拆一个真实存在的场景:注册页面。这个页面上有用户名输入框、手机号输入框、验证码输入框和一个提交按钮。用户填完点提交,前端要校验输入、组装请求体、解析返回结果,再把结果展示到界面上。整个过程几乎把字符串操作全走了一遍。
4.1 输入校验:长度、字符集、正则
用户名校验规则一般是:长度 3~20 个字符,只能包含字母、数字、下划线。手机号校验规则按国内来说是一段 11 位数字。这里有几个容易掉进去的坑:
- 校验长度之前,一定要
trim(),否则用户输入前后空格,长度检测会误判。 - 中文用户名的长度计算别用
length,因为汉字在 JS/TS 里是一个编码单元,可用户感知上是“一个字符”,如果你要按字节数限制,逻辑又不一样。 - 正则里的
\d只匹配 ASCII 数字,某些语言环境可能会匹配其他数字字符,为了保险,手机号场景直接用[0-9]。
function validateUsername(name: string): boolean { const trimmed = name.trim(); if (trimmed.length < 3 || trimmed.length > 20) { return false; } return /^[a-zA-Z0-9_]+$/.test(trimmed); } function validatePhone(phone: string): boolean { const trimmed = phone.trim(); return /^1[3-9]\d{9}$/.test(trimmed); }这里我强烈建议使用“先 trim 后校验”的原则,既保证用户输入不被误杀,又能统一后续传给后端的数据格式。
4.2 格式化:手机号脱敏、银行卡分段
注册功能一般用不上脱敏,但“手机号脱敏展示”是很多 App 个人信息页的标配。比如把13812345678显示成138****5678。字符串截取加拼接就能完成:
function maskPhone(phone: string): string { const cleaned = phone.replace(/\s/g, ""); if (cleaned.length !== 11) { return phone; } return cleaned.slice(0, 3) + "****" + cleaned.slice(7); }如果要做银行卡号或信用卡号分段展示,比如每 4 位加一个空格,可以用正则一行搞定:
let cardNo = "6222021234567890"; let formatted = cardNo.replace(/(\d{4})(?=\d)/g, "$1 "); console.log(formatted); // "6222 0212 3456 7890"这种“格式化”操作的共同点是:先清理原始字符串里的杂质(空格、横线),再按规则重新拼接。顺序反了,结果就会乱。
4.3 与 UI 组件对接:TextInput 的 onChange 和 Text 显示
HarmonyOS 里,TextInput 组件通过onChange回调把用户输入的内容返回给你,类型就是字符串。做受控组件或者非受控处理时,每次变更都可能触发重绘,所以高频操作要注意性能。
我处理输入时的习惯是:
- 用
onChange((value: string) => { ... })拿到最新字符串,先做必要的trim或格式限制。 - 如果要做“只允许输入数字”这种限制,不要等到提交时才校验,而是在
onChange里直接过滤非法字符,把新的字符串回填给组件。 - 如果要做“防抖搜索”,那么回调函数里收集字符串后,用定时器或
setTimeout延迟触发后续逻辑,避免每次按键都立即发起网络请求。
@State username: string = ''; onChange = (value: string) => { // 只保留字母、数字、下划线 let filtered = value.replace(/[^a-zA-Z0-9_]/g, ""); this.username = filtered; };展示层用Text组件渲染时,字符串里如果包含换行,可以用\n,但注意不同设备的字体渲染宽度不同,长文本最好让组件自动换行,别自己截断,除非你明确知道需求。
4.4 网络请求中的字符串处理:组装表单、JSON 解析
注册接口通常要传一个 JSON 对象,里面包含用户名、手机号、验证码。调用网络前,组装 JSON 字符串是必经之路:
let payload = { username: this.username, phone: this.phone, code: this.code, }; let requestBody = JSON.stringify(payload);这一步看起来简单,但有几个注意点:
this.username里的值一定要经过校验,否则后端可能收到不可控字符,引发安全问题。- 如果传
FormData表单,那么就是一堆key=value用&拼接,需要用到encodeURIComponent对每个值编码,否则特殊字符会破坏请求格式。 - 服务端返回的响应体是字符串,要先
JSON.parse。解析前最好判断一下字符串是否为空或者是否为"null",别指望所有后端都规范返回 JSON。
解析响应后,前端经常要更新界面上的提示文案。比如“注册成功”或“手机号已存在”,这些字符串在业务代码里反复出现,最好抽成常量,避免拼写错误。
5. 性能与内存:字符串操作没你想的那么廉价
5.1 不可变性:拼接和替换为什么会“变慢”
字符串在 ArkTS/JavaScript/TypeScript 里是不可变的。这意味着你对一个字符串做+=、replace、substring等操作时,并不会修改原来的字符串,而是生成一个全新的字符串对象,旧字符串等待垃圾回收。
低频场景无感,但如果你在一个for循环里拼接 5000 次字符串,每次操作都会创建新对象,内存和 CPU 开销迅速上升。这就像你在一张纸上写字,每写一个字就换一张新纸,最后虽然字全写对了,纸却浪费了一大堆。
我在项目里做过一个简单基准测试:用+=拼接 10000 个短字符串,耗时约几十毫秒;用数组push加join拼接同样内容,耗时只有几毫秒甚至更少。数据量越大,差距越明显。
5.2 高频拼接:数组 join 还是模板字符串
批量拼接字符串时,我推荐用数组收集再join:
let parts: string[] = []; for (let i = 0; i < 10000; i++) { parts.push(`item-${i}`); } let result = parts.join(",");模板字符串在少量拼接时最优雅,可读性好,但不要在巨大的循环里使用嵌套模板字符串,因为每次迭代都涉及解析和拼接。
还有一点,HarmonyOS 应用运行在设备上,性能瓶颈比 PC 上更明显。如果你处理的是来自文件、网络日志或数据库的大段文本,比如几十 KB 的日志字符串,频繁切割、查找、替换会导致明显的卡顿。这种场景下,优先用indexOf定位,再用slice截取,避免使用大量正则。
5.3 大文本的裁剪、截断、懒加载
列表页展示长文本时,经常需要做“超过 N 字显示省略号”。一个简单的截断函数:
function truncate(text: string, maxLen: number): string { if (text.length <= maxLen) { return text; } return text.slice(0, maxLen) + "..."; }但这里有个问题:如果你按length截断,遇到 emoji 或特殊字符,截出来可能是一个不完整的字符,显示成乱码。更稳的方案是用Array.from(text)转成字符数组,按字符个数截断,或者用Intl.Segmenter(如果运行环境支持)做更精准的文本分割。
处理超大文本(比如文件内容)时,别一次性加载到字符串里再截取,可以分段读取、按行处理,只保留需要的部分。HarmonyOS 的文件读取 API 支持设置读取大小,配合字符串的split("\n")可以逐行分析,内存占用会友好很多。
5.4 StringBuilder 等替代方案:ArkTS 中怎么办
做过 Java 开发的人可能第一反应是找StringBuilder。ArkTS/TypeScript 标准库里没有这个类,但你可以自己封装一个轻量版本:
class StringBuilder { private parts: string[] = []; append(str: string): StringBuilder { this.parts.push(str); return this; } toString(): string { return this.parts.join(""); } }用起来就是:
let sb = new StringBuilder(); sb.append("Hello"); sb.append(" "); sb.append("HarmonyOS"); console.log(sb.toString()); // "Hello HarmonyOS"本质上是把多次拼接变成一次join,底层原理和我前面说的数组拼接一致。如果你嫌封装麻烦,直接用数组也行。关键是理解“减少中间字符串对象创建”这个思路。
6. 常见 Bug 与排查技巧实录
6.1 中文、emoji、四字节字符的 length 陷阱
前面反复提到 emoji 问题,我再强调一遍。length属性统计的是 UTF-16 编码单元数量,而消费者视角的“字符数”更接近“码点”数量。做字数限制、输入框字数统计、评论区楼层展示时,这两种统计的差异非常致命。
解决方案有三种:
- 用
Array.from(str).length按码点统计,简单直接。 - 用
for...of循环计数。 - 使用
Intl.Segmenter(如果目标设备支持)做更细粒度的文本分割。
我自己的经验是:绝大多数业务场景用Array.from就够了,性能可接受,代码也简单。
6.2 TextInput 值里的不可见字符:空格、换行、零宽字符
用户从网页或聊天软件复制内容粘贴到输入框时,常常会带上不可见字符。最常见的是普通空格、全角空格、换行符,偶尔还会出现零宽空格(U+200B)。这些字符用肉眼根本看不出来,但会导致字符串比较失败、正则校验不通过、长度统计超限。
排查技巧:遇到“数据看着一样,但程序说不一样”的情况,先把字符串的每个字符的 Unicode 码点打印出来:
let s = "abc"; for (let ch of s) { console.log(ch.codePointAt(0).toString(16)); }如果看到200b、3000这类“幽灵字符”,就知道问题在哪了。解决方式是清洗输入:去掉零宽字符、统一空格类型,再做后续处理。HarmonyOS 的 TextInput 组件也可以设置showErrorText的时机把握准确,但更实际的做法是在数据源头做清洗。
6.3 类型转换:NaN 和 undefined 的来源
parseInt("")返回NaN,Number(null)返回 0,Number(undefined)返回NaN。这三个结果差异,足以让一个表单校验逻辑全线崩溃。我来列一张速查表,你迟早用得上:
| 输入值 | parseInt(x, 10) | Number(x) | +x |
|---|---|---|---|
"123" | 123 | 123 | 123 |
"" | NaN | 0 | 0 |
" " | NaN | 0 | 0 |
"12abc" | 12 | NaN | NaN |
null | NaN | 0 | 0 |
undefined | NaN | NaN | NaN |
"0x10" | 16 | 16 | 16 |
"3.14" | 3 | 3.14 | 3.14 |
我的习惯是:如果数据可能为null或空,先统一做字符串化,但要注意String(null)会变成"null",String(undefined)变成"undefined"。真正稳妥的写法是提前做空值判断,别把脏数据交给转换函数。
6.4 正则转义:想匹配点和方括号,请先学会说谎
正则里.表示任意字符,[是字符集的开始,*、+、?都有特殊含义。如果你要按字面意义匹配这些符号,必须转义。很多新手在字符串里写"."来匹配小数点,结果把"a1b2"也匹配了,因为.可以匹配任意字符。
更常见的是动态正则。比如用户输入一个关键词,你想用它做正则搜索,就必须先对关键词转义:
function escapeRegExp(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }这样用户输入的[abc]才会被当作普通字符串去匹配,而不是字符集。这个函数我放在工具类里,几乎每个项目都会用到。
6.5 与后端交互时字符串编码不一致
HarmonyOS 应用请求后端接口时,如果后端接口返回的中文乱码,问题通常出在字符编码上。HTTP 请求头里的Content-Type要带上charset=utf-8,服务端也要按 UTF-8 返回。如果涉及到 URL 参数拼接,一律用encodeURIComponent处理,不要直接拼接中文。
本地存储场景中,读写 Preference 或文件时,也建议统一 UTF-8 编码。HarmonyOS 提供了一些转码工具,具体 API 可以查官方文档,但核心原则只有一条:全局统一字符编码,不要在不同模块之间来回转码。
结语:字符串这点事,值得多花时间磨
字符串操作在 HarmonyOS 开发里看着不起眼,实际却是最影响开发效率的细节之一。我自己的体会是:与其在网上搜零散的报错解决方案,不如花半天时间把字符串的 API、转换、正则、编码、性能这几条线彻底过一遍,后面能省下大量排错时间。
如果你现在正在做一个和输入、展示、网络请求强相关的功能,我建议你拿这个小册子里的代码片段当模板,先跑通再优化。尤其是trim()和encodeURIComponent这两个函数,用好了能避免一大半字符相关的诡异 bug。
后续如果你想把字符串处理能力再往上提一层,可以研究一下Intl这个内置对象,它对多语言环境下的字符串比较、日期格式化、数字格式化支持得很好。另一个方向是正则表达式的性能优化,复杂正则写不好,在小内存设备上能卡到让人崩溃。
这篇文章算是我个人项目里的一个阶段性沉淀,代码片段都是实际跑过的,但设备型号和 SDK 版本不同,API 细节可能会有微调。你照着写的时候如果遇到编译报错,优先查一下当前项目的 SDK 版本和官方 API 变更日志。