前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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字符串、对象、数组常用方法速查

首页2018-02-23 15:10:12Front-End
JavaScriptAPI字符串对象

写一个手机号脱敏的小函数,我先用了 substr,同事 review 说这个方法在 MDN 上标着「已废弃」,让我换成 slice。换完发现负数索引的行为不太一样,又跑去试 substring,结果传负数直接被当成 0 处理了。三个名字长得像的方法,行为各不相同,这种事情不查文档真记不住。

这篇把字符串截取的三兄弟、对象的几种遍历方式一次性对齐讲清楚。数组方法那部分做成索引,展开的内容我放在另一篇里,避免重复。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • slice、substr、substring 三个截取方法到底差在哪,该用哪个
  • 字符串还有哪些高频方法,以及 2018 之后新增的那几个
  • 数组常用方法的索引表,一行一个,方便速查
  • for...in 和 for...of 各自能遍历什么、不能遍历什么
  • 对象属性的枚举顺序规则,那个「整数键会被排序」的坑
  • Object.keys、entries、fromEntries 这一套静态方法怎么组合用

# 一、字符串截取的三兄弟

# 1.1 slice

stringObject.slice(start, end)

var a = 'Hello world!';
var b = a.slice(2);
var c = a.slice(-4, -2);
// a: 'Hello world!'
// b: 'llo world!'
// c: 'rl',参数可为负
@前端进阶之旅: 代码已经复制到剪贴板

slice 取的是 [start, end) 这个左闭右开区间。负数参数会自动加上字符串长度,-4 就是倒数第四个字符。a.slice(-4, -2) 从倒数第四位取到倒数第三位,得到 'rl'。

如果 start 比 end 大,slice 不做任何补救,直接返回空字符串。

# 1.2 substr

stringObject.substr(start, length)

var a = 'Hello world!';
var b = a.substr(0, 4);
var c = a.substr(-5, 2);
// a: 'Hello world!'
// b: 'Hell'
// c: 'or',参数可为负
@前端进阶之旅: 代码已经复制到剪贴板

substr 是唯一一个第二个参数表示「长度」而不是「结束位置」的。这也是最容易记混的一点,a.substr(0, 4) 是取 4 个字符,a.slice(0, 4) 是取到下标 4 之前。碰巧这两个在起点为 0 时结果相同,换个起点就分道扬镳了。

这里得补一句现状。String.prototype.substr 现在被放在 ECMAScript 规范的 Annex B 里,也就是「为了兼容存量网页保留的遗留特性」。它不会被移除,各家浏览器还会一直支持,但 MDN 上明确标了 deprecated,新代码里不建议再写。要按长度截取,用 slice(start, start + length) 表达同一件事。

# 1.3 substring

stringObject.substring(start, stop)

var a = 'Hello world!';
var b = a.substring(0, 4);
var c = a.substring(3, 2);
var d = a.substring(0, -1);
// a: 'Hello world!'
// b: 'Hell'
// c: 'l',start 比 stop 大,交换这两个参数
// d: '',参数为负,返回空字符串
@前端进阶之旅: 代码已经复制到剪贴板

substring 有两个很特别的行为。一是发现 start 大于 stop 会自动把两个参数对调,二是负数一律当 0 处理。

第一条在特定场景下挺方便,你不用关心用户输入的两个下标谁大谁小。但它同时也是 bug 的温床,因为出错的时候你得不到任何反馈,只会拿到一段莫名其妙但看起来合法的字符串。我自己的习惯是统一用 slice,行为最好预测。

slice、substr、substring 都是字符串的切割方法,三者之间有细微的区别,根据不同的使用场景可以灵活使用。三种方法都是生成新的字符串,而不是修改原 string。字符串在 JS 里是不可变的,所有看起来像「修改字符串」的方法,实际上都在返回新的字符串。

一张表收尾:

方法 第二个参数含义 负数处理 start > end 时 现状
slice 结束下标(不含) 加上长度 返回空串 推荐
substr 截取长度 加上长度 不适用 Annex B 遗留特性
substring 结束下标(不含) 当作 0 自动交换参数 可用,行为怪

# 二、字符串还有这些高频方法

原文只列了截取的部分,实际业务里还有几个用得同样多,顺手补齐。

判断包含关系用 includes、startsWith、endsWith(ES2015),比以前写 indexOf(x) !== -1 直观得多。

去空白用 trim,只去一头用 trimStart 和 trimEnd(ES2019)。表单提交前先 trim 一下,能省掉一半的「为什么后端说我账号不存在」的排查。

补位用 padStart 和 padEnd(ES2017)。做时间格式化的时候 String(m).padStart(2, '0') 是标配写法。

替换全部匹配用 replaceAll(ES2021)。以前只能写 replace(/x/g, y),字符串里带正则元字符的时候还得先转义,很烦。

按下标取字符用 at(ES2022),支持负数,str.at(-1) 拿最后一个字符。

还有一个坑要注意,上面这些方法处理的都是 UTF-16 码元而不是「人眼看到的字符」。emoji 和一些生僻字占两个码元,'👍'.length 是 2,用 slice 从中间切开会得到乱码。真要按字符处理,得用 [...str] 或者 Array.from(str) 先拆成数组,它们走的是字符串迭代器,认码点。

