前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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值与类型

为什么字符串方法都返回新字符串,数组方法却会直接改原数组?

字符串是原始值,一旦创建内容就改不了,toUpperCase、replace、slice 都是生成一个新字符串还给你。数组是对象,push、sort、splice 这类方法直接改原数组,所有引用它的地方都会看到变化。

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

值不可变和引用可变

  1. 字符串原始值,按值使用,内容创建后固定
  2. 数组对象,变量里存的是引用,多个变量可以指向同一个数组
  3. s += "a"变量指向了一个新字符串,旧字符串本身没被改动

const 和不可变是两回事。const arr 只是不能让 arr 指向别的数组,arr.push() 照样能改内容;let s 可以重新赋值,但字符串本身从来没被改过。

核心回答

先记住这个答案

JavaScript 里字符串是原始值,数组是对象,这是差别的根源。原始值创建后内容就固定了,所以字符串上所有方法都只能返回一个新字符串,原字符串保持不变。数组是可变对象,变量里存的是指向它的引用,push、sort、splice、reverse 这些方法会直接修改这块数据,所有持有这个引用的地方都会同时看到变化。这个差别会影响函数传参是否安全、React 状态更新能不能被检测到,以及大量拼接字符串时的写法。

  • 字符串方法全部返回新值,原字符串永远不变
  • s[0] = "x" 不会生效,严格模式下直接报错
  • push、sort 等会改原数组
  • 不改原数组可用复制式方法

同样是「排序、转大写」,一个改原值一个不改

把字符串和数组放在一起操作,打印原值,差别很直观。

字符串和数组的对照JavaScript
'use strict'
const s = 'hello'
try {
  s[0] = 'H' // 严格模式下给字符串下标赋值会报错
} catch (e) {
  console.log(e.name)
}
const upper = s.toUpperCase()
console.log(s, upper) // 原字符串没变,拿到的是新字符串

const arr = [3, 1, 2]
const res = arr.sort()
console.log(arr, res === arr) // 原数组被改了,返回的还是它自己

const sorted = [3, 1, 2].toSorted()
console.log(sorted)
查看输出与解释
TypeError
hello HELLO
1,2,3 true
1,2,3

字符串的下标是只读的。非严格模式下 s[0] = 'H' 不报错,但也没有任何效果,这比报错更容易坑人。arr.sort() 不仅改了原数组,返回值还是同一个数组,所以 res === arr 是 true,很多人以为拿到的是排好序的副本。最后的 toSorted 是 ES2023 加的,排序结果放在新数组里,原数组不动。

为什么字符串要设计成不可变

字符串用得太多了,对象的键、比较、拼接、当 Map 的键全是它。如果字符串能被原地修改,一个函数改了传进来的字符串,调用方手里的值也跟着变,那用字符串当对象属性名就不安全了,键可能在你不知情的情况下变掉。不可变之后,同样内容的字符串可以放心共享,引擎也可以做缓存和去重,比较两个字符串也不用担心中途被改。

代价是每次「修改」都要生成新字符串。早期在循环里写 str += piece 被认为很慢,因为每次都复制一遍。现在的 V8 会先把拼接结果记成一棵拼接树,等真正读取内容时再压平,所以常规循环拼接已经不是瓶颈。拼接成千上万段时,用数组收集再 join('') 仍然是稳妥写法。

数组可变带来的实际问题

数组可变本身不是坏事,大数组原地排序不用额外开一份内存。麻烦出在「共享引用」上:把数组传给函数,函数内部 sort 了一下,调用方的数据顺序也乱了。

React 里原地修改导致不重新渲染
setList(list) 之前先 list.push(item),React 用 Object.is 比较新旧状态,发现还是同一个数组引用,就认为没变化,界面不会更新。正确写法是生成新数组:setList([...list, item]),删除用 filter,修改某一项用 map 或 list.with(index, newItem)。
哪些数组方法是安全的
不改原数组的有 map、filter、slice、concat、reduce,以及 ES2023 新增的 toSorted、toReversed、toSpliced、with。会改原数组的主要是 push、pop、shift、unshift、splice、sort、reverse、fill、copyWithin。新增的四个里,带 to 前缀的三个正好对应 sort、reverse、splice 的副本版,with 对应的是 arr[i] = x。
回答前,多想一步

容易答错的地方

把 const 当成不可变
const arr = [] 之后 arr.push(1) 完全合法,const 只锁住变量指向谁,不锁内容。真要冻结内容用 Object.freeze,而且它只冻结一层。
以为 s += 'a' 修改了字符串
这行代码是创建了一个新字符串,再让变量 s 指向它。原来那个字符串如果还有别的变量引用,内容完全没变。
以为 slice 和 splice 差不多
slice 返回截取的副本,原数组不变;splice 直接在原数组上删除和插入,返回的是被删掉的元素。两个名字只差一个字母,面试和代码评审里都经常被问。
试着用自己的话回答

面试官还会怎么问?

字符串是原始值,为什么能调用 s.length 和方法?

访问属性时引擎会临时创建一个 String 包装对象,调用完就丢掉。所以给字符串加属性 s.foo = 1 也不会保留,下次读 s.foo 是 undefined。

怎么反转一个字符串?

字符串没有 reverse,常见写法是 [...s].reverse().join('')。用展开而不是 split(''),是因为展开按码点拆分,emoji 这类由两个 UTF-16 单元组成的字符不会被拆坏。

函数参数传数组,怎么避免被内部修改?

调用方传一份副本,比如 fn([...arr]);或者函数内部约定只用不改原数组的方法。TypeScript 里可以把参数声明成 readonly number[],调用 push、sort 会直接报类型错误。

从一道题,走向一组知识

把知识连起来

值与类型

undefined 和 null 到底有什么区别,什么时候该用哪个?

字符串、undefined、null 都是原始值,理解了原始值不可变,再看它们的区别更清楚。

值与类型

Number.prototype.valueOf 返回什么?自定义对象的 valueOf 会怎样影响类型转换?

"abc".length 能用是因为临时包装成了 String 对象,和 Number 包装对象的 valueOf 是同一个机制。

参考资料

  • JavaScript:值与类型参考资料

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

本题目录
  1. 先记住这个答案
  2. 同样是「排序、转大写」,一个改原值一个不改
  3. 为什么字符串要设计成不可变
  4. 数组可变带来的实际问题
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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