前言
本篇文章主要讲解浅拷贝和深拷贝
浅拷贝和深拷贝
JS 数据类型分为基本数据类型和引用数据类型,浅拷贝和深拷贝的行为在对基本类型时无区别,都是拷贝其值,两者的区别,主要体现在对引用类型的处理上
浅拷贝的行为是只进行第一层复制,对基本类型的属性进行值拷贝,对引用类型的属性进行内存地址拷贝(因此嵌套的引用类型,修改还是会互相影响)
深拷贝是递归复制对象的所有层级,从原对象上完整拷贝出一个新对象,新对象和原对象互不影响,也不相等
浅拷贝的实现方式
Object.assign 或展开运算符 ...
Object 对象上存在一个静态方法 assign
:MDN Object.assign()
Object.assign
的作用是可以把多个对象的自有属性(可枚举)复制到一个目标对象,并且返回复制后的目标对象
ES 6 新增了展开语法(...),可以很方便的来展开对象、数组、字符串等可迭代类型
使用 Object.assign
或展开运算符 ...
都可以实现浅拷贝
js
let o1 = {
name:'张三',
hobby:['吃饭','睡觉','打豆豆'],
}
let o2 = Object.assign({}, o1)
// 或者使用展开运算符(...)
//let o2 = {...o1}
o2.name = '李四' // 不会影响原对象
o2.hobby.push('学习') // 嵌套引用类型,共享内存地址,会改变原对象
console.log(o1) // { name: '张三', hobby: [ '吃饭', '睡觉', '打豆豆', '学习' ] }
console.log(o2) // { name: '李四', hobby: [ '吃饭', '睡觉', '打豆豆', '学习' ] }
Array.prototype.slice() 与 Array.prototype.concat()
Array
数组上存在 slice
、concat
两个实例方法,都可以实现数组的浅拷贝
Array.prototype.slice()
方法用于数组分割,传入分割开始和结束的索引,返回指定的新数组,不影响原始数组
Array.prototype.concat()
方法用于合并两个或多个数组。返回一个合并后的数组,不影响原始数组
js
const originArr = ['a','b','c',['d','e']]
const arr1 = originArr.slice()
const arr2 = originArr.concat()
arr1.push('f') // 不会影响原数组
arr2.push('g') // 不会影响原数组
arr1[3].push('h') // 嵌套引用类型,会影响到 originArr、arr2
console.log(arr1) // [ 'a', 'b', 'c', [ 'd', 'e', 'h' ], 'f' ]
console.log(arr2) // [ 'a', 'b', 'c', [ 'd', 'e', 'h' ], 'g' ]
console.log(originArr) // [ 'a', 'b', 'c', [ 'd', 'e', 'h' ] ]
lodash.clone
lodash 库 提供了 clone
方法,用于浅拷贝
js
var _ = require('lodash');
let o1 = {
name:'张三',
hobby:['吃饭','睡觉','打豆豆'],
}
let cloneResult = _.clone(o1);
cloneResult.hobby.push('学习')
console.log(cloneResult) // { name: '张三', hobby: [ '吃饭', '睡觉', '打豆豆', '学习' ] }
console.log(o1) // { name: '张三', hobby: [ '吃饭', '睡觉', '打豆豆', '学习' ] }
手动实现浅拷贝
写一个考虑对象和数组的 shallowClone
函数
js
function shallowClone(params){
// 可能是基本类型或 Null,直接返回
if(typeof params !== 'object' || !params) return params;
const result = Array.isArray(params) ? [] : {};
for (const key of Object.keys(params)) {
result[key] = params[key];
}
return result
}
const o1 = {
name:'张三',
age:18,
sex:'男',
}
const arr1 = [1,2,3,4,5]
console.log(shallowClone(o1)) // { name: '张三', age: 18, sex: '男' }
console.log(shallowClone(arr1)) // [ 1, 2, 3, 4, 5 ]
使用
Object.keys
静态方法获取对象本身可枚举的字符串属性数组
深拷贝的实现方式
JSON.parse(JSON.stringify())
这是一种最简单粗暴的深拷贝方式
JSON.stringify()
静态方法将一个对象或值转化为 JSON 字符串
JSON.parse()
静态方法将一个 JSON 字符串解析回 JavaScript 对象
先把目标序列化为 JSON 字符串,再反序列化为 JS 对象,就实现了一个深拷贝,这在大部分情况下是可行的
但注意,这个方法不能处理以下情况
- 属性有
Function
函数、undefined
,Symbol
等,会被忽略 - 属性有
Date
对象 等,会被转换为字符串 - 属性有
RegExp
对象、Error
对象等,会被转换为空对象 - 属性有
NaN
、Infinity
、-Infinity
,会被转换为null
js
const obj = {
name:'张三',
age:18,
sex: undefined,
date:new Date(),
getName(){
console.log(name)
}
}
JSON.parse(JSON.stringify(obj)) // { name: '张三', age: 18, date: '2025-08-20T13:55:14.625Z' }
lodash.cloneDeep
lodash 库提供了一个 cloneDeep
方法,用于深拷贝
js
const _ = require('lodash');
const obj = {
name:'张三',
age:18,
sex: undefined,
getName(){
console.log(name)
}
}
const deepClone = _.cloneDeep(obj)
deepClone.getName() // 张三
手动实现深拷贝
手写实现深拷贝,想一想我们需要考虑哪些方面:
- 处理基本类型和
null
等,直接返回 - 考虑数据的嵌套结构,使用递归处理
- 处理数据内的引用类型,除了
Object
、Array
这些外,还有比如Date
、RegExp、Set
、Map
等 这些引用类型有的实现了迭代器方法,也就是可以遍历的类型,这意味着可能存在嵌套的数据,因此处理方法也需要递归,比如Set
、Map
- 不能遍历的引用类型,没有层级嵌套,所以处理的时候只需要拷贝一份副本返回
根据这个思路,目前实现的深拷贝是这样的:
部分代码参考了
lodash
库源码
js
const protoString = Object.prototype.toString;
const setTag = "[object Set]";
const mapTag = "[object Map]";
const dateTag = "[object Date]";
const regexpTag = "[object RegExp]";
const arrayTag = "[object Array]";
const objectTag = "[object Object]";
// 深拷贝
function deepClone(params) {
// 1. 基本类型或 null
if (typeof params !== "object" || params === null) return params;
const currentTag = protoString.call(params);
// 2. 考虑不同引用类型的处理
switch (currentTag) {
case dateTag:
return new Date(params.getTime());
case regexpTag: {
const result = new RegExp(params.source, params.flags);
result.lastIndex = params.lastIndex;
return result;
}
case setTag: {
const result = new Set();
params.forEach((v) => result.add(deepClone(v)));
return result;
}
case mapTag: {
const result = new Map();
params.forEach((v, k) => result.set(k, deepClone(v)));
return result;
}
//...
}
const isArray = currentTag === arrayTag;
const isObject = currentTag === objectTag;
// 3. 处理对象或数组:注意递归返回
if (isObject || isArray) {
const newValue = isArray ? [] : {};
for (const key in params) {
if (params.hasOwnProperty(key)) {
newValue[key] = deepClone(params[key]);
}
}
return newValue;
}
}
模拟一段数据,看看 deepClone
函数的实现效果
js
const obj = {
name: "张三",
age: 18,
hobby: ["唱", "跳", "rap", "篮球"],
time: new Date(),
arr: [1, 2, 3],
set: new Set([1, 2, 3]),
map: new Map([
["a", 1],
["b", 2],
]),
};
const deepCloneObj = deepClone(obj);
console.log(deepCloneObj)
打印效果

