刚接手一个小程序项目时,我最常用的不是框架API,反而是字符串操作。商品名称要截断、后端返回的"苹果|香蕉|橘子"要拆成标签数组、下单备注要拼接、手机号要脱敏——这些都是字符串方法在处理。这篇内容围绕微信小程序JS字符串操作方法汇总来做一次系统梳理,把split、join、concat、charAt、substring这些高频方法逐个拆开讲透,再结合一个商品卡片的完整示例展示它们怎么配合使用。不管是刚入门小程序开发的新手,还是写了两年代码偶尔还会纠结substring和slice到底差在哪的开发者,都可以从里面拿到能直接落地的方案。
1. 字符串操作在小程序里的真实应用场景
1.1 为什么单独把字符串操作拎出来讲
很多人在小程序开发里习惯性搜索"怎么截取字符串""怎么拆分字符串",其实底层都是JavaScript原生能力,并没有小程序私有的API。但小程序和纯网页开发还是有一个明显区别:数据从后端到渲染层,中间经过setData,这个环节里字符串的操作频率非常高。
我在实际项目里遇到过的场景包括:
- 列表页的商品名称过长,需要截断成两行并加省略号。
- 后端返回的标签字段是"新品|热销|特惠",前端要拆成数组才能用wx:for渲染。
- 用户提交筛选条件时,多个选中值需要拼成一个纯字符串传给后端。
- 订单号、手机号脱敏,只保留头尾几位。
- 金额数字需要转成千分位展示,价格文本需要拼接人民币符号。
这些需求单独看都不难,串起来就会发现,字符串方法的选择和执行时机直接影响代码可读性和渲染性能。
1.2 一套方法族谱理清关系
第一次看字符串方法时,容易觉得名称相近、功能重叠。我给它们分了一下类,理解起来就简单多了。
| 方法 | 类型 | 作用 | 是否改变原值 |
|---|---|---|---|
| split | String → Array | 按分隔符切分成数组 | 否 |
| join | Array → String | 把数组合并成字符串 | 否 |
| concat | String / Array | 连接多个字符串或数组 | 否 |
| charAt | String → String | 按索引位置返回单个字符 | 否 |
| substring | String → String | 按区间截取字符串 | 否 |
| slice | String / Array | 按区间截取,支持负数参数 | 否 |
| indexOf | String → Number | 查找子串首次出现的位置 | 否 |
| replace | String → String | 替换指定内容 | 否 |
这里面最容易被忽视的一点是:split是字符串转数组,join是数组转字符串,二者天生就是一对。concat和substring则是纯字符串场景下的主力。charAt虽然用的频率没那么高,但遇到逐字符处理的场景时,比str[index]更安全。
2. split、join、concat、charAt、substring用法逐个拆解
2.1 split:把"苹果|香蕉|橘子"变成数组
split的语法:str.split(separator, limit)。第一个参数是分隔符,可以是字符串,也可以是正则表达式;第二个参数可选,用来限制返回数组的最大长度。
var str = '苹果|香蕉|橘子'; var list = str.split('|'); // 输出 ['苹果', '香蕉', '橘子'] var str2 = 'a,b,c,d,e'; var list2 = str2.split(',', 3); // 输出 ['a', 'b', 'c']在小程序里最常见的用法就是拆分后端返回的分隔字符串。我们的项目里遇到过接口返回的富文本图片地址,多个URL用分号连接,前端就得用split拆成数组再来做图片预览。
还有几个细节值得注意:如果分隔符传空字符串,整个字符串会被拆成一个个字符;如果字符串本身就是空字符串,split返回的是['']而不是[]。后面这个坑我在代码里踩过一次,用wx:for渲染拆分结果时,空数组和含有空字符串的数组表现完全不一样。
''.split(','); // 输出 [''] ''.split('|').length; // 输出 1判空条件必须写成arr.length > 0 && arr[0] !== '',否则容易混进一个空的渲染项。
2.2 join:把数组重新拼成"苹果-香蕉-橘子"
join和split正好是反向操作。array.join(separator)会把数组所有元素按指定分隔符拼成一个字符串,不传参数时默认用逗号。
var tags = ['新品', '热销', '特惠']; var str = tags.join('|'); // 输出 '新品|热销|特惠'在小程序里,我经常用join做条件回显。比如用户从多选组件里选择了三个筛选条件,提交时用join('|')传给后端;下次进入编辑页时再通过split拆分回数组,重新回填到组件里。这两个方法配合起来,数据的流转闭环就打通了。
join还有一个可读性很好的场景:拼提示消息。
var name = '某商品'; var count = 3; var unit = '件'; var message = [name, count + unit].join(','); // 输出 '某商品,3件'比用加号连续拼接字符串要清爽很多。多个元素需要组合时,数组形式的拼接也更方便维护。
2.3 concat:拼URL、拼提示语都能用
concat的语法是str.concat(str1, str2, ...),可以一次传入多个参数,按顺序拼接到原字符串后面。它和加号拼接很相似,但有一个好处:当你需要拼接的内容来自多个变量时,concat的意图更明确。
var baseUrl = 'https://example.com'; var path = '/api/product/detail'; var id = '123456'; var fullUrl = baseUrl.concat(path, '?id=', id);在小程序里,我常用的一个小场景是拼接图片完整地址。后端经常只返回相对路径,比如/storage/2025/06/01/abc.jpg,前端拿基础域名来拼接。代码写成这样后,后来接手的同事一眼就能看出是在拼URL。
还要提一句:concat是两栖方法,数组也有concat。数组版用来合并多个数组非常方便:
var arr1 = [1, 2]; var arr2 = [3, 4]; var result = arr1.concat(arr2, [5, 6]); // 输出 [1, 2, 3, 4, 5, 6]如果只是简单拼两三个字符串,用加号完全没问题;当一段代码里出现四五处加号时,换成concat或数组join会容易读很多。性能上,小程序处理的数据量远达不到需要担心的程度,可读性优先级更高。
2.4 charAt:按位置取字符,越界不报错
charAt的语法:str.charAt(index)。它返回指定索引位置的字符,索引从0开始。和直接通过str[index]访问相比,它有一个明显的安全优势:越界时返回空字符串,不返回undefined。
var str = 'ABCDEF'; str.charAt(0); // 输出 'A' str.charAt(2); // 输出 'C' str.charAt(999); // 输出 '' str[999]; // 输出 undefined在小程序里,charAt比较常见的是用在脱敏场景。比如身份证号、学号这类固定长度的业务编号,要保留第一位和最后一位。
var idCode = '330102199001011234'; var masked = idCode.charAt(0) + '********' + idCode.charAt(idCode.length - 1); // 输出 '3********4'使用charAt还有一个隐形好处:不会误修改原字符串,返回的是全新字符串值。这对小程序这种数据驱动视图的模式很友好,不需要担心副作用。
2.5 substring:按范围提取内容,注意参数自动交换
substring的语法:str.substring(indexStart, indexEnd)。返回从indexStart开始到indexEnd结束之间的字符,包含起始位置,不包含结束位置。和slice一样,只传一个参数时表示从该位置截到末尾。
var str = 'HelloWorld'; str.substring(0, 5); // 输出 'Hello' str.substring(5); // 输出 'World' str.substring(5, 0); // 输出 'Hello'最后一行是substring最容易让人困惑的地方:当起始参数大于结束参数时,substring会自动交换两个参数,按小的在前大的在后处理。而slice遇到相同情况会直接返回空字符串。这个差异项目里实测过,确实是真实行为。
var str = 'HelloWorld'; str.substring(5, 2); // 输出 'llo',等价于 substring(2, 5) str.slice(5, 2); // 输出 ''substring对负数的处理也和slice不一样。substring会把负数当0处理,而slice会从末尾倒着数。
var str = 'HelloWorld'; str.substring(-3); // 输出 'HelloWorld',负数变成0 str.slice(-3); // 输出 'rld',从倒数第3位开始截所以在做截断、提取后缀等场景时,我一般首选slice而不是substring,因为负数参数的语义更符合直觉。但标题里既然列出substring,就有必要把它的行为特征讲清楚,避免你换用时不习惯。
2.6 高频补充成员:slice、indexOf、replace
这三个方法不是标题里的主角,但和小程序开发绑定得很紧,顺便一起说一下,因为你迟早会碰到。
slice:截取类方法的进阶版。支持负数参数,可以很方便地从字符串末尾取内容。取文件名后缀时特别顺手:
var fileName = 'report_2025.pdf'; var ext = fileName.slice(fileName.lastIndexOf('.') + 1); // 输出 'pdf'indexOf:查找子串位置。找不到返回-1,这个判断是字符串处理的基本功。比如判断一个字符串里是否包含指定关键词:
var content = '商品已发货,快递单号已生成'; if (content.indexOf('已发货') > -1) { // 命中 }replace:替换字符串中的内容。它有一个很有意思的行为:当第一个参数传字符串时,只替换第一个匹配项;想要全部替换,必须传带全局标志g的正则。
var str = '2025-06-01'; str.replace('-', '/'); // 输出 '2025/06-01',只替换了第一处 str.replace(/-/g, '/'); // 输出 '2025/06/01',全部替换小程序里做日期格式标准化、富文本内容清洗时,replace正则用得非常多。相关经验后面会专门整理。
3. 实战:商品卡片里的字符串处理Demo
3.1 场景设定与数据处理流程
假设现在要做一个电商小程序首页的商品卡片。卡片需要展示四个信息:商品名称(超长截断)、标签列表(后端返回竖线分隔字符串)、价格(千分位展示)、销量(带单位换算)。
数据结构定成这样:
// 模拟后端返回 var goods = { rawName: '【限时特惠】新款轻薄笔记本电脑 2025旗舰款超长续航', rawTags: '新品|热销|特惠', price: 8899, salesCount: 12345 };处理流程上,我建议在onLoad或onShow阶段把这四个字段统一处理完,再一次性setData。不要在WXML里写一堆复杂逻辑,也不要在多个地方重复处理同一个字段。
3.2 页面JS里的核心实现
Page({ data: { goodsInfo: {} }, onLoad: function () { // 模拟从接口拿到的数据 var rawData = { rawName: '【限时特惠】新款轻薄笔记本电脑 2025旗舰款超长续航', rawTags: '新品|热销|特惠', price: 8899, salesCount: 12345 }; var goodsInfo = this.formatGoods(rawData); this.setData({ goodsInfo: goodsInfo }); }, formatGoods: function (raw) { var name = raw.rawName.replace(/【.*?】/g, ''); // 去掉【】说明 var displayName = name.length > 12 ? name.substring(0, 12) + '...' : name; var tagList = raw.rawTags.split('|'); var priceText = this.formatPrice(raw.price); var salesText = this.formatCount(raw.salesCount); return { displayName: displayName, tagList: tagList, priceText: priceText, salesText: salesText, rawName: raw.rawName }; }, formatPrice: function (price) { var intPart = String(parseInt(price, 10)); var intPartFormatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); return intPartFormatted + '.00'; }, formatCount: function (count) { if (count >= 10000) { var wan = count / 10000; return wan.toFixed(1) + '万'; } return String(count); }, onGoodsTap: function () { var name = this.data.goodsInfo.rawName; wx.showToast({ title: name.length > 10 ? name.substring(0, 10) : name, icon: 'none' }); } });这段代码里,replace加了正则 /【.*?】/g 去掉了商品名里的活动标记,substring控制长度并追加省略号,split把竖线字符串拆成标签数组,正则replace实现了数字千分位。操作集中在一个格式化函数里,页面模板只负责展示。
对应WXML片段:
<view class="goods-card" bindtap="onGoodsTap"> <view class="goods-name">{{goodsInfo.displayName}}</view> <view class="goods-tags"> <text wx:for="{{goodsInfo.tagList}}" wx:key="index" class="tag">#{{item}}</text> </view> <view class="goods-price">¥{{goodsInfo.priceText}}</view> <view class="goods-sales">已售{{goodsInfo.salesText}}</view> </view>3.3 WXS模块里的字符串处理
有些字符串处理无法在页面JS里做完,比如一个列表页里每个item都需要实时格式化,而数据已经通过setData传给了模板。这种情况下可以引入WXS,在WXML模板内部做处理。
WXS文件里的语法和JavaScript基本一致,但必坑点是不支持ES6语法,比如const、箭头函数都不能用,只能用var定义变量。
// utils/str.wxs var formatName = function (name, maxLen) { if (!name) { return ''; } var result = name.substring(0, maxLen); if (name.length > maxLen) { result += '...'; } return result; }; var splitTags = function (str, sep) { if (!str) { return []; } return str.split(sep); }; var maskPhone = function (phone) { if (!phone || phone.length !== 11) { return phone; } return phone.substring(0, 3) + '****' + phone.substring(7); }; module.exports = { formatName: formatName, splitTags: splitTags, maskPhone: maskPhone };WXML中这样引用:
<wxs src="../../utils/str.wxs" module="strUtil" /> <view>{{strUtil.formatName(item.name, 12)}}</view> <view> <text wx:for="{{strUtil.splitTags(item.tags, '|')}}" wx:key="index">{{item}}</text> </view> <view>{{strUtil.maskPhone(item.phone)}}</view>这套方案的适用场景是:列表数据已经在data里,且无法提前统一格式化,或者模板中多处需要相同规则格式化时。但要注意,WXS里的处理能力有限,正则表达式虽然支持但是用的全局对象是getRegExp,日常字符串方法够用了,别放太复杂的逻辑进去。
提示:WXS运行在视图层,和页面JS逻辑层是隔离的。如果在WXS里改数据,是不会同步回Page.data的。它的定位就是纯展示型格式化工具。
4. 小程序字符串操作高频踩坑与排查
4.1 五个藏得比较深的坑
第一个坑:substring和slice的参数混淆。我们已经聊过,substring会交换参数,slice不会。同一个字符串,substring(5, 2)和slice(5, 2)结果完全不同。排查问题是先确认自己用的是哪个方法,再看参数有没有写反。很多人觉得自己"明明截的是5到2",结果一看代码用的是substring,却被自动交换成了2到5。
第二个坑:split空字符串的行为。对空字符串执行''.split('|')得到的是[''],数组长度为1而不是0。做条件渲染之前,最好加一个真实长度判断,否则页面上会出现一个空白标签。接口返回空字符串的情况在前端开发里太常见了,必须预防。
第三个坑:用split('')处理emoji和生僻字。普通中文字符每个占一个UTF-16单元,split('')没问题。但emoji这种位于补充平面的字符会占两个单元,直接拆会得到乱码。
'😊'.length; // 输出 2 '😊'.split(''); // 输出 ['\ud83d', '\ude0a']处理这种字符串,要用Array.from或者扩展运算符:
var arr = Array.from('abc😊'); // 输出 ['a', 'b', 'c', '😊']但Array.from属于ES6特性,在WXS里用不了,只能在页面JS逻辑层处理。需要给这种代码做兼容时,最好在进入WXML之前就转换好。
第四个坑:replace只替换第一处。如果你用replace('-', '/')处理日期字符串,你会意外地发现只有第一个横杠变成了斜杠。小程序里格式化日期的需求很多,解决办法是使用正则表达式加全局标志。这个坑几乎每个月都会遇到一次。
第五个坑:对null或undefined直接调用字符串方法。接口返回的字段没有做判空,直接拿来做split或substring,会导致页面渲染时直接报错。我习惯在格式化函数开头做防御性判断:
if (!raw || typeof raw !== 'string') { return ''; }不要因为数据看起来总会有值就跳过这一步。线上环境某个字段为空后,报错非常隐蔽,排查起来很费时间。
4.2 问题排查速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 模板里调用toFixed报错 | WXML表达式不支持方法调用 | 在JS里处理完再setData,或使用WXS |
| 商品名截断出现半个emoji | substring或slice按UTF-16单元截取 | 用Array.from转数组后处理 |
| 空字符串拆完渲染出一个空元素 | split返回值是[''] | 判断arr[0]是否为空字符串 |
| 日期替换只换了一个横杠 | replace第一个参数传的是字符串 | 换成 /-/g 正则 |
| 标签数组里每个标签前后有空格 | 分隔符旁存在空白字符 | split后再用trim清理,或正则拆 |
| 数字转字符串后千分位正则无效 | 正则写错位置或小数点干扰 | 整数部分单独处理,小数部分保留 |
4.3 WXML里不能直接调用字符串方法
这是个常被新手踩的坑。WXML的{{}}表达式支持算术运算、字符串拼接、三元判断,但不支持方法调用。
<!-- 这样写是不会生效的 --> <view>{{goods.name.substring(0, 5)}}</view>正确的做法有两种:第一种是在Page.onLoad里用JS处理完,再把结果放进data;第二种是用WXS模块,在模板内部调用封装好的函数。
我个人建议:如果是单个页面内两三个字段的格式化,优先用第一种,代码直观好查问题;如果是多个页面复用同一套格式化规则,就封装WXS,保持模板简洁。
5. 值得封装的字符串工具函数与工程化建议
5.1 可以直接写进utils里的工具
项目里实际用下来,下面几个工具函数复用率最高。统一放在utils/string.js里,后面所有页面共用。
// 截断字符串并追加省略号 function truncate(str, maxLen) { if (!str) { return ''; } if (str.length <= maxLen) { return str; } return str.substring(0, maxLen) + '...'; } // 手机号中间四位脱敏 function maskPhone(phone) { if (!phone || phone.length !== 11) { return phone; } return phone.substring(0, 3) + '****' + phone.substring(7); } // 拆分竖线分隔字符串为标签数组 function splitTags(str, sep) { if (!str) { return []; } var sepStr = sep || '|'; var list = str.split(sepStr); var result = []; for (var i = 0; i < list.length; i++) { var item = list[i].replace(/^\s+|\s+$/g, ''); if (item !== '') { result.push(item); } } return result; } // 金额千分位格式化 function formatPrice(price) { if (typeof price === 'undefined' || price === null) { return '0.00'; } var str = String(price); if (str.indexOf('.') === -1) { str = str + '.00'; } var parts = str.split('.'); var intPart = parts[0]; var decimalPart = parts[1]; intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); return intPart + '.' + decimalPart; } // 日期补零 function padZero(num) { return num < 10 ? '0' + num : '' + num; }以splitTags为例,平时可能只想拆个字符串,但后端返回的标签经常带着不必要的空格,直接在工具函数里顺手处理掉,页面就再也不会出现" #新品 "这种难看的样式了。这种细节是普通字符串方法文档里不会写的。
5.2 处理字符串的执行时机建议
在小程序里,字符串处理的执行时机值得专门说一说。数据从接口返回之后,经过setData传到视图层,这个流程中处理字符串的位置有两种选择。
第一种是在setData之前统一处理。这种方案适合大部分场景,因为数据处理逻辑集中在一个地方,排查问题方便。缺点是一旦数据要在多个页面上以不同格式显示,就需要每个页面重复处理一遍。
第二种是在WXS里处理。这种方案优点是模板渲染时实时格式化,同一份数据在不同页面可以有不同的展示规则,不用污染原始数据。缺点是WXS能力受限,且调试时不好打断点,复杂逻辑难排查。
我的建议是:对于列表渲染这类高频场景,尽量在setData前处理完,减少视图层的运行负担;对于偶尔使用的展示格式化,用WXS更灵活。两种方案不冲突,一个项目里可以共存。
5.3 关于性能的一个实在建议
有些开发者喜欢在wx:for里面直接做字符串拼接,比如每个商品名称后面拼一个图标字符串。这样做在数量级较小时没问题,但列表一旦上百条,每一条都要执行一次字符串方法,视图层压力就会上来。
更稳妥的做法是在数据源清洗时就把最终展示文案计算好,模板里只渲染字段。哪怕多用一些存储空间,换来的渲染流畅度是值的。实测在小程序里渲染一个200条的列表,提前处理完字符串和边渲染边处理,体验差异很明显。
另外注意,setData本身承担了整个视图更新的成本。尽量不要在一个流程里多次setData,把需要更新的字段合并成一次提交。字符串处理只是其中一环,但配合好能明显优化启动速度。
结尾:我的一点实操体会
字符串方法本身没有太多门道,真正的挑战都藏在边界情况里:空字符串、越界索引、负数参数、emoji字符、WXML限制。我自己的习惯是,每遇到一种新情况就把它沉淀成一个工具函数放到utils里,同时把边界判断写进去。做的时间长了,工具库越来越大,后面接新功能时往往直接调用就行,再也不用来回百度"小程序字符串截断""JS split用法"这类基础问题了。
如果你在项目里也碰到过其他有意思的字符串操作坑,或者想到了更好的封装方式,欢迎在评论区分享。字符串处理看起来平凡,但做好了对项目体验的提升非常明显。