# 三、数组方法索引

数组这块方法太多,一个个展开会把这篇撑爆,而且我另开了一篇专门讲,包括每个方法改不改原数组、返回值边界、时间复杂度。这里只做一张索引表,需要展开的直接跳过去看 JavaScript数组方法总结。

方法 一句话 改原数组
concat 连接数组或值,返回副本 否
slice 按区间截取,返回新数组 否
join 用分隔符拼成字符串 否
push 尾部追加,返回新长度 是
pop 删尾部一项,返回被删的值 是
unshift 头部插入,返回新长度 是
shift 删头部一项,返回被删的值 是
splice 从指定位置删除或插入 是
sort 排序,无参时按字符串比较 是
reverse 颠倒顺序 是
map 每项过一遍转换器,返回等长新数组 否
forEach 只跑副作用,没有返回值 否
find / findIndex 返回第一个满足条件的元素 / 下标 否
filter 返回全部满足条件的元素 否
reduce / reduceRight 从左 / 从右归并成一个值 否
some 有任意一项满足就返回 true 否
every 全部满足才返回 true 否

有两条我在这里单独拎出来,因为最容易翻车。

sort 无参数调用的时候,是把每一项转成字符串再按 UTF-16 码元比较,所以 [1, 10, 8, 6, 9].sort() 得到的是 [1, 10, 6, 8, 9],10 排在 8 前面。排数字必须传比较函数:

var a = [1, 10, 8, 6, 9];
var b = a.sort(function (prev, next) {
  return prev - next;
});
// a: [1, 6, 8, 9, 10],修改了原数组
// b: [1, 6, 8, 9, 10],返回修改后的数组
@前端进阶之旅: 代码已经复制到剪贴板

注意 a 和 b 指向的是同一个数组,sort 是原地排序,返回值只是为了方便链式调用。想不动原数组,2018 年的写法是 [...a].sort(fn),现在有 toSorted(ES2023)可以直接用。

另一条是 forEach 没法中断。它没有返回值,只针对每个元素调用回调,代码是简洁,但 break、continue 都用不了,return 只结束当前这次回调。需要提前跳出就换 for...of 或者 some。

map、filter、reduce 在业务里怎么组合、怎么选型,我单独写了一篇 业务中处理数据结构常用的JS方法,那篇讲的是实战套路,跟这里的清单不重复。

# 四、对象怎么遍历

# 4.1 for in

for-in 循环实际是为循环 enumerable 对象而设计的,for in 也可以循环数组,但是不推荐这样使用。for-in 是用来循环带有字符串 key 的对象的方法。

var obj = {a: 1, b: 2, c: 3};
for (var prop in obj) {
  console.log("obj." + prop + " = " + obj[prop]);
}
// print:  "obj.a = 1" "obj.b = 2" "obj.c = 3"
@前端进阶之旅: 代码已经复制到剪贴板

不推荐用它遍历数组,有两个具体原因。一是它拿到的 key 是字符串 '0'、'1' 而不是数字,做算术之前得先转。二是它会把数组对象上挂的其他可枚举属性也遍历出来。

第二条更要命的地方在于,for...in 会顺着原型链一路往上找。谁往 Array.prototype 或者 Object.prototype 上挂过东西(老库很爱干这事),你的循环里就会冒出没预料到的 key。所以规范的写法要加一道过滤:

for (var prop in obj) {
  if (!Object.prototype.hasOwnProperty.call(obj, prop)) continue;
  console.log(prop, obj[prop]);
}
@前端进阶之旅: 代码已经复制到剪贴板

ES2022 给了个更短的写法,Object.hasOwn(obj, prop),语义一样但不用绕 call。

# 4.2 for of

for of 为 ES6 提供,具有 iterator 接口,就可以用 for of 循环遍历它的成员。

for of 循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)、Generator 对象,以及字符串。

那为什么普通对象不能直接 for of 呢?因为它身上没有 Symbol.iterator。for...of 拿到目标先去找这个方法,找不到就抛 TypeError: obj is not iterable。数组、Set、Map、字符串都在原型上部署了这个方法,普通对象没有。

一种解决方法是,用 Object.keys 方法将对象的键名生成一个数组,然后遍历这个数组。Set、Map 这些结构的迭代细节,可以看 Set WeakSet Map WeakMap 梳理。

# 4.3 entries、keys、values

entries() 返回一个遍历器对象,用来遍历「键名, 键值」组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 iterator 接口,默认就是调用 entries 方法。

keys() 返回一个遍历器对象,用来遍历所有的键名。

values() 返回一个遍历器对象,用来遍历所有的键值。

这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。

fe
  • 一、字符串截取的三兄弟
    • 1.1 slice
    • 1.2 substr
    • 1.3 substring
  • 二、字符串还有这些高频方法
  • 三、数组方法索引
  • 四、对象怎么遍历
    • 4.1 for in
    • 4.2 for of
    • 4.3 entries、keys、values
  • 五、属性顺序这件事,比想象中有规则
  • 六、Object 静态方法的现代组合
  • 总结
  • 参考

← 日常频繁使用的Linux命令,按排查场景整理持续集成 Travis CI →