人人都会AI编程

32.7 数组与对象引用赋值的副作用问题

更新时间:2026-07-11

在 JavaScript 中,对象(包括数组、函数、普通对象等)属于引用类型。这意味着变量存储的并不是对象本身,而是对象在内存堆中的地址。当我们将一个引用类型的变量赋值给另一个变量时,传递的是地址,而不是值的副本。这是导致大量隐蔽副作用的根源。

问题本质:共享同一份数据

const original = { name: 'Alice', skills: ['JavaScript', 'React'] };
const copy = original;

copy.name = 'Bob';
copy.skills.push('Node.js');

console.log(original.name);   // 'Bob' —— 原对象被修改了
console.log(original.skills); // ['JavaScript', 'React', 'Node.js']

这里 copyoriginal 指向内存中的同一个对象,通过 copy 修改属性,original 也会受到影响。数组同理:

const arr1 = [1, 2, 3];
const arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4]

很多开发者以为赋值操作会创建一个独立副本,结果在修改“副本”时无意中污染了原始数据,尤其是当数据在多个函数或组件间传递时,这类副作用极难排查。

常见踩坑场景

1. 函数参数传递的是引用

function addTask(tasks, newTask) {
  tasks.push(newTask); // 直接修改了传入的数组
}

const myTasks = ['代码审查'];
addTask(myTasks, '写测试');
console.log(myTasks); // ['代码审查', '写测试'] —— 外部数组被改变

函数内部直接修改引用类型参数,副作用会“泄漏”到外部,破坏数据不可变性,容易引发状态错乱(尤其在 React、Vue 等框架中)。

2. 嵌套对象/数组只做了浅拷贝

const user = {
  name: '张三',
  address: { city: '北京', district: '朝阳区' }
};

const shallowCopy = { ...user };
shallowCopy.address.city = '上海';

console.log(user.address.city); // '上海' —— 原对象也被修改了

扩展运算符 ...Object.assign()、数组的 slice()concat() 都只能做浅拷贝。如果对象内部还嵌套着其他引用类型,内层对象仍然是共享引用。

3. 状态管理中的意外修改

在 Vuex、Redux、Pinia 等状态管理方案中,要求不能直接修改状态,必须通过提交 mutation 或生成新对象来更新。很多 bug 都是因为直接对 state 中的数组或对象进行 pushsplice、属性赋值等操作导致的。

解决方案:从浅拷贝到深拷贝

浅拷贝 —— 适合只有一层结构的对象

// 对象浅拷贝
const copy1 = Object.assign({}, original);
const copy2 = { ...original };

// 数组浅拷贝
const arrCopy1 = arr.slice();
const arrCopy2 = [ ...arr ];

但如果对象存在嵌套结构,浅拷贝无能为力。

深拷贝 —— 完全独立的副本

  • JSON 方法(有局限)
const deepCopy = JSON.parse(JSON.stringify(original));

这个方法简便,但缺点明显:

  • 无法拷贝 undefinedFunctionSymbol 类型的属性
  • 遇到循环引用会直接报错
  • 日期对象会被转为字符串,正则、Map、Set 会丢失
  • 手写深拷贝(递归 + 类型判断)

手写深拷贝是面试高频题,也是理解引用问题的绝佳练习。基础版递归遍历所有属性,对于数组和普通对象分别处理,并利用 WeakMap 解决循环引用。更完善版本还需处理 Map、Set、Date、RegExp 等特殊对象。

  • 使用工具库

生产环境中推荐使用 Lodash 的 cloneDeep 等经过大量测试的成熟方案,避免在边界情况上踩坑。

防御性编程建议

  • 保持数据不可变:不直接修改原数组/对象,而是返回新数组/对象(如使用 concatfiltermap、展开运算符等)。
  • 明确函数副作用:如果函数内部需要修改传入的引用类型,应在文档或命名上清晰标注,或干脆改为返回新值。
  • 使用 TypeScript:类型系统虽不能彻底杜绝引用问题,但 readonlyReadonlyArray 等工具类型能在编译期捕获大量意外修改。
  • 组件间传递数据时:若非有意共享状态,传递深拷贝,或者使用状态管理库统一控制数据流。

数组与对象的引用赋值问题几乎每个 JavaScript 开发者都会遇到,它不是一个可以绕开的“语法坑”,而是由语言核心的引用机制决定的固有特性。养成主动思考数据是否被共享的习惯,能避免相当一部分难以追踪的状态错误。