JavaScript 中十种数组拷贝方法(从浅拷贝到深拷贝)
在JavaScript开发中,数组拷贝是一项基本但极其重要的操作。无论是数据处理、状态管理还是性能优化,理解不同的数组拷贝方法及其适用场景都至关重要。本文将全面介绍JavaScript中十种数组拷贝方法,包括浅拷贝和深拷贝技术,并通过丰富的代码示例和图表帮助你彻底掌握这些知识点。
一、浅拷贝方法
浅拷贝只复制数组的第一层元素,如果数组包含对象或嵌套数组,这些嵌套元素仍然是共享的。以下是七种常见的浅拷贝方法:
1. 扩展运算符(Spread Operator)
// 原始数组
const originalArray = [1, 2, {name: '张三'}];// 使用扩展运算符进行浅拷贝
const copiedArray = [...originalArray];// 修改拷贝数组的基本类型元素
copiedArray[0] = 100;
console.log(originalArray[0]); // 输出: 1 (未受影响)// 修改拷贝数组中的对象元素
copiedArray[2].name = '李四';
console.log(originalArray[2].name); // 输出: '李四' (原始数组也被修改)
适用场景:React状态更新、Redux reducer中不可变数据更新等。
2. for循环拷贝
const original = ['a', 'b', {title: '标题'}];
const copy = [];// 使用for循环进行浅拷贝
for (let i = 0; i < original.length; i++) {copy[i] = original[i];
}// 验证拷贝效果
copy[1] = 'B';
console.log(original[1]); // 'b' (未改变)copy[2].title = '新标题';
console.log(original[2].title); // '新标题' (被改变)
特点:最基础的拷贝方式,兼容性好但代码略显冗长。
3. while循环拷贝
const source = [10, 20, [30, 40]];
const target = [];
let i = 0;// 使用while循环进行浅拷贝
while (i < source.length) {target[i] = source[i];i++;
}// 测试嵌套数组修改
target[2][0] = 300;
console.log(source[2][0]); // 300 (原始数组被修改)
4. Array.map()方法
const data = [1, 2, {id: 101}];// 使用map进行浅拷贝 - 恒等函数
const dataCopy = data.map(item => item);// 或者使用更简洁的写法
const dataCopy2 = data.map(x => x);// 修改对象属性
dataCopy[2].id = 202;
console.log(data[2].id); // 202 (原始数据被修改)
原理:map
方法通过对每个元素应用恒等函数(x => x
)来创建新数组。
5. Array.filter()方法
const nums = [5, 10, {value: 15}];// 使用filter进行浅拷贝 - 条件始终为true
const numsCopy = nums.filter(() => true);// 测试效果
numsCopy[0] = 50;
console.log(nums[0]); // 5 (未改变)numsCopy[2].value = 150;
console.log(nums[2].value); // 150 (被改变)
6. Array.reduce()方法
const items = ['apple', 'banana', {fruit: 'cherry'}];// 使用reduce进行浅拷贝
const itemsCopy = items.reduce((acc, current) => {acc.push(current);return acc;
}, []);// 验证拷贝效果
itemsCopy[1] = 'BANANA';
console.log(items[1]); // 'banana' (未改变)itemsCopy[2].fruit = 'CHERRY';
console.log(items[2].fruit); // 'CHERRY' (被改变)
7. Array.slice()方法
const colors = ['red', 'green', {code: 'blue'}];// 使用slice进行浅拷贝
const colorsCopy = colors.slice();// 或者指定从开始到结束
const colorsCopy2 = colors.slice(0, colors.length);// 测试修改效果
colorsCopy[0] = 'RED';
console.log(colors[0]); // 'red' (未改变)colorsCopy[2].code = 'BLUE';
console.log(colors[2].code); // 'BLUE' (被改变)
特点:slice
不传参数时默认返回整个数组的浅拷贝。
8. Array.concat()方法
const langs = ['JavaScript', 'Python', {name: 'Java'}];// 使用concat进行浅拷贝 - 不传参数或传空数组
const langsCopy = langs.concat();
// 或者
const langsCopy2 = langs.concat([]);// 验证拷贝效果
langsCopy[1] = 'PYTHON';
console.log(langs[1]); // 'Python' (未改变)langsCopy[2].name = 'JAVA';
console.log(langs[2].name); // 'JAVA' (被改变)
9. Array.from()方法
const sets = [new Set([1]), new Set([2]), {type: 'set'}];// 使用Array.from进行浅拷贝
const setsCopy = Array.from(sets);// 测试修改效果
setsCopy[0] = new Set([10]);
console.log(sets[0]); // Set {1} (未改变)setsCopy[2].type = 'SET';
console.log(sets[2].type); // 'SET' (被改变)
二、深拷贝方法
深拷贝会复制数组的所有层级,包括嵌套的对象和数组,使得原始数组和拷贝数组完全独立。
1. JSON.parse & JSON.stringify
const complexArray = [{id: 1, name: '项目1'}, [2, 4, 6], new Date(),function test() { console.log('test'); }
];// 使用JSON方法进行深拷贝
const deepCopy = JSON.parse(JSON.stringify(complexArray));// 修改拷贝后的数组
deepCopy[0].id = 100;
deepCopy[1][0] = 200;console.log(complexArray[0].id); // 1 (未改变)
console.log(complexArray[1][0]); // 2 (未改变)// 注意:函数和特殊对象会被忽略
console.log(deepCopy[2]); // 日期被转换为字符串
console.log(deepCopy[3]); // undefined (函数丢失)
局限性:
- 无法拷贝函数、undefined
- 日期对象会被转换为字符串
- RegExp、Error等特殊对象会被忽略
- 会丢失构造函数信息
2. 深拷贝工具函数
function deepClone(obj, hash = new WeakMap()) {// 处理基本类型和nullif (obj === null || typeof obj !== 'object') {return obj;}// 处理日期对象if (obj instanceof Date) {return new Date(obj);}// 处理正则表达式if (obj instanceof RegExp) {return new RegExp(obj);}// 处理循环引用if (hash.has(obj)) {return hash.get(obj);}// 创建新对象/数组const cloneObj = new obj.constructor();hash.set(obj, cloneObj);// 递归拷贝所有属性for (const key in obj) {if (obj.hasOwnProperty(key)) {cloneObj[key] = deepClone(obj[key], hash);}}return cloneObj;
}// 使用示例
const original = {arr: [1, 2, {name: '测试'}],date: new Date(),regex: /test/g
};
const cloned = deepClone(original);
深拷贝方法对比
方法 | 速度 | 功能完整性 | 循环引用支持 | 特殊对象支持 |
---|---|---|---|---|
JSON方法 | 快 | 不完整 | 不支持 | 有限支持 |
递归深拷贝 | 慢 | 完整 | 支持 | 完整支持 |
lodash.cloneDeep | 中等 | 完整 | 支持 | 完整支持 |
三、应用场景与最佳实践
1. 何时使用浅拷贝
- 原始数组只包含基本数据类型
- 你确实需要共享嵌套对象的引用
- 性能要求极高,且数据量较大
2. 何时使用深拷贝
- 数组包含嵌套对象或数组
- 需要完全独立的数据副本
- 避免意外的副作用
3. 实际开发建议
四、常见问题解答
Q1:为什么我的数组拷贝后修改还是影响了原数组?
A:这通常是因为你使用了浅拷贝方法,而数组中包含对象或嵌套数组。解决方法是使用深拷贝技术。
Q2:JSON.parse(JSON.stringify())有什么缺点?
A:这种方法会丢失函数、undefined值,将日期转为字符串,无法处理循环引用,且会破坏特殊对象的原型链。
Q3:在React中推荐使用哪种拷贝方法?
A:推荐使用扩展运算符或slice进行浅拷贝,因为React推崇不可变数据流,而浅拷贝已经足够满足大多数状态更新的需求。
五、扩展知识
1. 不可变数据的重要性
在React、Redux等现代前端框架中,不可变数据是核心概念之一。正确的数组拷贝方法可以帮助我们:
- 更容易追踪数据变化
- 实现时间旅行调试
- 优化组件渲染性能
2. 结构化克隆算法
现代浏览器提供了structuredClone
API,它是比JSON方法更强大的深拷贝方案:
const original = {set: new Set([1, 2, 3]),map: new Map([['a', 1]]),date: new Date()
};// 使用structuredClone进行深拷贝
const cloned = structuredClone(original);
支持:
- 循环引用
- Map/Set等复杂对象
- ArrayBuffer等二进制数据
单词、短语表
单词/短语 | 音标 | 词性 | 词根/词缀 | 释义 | 搭配 | 例子 |
---|---|---|---|---|---|---|
shallow copy | /ˈʃæloʊ ˈkɒpi/ | 名词短语 | shallow(浅的)+copy(拷贝) | 浅拷贝 | perform a ~ | JavaScript中大多数数组方法是浅拷贝 |
deep copy | /diːp ˈkɒpi/ | 名词短语 | deep(深的)+copy(拷贝) | 深拷贝 | create a ~ | JSON方法可以实现简单的深拷贝 |
spread operator | /spred ˈɒpəreɪtə/ | 名词短语 | spread(展开)+operator(操作符) | 扩展运算符 | use the ~ | 扩展运算符是ES6引入的便捷语法 |
immutable | /ɪˈmjuːtəbl/ | 形容词 | im-(不)+mut(变化)+able(能) | 不可变的 | ~ data | React推崇不可变数据流 |
nested | /ˈnestɪd/ | 形容词 | nest(巢)+ed | 嵌套的 | ~ array | 处理嵌套数组需要深拷贝 |
recursion | /rɪˈkɜːrʒn/ | 名词 | re-(再)+curs(跑)+ion | 递归 | ~ function | 深拷贝通常需要递归实现 |
reference | /ˈrefrəns/ | 名词 | re-(回)+fer(带来)+ence | 引用 | by ~ | 对象在JavaScript中通过引用传递 |
circular reference | /ˈsɜːrkjələr ˈrefrəns/ | 名词短语 | circular(循环的)+reference(引用) | 循环引用 | handle ~ | 深拷贝需要处理循环引用问题 |
enumeration | /ɪˌnjuːməˈreɪʃn/ | 名词 | e-(出)+numer(数)+ation | 枚举 | property ~ | JSON.stringify只能序列化可枚举属性 |
paradigm | /ˈpærədaɪm/ | 名词 | para-(旁)+digm(显示) | 范式 | programming ~ | 函数式编程范式强调不可变性 |