栈溢出问题
后来看到一些文章说有"爆栈"的问题,也就是说,对象中的某个属性指向了这个对象本身,形成了一个闭环,导致递归调用无限循环,最终导致栈溢出
栈溢出:每次函数调用都会向调用栈添加一个新的帧,而调用栈有其最大容量限制。当调用深度超过这个限制时,就会触发栈溢出错误
还是拿上面那个 obj
数据来复现一下问题
js
const obj = {
name: "张三",
age: 18,
hobby: ["唱", "跳", "rap", "篮球"],
time: new Date(),
arr: [1, 2, 3],
set: new Set([1, 2, 3]),
map: new Map([
["a", 1],
["b", 2],
]),
}
// 指向 obj 本身
obj.name = obj
console.log(deepClone(obj))
打印报错:Uncaught RangeError: Maximum call stack size exceeded

文章也提到了解决办法,重点在于记录,把初次进入的数据记录下来,后续再遇到相同的属性时,直接返回记录的值,避免无限循环
Map 提供保存键值对数据的功能,key 作为记录标识符,value 作为记录的值,很合适
Map 是 ES6 提供的新数据结构,保存键值对数据,键的类型不限于字符串,参考:阮一峰 - ES6、MDN - Map
最终的深拷贝代码如下:
js
const protoString = Object.prototype.toString;
const setTag = "[object Set]";
const mapTag = "[object Map]";
const dateTag = "[object Date]";
const regexpTag = "[object RegExp]";
const arrayTag = "[object Array]";
const objectTag = "[object Object]";
// 深拷贝
function deepClone(params, map = new Map()) {
// 1. 基本类型或 null
if (typeof params !== "object" || params === null) return params;
// 检查循环引用
if (map.has(params)) return map.get(params);
const currentTag = protoString.call(params);
// 2. 考虑不同引用类型的处理
switch (currentTag) {
case dateTag:
return new Date(params.getTime());
case regexpTag: {
const result = new RegExp(params.source, params.flags);
result.lastIndex = params.lastIndex;
return result;
}
case setTag: {
const result = new Set();
map.set(params, result);
params.forEach((v) => result.add(deepClone(v, map)));
return result;
}
case mapTag: {
const result = new Map();
map.set(params, result);
params.forEach((v, k) => result.set(k, deepClone(v, map)));
return result;
}
//...
}
const isArray = currentTag === arrayTag;
const isObject = currentTag === objectTag;
// 3. 处理对象或数组:注意递归返回
if (isObject || isArray) {
const newValue = isArray ? [] : {};
map.set(params, newValue);
for (const key in params) {
if (params.hasOwnProperty(key)) {
newValue[key] = deepClone(params[key], map);
}
}
return newValue;
}
}
最后再来试一下效果
js
const obj = {
name: "张三",
age: 18,
hobby: ["唱", "跳", "rap", "篮球"],
time: new Date(),
arr: [1, 2, 3],
set: new Set([1, 2, 3]),
map: new Map([
["a", 1],
["b", 2],
]),
getName() {
return this.name;
},
};
obj.key = obj
console.log(deepClone(obj));
打印结果如下:

总结
文章主要讲解浅拷贝和深拷贝两种方法的特点和实现
浅拷贝只拷贝开始一层,手写实现也好处理
深拷贝是递归拷贝所有层级,手写实现的话,需要考虑很多实际场景,代码量也会比较大
如有兴趣的话,建议查看 lodash
中 cloneDeep
方法的实现
参考资料
参透JavaScript系列
本文已收录至《参透 JavaScript 系列》,全文地址:我的 GitHub 博客 | 掘金专栏
交流讨论
对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正