【JS手写系列】手写实现深拷贝、浅拷贝
靡不有初,鲜克有终
不积跬步无以至千里
0、前言
JavaScript
的数据类型分为基本数据类型和引用数据类型- 如果对于基本数据类型的拷贝,并没有深浅拷贝的区别
- 深浅拷贝都是对于引用数据类型而言的
本文会涉及到一些 数据类型 && 类型检测方法 这两个方面的知识,不太熟悉的朋友可以看一下我的这篇文章:
1、浅拷贝
1.1、原理
所谓浅拷贝,就是只复制最外一层,里面的都还是相同引用
- 如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值
- 如果拷贝的是引用数据类型,拷贝的仅仅只是内存地址(引用)
看个🌰:
let a = { name: 'test', age: 18, arr: [1, 2, 3] }
let b = a
b.age = 20
b.arr[0] = 666
console.log(a); // { name: 'test', age: 20, arr: [666, 2, 3] }
console.log(b); // { name: 'test', age: 20, arr: [666, 2, 3] }
1.2、实现方式
a、Object.assign
-
MDN
官方参考:- Object.assign,将所有可枚举(
Object.propertyIsEnumerable()
返回true
)的自有属性从一个或多个源对象复制到目标对象,返回修改后的对象。
- Object.assign,将所有可枚举(
-
object.assign
是ES6
中Object
的一个方法,该方法可以用于JS
对象的合并。我们可以使用它来实现浅拷贝。 -
该方法的参数
target
指的是目标对象,sources
指的是源对象。使用形式如下:
Object.assign(target, ...sources)
看个🌰:
let target = {a: 1};
let object2 = {b: {d : 2}};
let object3 = {c: 3};
let finalObj = Object.assign(target, object2, object3);
// 返回的是已经被新增的target本身
console.log(target); // {a: 1, b: {d : 2}, c: 3}
console.log(finalObj); // {a: 1, b: {d : 2}, c: 3}
object2.b.d = 666;
console.log(target); // {a: 1, b: {d: 666}, c: 3} target中的d值已经发生改变
object3.c = 888;
console.log(object3); // {c: 888} 实际上object3已经发生改变
console.log(target); // {a: 1, b: {d: 666}, c: 3} 但target中的c值未发生改变。。。👇
Note:
- 如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值 --- 已完全拷贝,老死不相往来
- 如果拷贝的是引用数据类型,拷贝的就是内存地址 --- 引用关系仍然存在
b、扩展运算符
使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。使用形式如下:
let cloneObj = { ...obj };
使用示例:
let obj1 = { a: 1, b: { c: 1 } }
let obj2 = { ...obj1 };
// 老死不相往来
obj1.a = 2;
console.log(obj1); // { a: 2, b: { c: 1 } }
console.log(obj2); // { a: 1, b: { c: 1 } }
// 藕断丝连
obj1.b.c = 2;
console.log(obj1); // { a: 1, b: { c: 2 } }
console.log(obj2); // { a: 1, b: { c: 2 } }
Note:
- 还是那句话,基本类型拷贝后老死不相往来,引用类型则拷贝后引用关系仍然存在
- 扩展运算符 和
Object.assign
实现的浅拷贝的功能差不多,如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
c、数组浅拷贝
(1)Array.prototype.slice
slice()
方法是JavaScript
数组方法,该方法可以从已有数组中返回选定的元素,不会改变原始数组。使用方式如下:
array.slice(start, end)
该方法有两个参数,两个参数都可选:
-
start
:- 规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
-
end
:- 规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
看个🌰:
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
console.log(animals.slice(2));
// Expected output: Array ["camel", "duck", "elephant"]
console.log(animals.slice(2, 4));
// Expected output: Array ["camel", "duck"]
console.log(animals.slice(1, 5));
// Expected output: Array ["bison", "camel", "duck", "elephant"]
console.log(animals.slice(-2));
// Expected output: Array ["duck", "elephant"]
console.log(animals.slice(2, -1));
// Expected output: Array ["camel", "duck"]
console.log(animals.slice());
// Expected output: Array ["ant", "bison", "camel", "duck", "elephant"]
slice
方法不会修改原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。如果向两个数组任一中添加了新元素,则另一个不会受到影响
如果两个参数都不写,就可以实现一个数组的浅拷贝:
let arr = [1, 2, 3, 4];
let newArr = arr.slice()
console.log(newArr); // [1, 2, 3, 4]
console.log(arr.slice() === arr); // false
arr.push(5)
console.log(arr); // [1, 2, 3, 4, 5]
console.log(newArr); // [1, 2, 3, 4]
(2)Array.prototype.concat
concat()
方法用于合并两个或多个数组,此方法不会更改原始数组,而是返回一个新数组。使用方式如下:
arrayObject.concat(arrayA, arrayB, ......, arrayZ)
如果省略了所有参数,则会返回调用此方法的现存数组的一个浅拷贝(新数组):
let arr = [1, 2, 3, 4];
let newArr = arr.concat([5])
console.log(newArr); // [1, 2, 3, 4, 5]
console.log(arr.concat()); // [1, 2, 3, 4]
console.log(arr.concat() === arr); // false
d、手写实现浅拷贝
根据以上对浅拷贝的理解,实现浅拷贝的思路:
- 对基础类型做最基本的拷贝;
- 对引用类型开辟新的存储,并且仅拷贝一层对象属性。
代码实现:
function shallowCopy (params) {
// 基本类型直接返回
if (!params || typeof params !== "object") return params;
// 根据 params 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(params) ? [] : {};
// 遍历 params 并判断是 params 的属性才拷贝
for (let key in params) {
if (params.hasOwnProperty(key)) {
newObject[key] = params[key];
}
}
return newObject;
}
let params = { a: 1, b: { c: 1 } }
let newObj = shallowCopy(params)
// 拷贝对象中---基本类型老死不相往来,引用类型藕断丝连
params.a = 222
params.b.c = 666
console.log(params); // { a: 222, b: { c: 666 } }
console.log(newObj); // { a: 1, b: { c: 666 } }
浅拷贝
- 可以看到,所有的浅拷贝都只能拷贝一层对象
- 如果存在对象的嵌套,那么浅拷贝就无能为力了
- 深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,实现彻底拷贝
3、深拷贝
3.1、 JSON.stringify
-
JSON.parse(JSON.stringify(obj))
是比较常用的深拷贝方法之一- 它的原理就是利用
JSON.stringify
将JavaScript
对象序列化成为JSON
字符串,并将对象里面的内容转换成字符串,再使用JSON.parse
来反序列化,将字符串生成一个新的JavaScript
对象
- 它的原理就是利用
这个方法是目前使用最多的深拷贝的方法,也是最简单的方法,使用示例:
let obj1 = {
a: 0,
b: {
c: 0
}
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}
这个方法虽然简单粗暴,但也存在一些问题,在使用该方法时需要注意:
-
拷贝的对象中如果有 function、undefined、symbol,当使用过
JSON.stringify()
进行处理之后,都会消失。const originObj = { name: 'test', age: undefined, func: function () { console.log('Hello World'); }, key: Symbol('一个独一无二的key') } const cloneObj = JSON.parse(JSON.stringify(originObj)); console.log(cloneObj); // 只剩下 {name: "test"}
-
无法拷贝不可枚举的属性;
-
无法拷贝对象的原型链;
-
拷贝
Date
引用类型会变成字符串; -
拷贝
RegExp
引用类型会变成空对象; -
对象中含有
NaN、Infinity
以及-Infinity
,JSON
序列化的结果会变成null
; -
无法拷贝对象的循环应用,即对象成环 (
obj[key] = obj
)。
在日常开发中,上述几种情况一般很少出现,所以这种方法基本可以满足日常的开发需求。如果需要拷贝的对象中存在上述情况,还是要考虑使用下面的几种方法。
3.2、函数库lodash
该函数库也有提供_.cloneDeep
用来做深拷贝,可以直接引入并使用:
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false
3.3、手写实现深拷贝
实现深拷贝的思路就是,使用for in
来遍历传入参数的属性值
- 如果值是基本类型就直接复制
- 如果是引用类型就进行递归调用该函数
基础递归
function deepClone (source) {
//判断source是不是对象
if (source instanceof Object === false) return source;
//根据source类型初始化结果变量
let target = Array.isArray(source) ? [] : {};
for (let i in source) {
// 判断是否是自身属性
if (source.hasOwnProperty(i)) {
//判断数据i的类型
if (typeof source[i] === 'object') {
target[i] = deepClone(source[i]);
} else {
target[i] = source[i];
}
}
}
return target;
}
const obj = {
info: { c: { d: 1 } },
age: undefined,
func: function () {
console.log('Hello World');
},
key: Symbol('一个独一无二的key')
}
const resultA = deepClone(obj)
obj.info.c.d = 3
console.log(111111, obj);
// { info: { c: { d: 3 } }, age: undefined, func: f, key: xxxxx}
console.log(222222, resultA);
// { info: { c: { d: 1 } }, age: undefined, func: f, key: xxxxx}
let resultB = [1, [2, 3], [4, [5]]]
let resultC = deepClone(resultB)
resultB[1][1] = 7
console.log(333333, resultB); // [1, [2, 7], [4, [5]]]
console.log(444444, resultC); // [1, [2, 3], [4, [5]]]
这只是粗略的版本,这样虽然实现了深拷贝,但也存在一些问题:
-
存在
环引用
问题(存在循环引用,拷贝会直接爆栈) -
对于
Date、RegExp、Set、Map
等引用类型不能正确拷贝
升级递归---解决环引用爆栈问题
function deepClone (source, map = new Map()) {
//判断source是不是对象
if (source instanceof Object === false) return source;
//根据source类型初始化结果变量
let target = Array.isArray(source) ? [] : {};
/* ----------------新增👇---------------- */
if (map.get(source)) {
// 已存在则直接返回
return map.get(source)
}
// 不存在则第一次设置
map.set(source, target)
/* ----------------新增👆---------------- */
for (let i in source) {
// 判断是否是自身属性
if (source.hasOwnProperty(i)) {
//判断数据i的类型
if (typeof source[i] === 'object') {
// 👇传递map
target[i] = deepClone(source[i], map);
} else {
target[i] = source[i];
}
}
}
return target;
}
const obj = {
info: { c: { d: 1 } },
age: undefined,
func: function () {
console.log('Hello World')
},
key: Symbol('一个独一无二的key'),
}
// 形成环引用
obj.loop = obj
console.log(obj);
const resultA = deepClone(obj)
console.log(resultA); // 拷贝成功
王者递归---解决其余类型拷贝结果不准确问题
// 可遍历类型
const arrTag = '[object Array]';
const objTag = '[object Object]';
const mapTag = '[object Map]';
const setTag = '[object Set]';
const argTag = '[object Arguments]';
const strTag = '[object String]';
// 不可遍历类型
const boolTag = '[object Boolean]';
const numTag = '[object Number]';
const dateTag = '[object Date]';
const errTag = '[object Error]';
const regexpTag = '[object RegExp]';
const symbolTag = '[object Symbol]';
const funTag = '[object Function]';
// 将可遍历类型做个集合
const traversalArr = [arrTag, objTag, mapTag, setTag, argTag, strTag];
// 判断类型的函数(采用最全且无遗漏的判断方式)
function checkType (source) {
return Object.prototype.toString.call(source)
}
// 拷贝RegExp的方法
function cloneReg (source) {
const reFlags = /\w*$/;
const result = new source.constructor(source.source, reFlags.exec(source));
result.lastIndex = source.lastIndex;
return result;
}
// 拷贝Date的方法
function cloneDate (source) {
return new source.constructor(source.valueOf())
}
function deepClone (source, map = new Map()) {
// 非对象直接返回
if (source instanceof Object === false) return source
// 根据source类型初始化结果变量
let target = Array.isArray(source) ? [] : {};
/* ----------------处理环引用问题👇---------------- */
// 已存在则直接返回(仅仅在环引用之间生效)
if (map.get(source)) return map.get(source)
// 不存在则第一次设置
map.set(source, target)
/* ----------------处理环引用问题👆---------------- */
/* ----------------处理Map、Set、Date、RegExp深拷贝失效问题👇---------------- */
const type = checkType(source)
console.log(type);
let emptyObj
// 如果是可遍历类型,直接创建空对象
if (traversalArr.includes(type)) {
emptyObj = new source.constructor()
}
// 处理Map类型
if (type === mapTag) {
source.forEach((value, key) => {
emptyObj.set(key, deepClone(value, map))
})
return emptyObj
}
// 处理Set类型
if (type === setTag) {
source.forEach(value => {
emptyObj.add(deepClone(value, map))
})
return emptyObj
}
// 处理Date类型
if (type === dateTag) return cloneDate(source)
// 处理Reg类型
if (type === regexpTag) return cloneReg(source)
/* ----------------处理Map、Set、Date、RegExp深拷贝失效问题👆---------------- */
for (let item in source) {
// 判断是否是自身属性
if (source.hasOwnProperty(item)) {
// 判断数据i的类型
// if (source[item] instanceof Object) {
if (typeof source[item] === 'obejct') {
target[item] = deepClone(source[item], map);
} else {
target[item] = source[item];
}
}
}
return target;
}
const obj = {
// 基本类型
str: 'test',
num: 18,
boolean: true,
sym: Symbol('独一无二key'),
// 引用类型(以下8种数据对象均需进行真正意义上的深拷贝)
obj_object: { name: 'squirrel' },
arr: [123, '456'],
func: (name, age) => console.log(`姓名:${name},年龄:${age}岁`),
map: new Map([['t', 100], ['s', 200]]),
set: new Set([1, 2, 3]),
date: new Date(),
reg: new RegExp(/test/g),
}
// 形成环引用
obj.loop = obj
const result = deepClone(obj)
console.log('手写deepClone结果:', result)
🚀🚀🚀搞定,看看结果:
4、总结
赋值运算符
=
实现的是浅拷贝,拷贝的是对象的引用地址;
JavaScript
中数组或者对象自带的拷贝方法都是首层浅拷贝;
JSON.stringify
实现的是深拷贝,但是对目标对象的部分拷贝结果存在问题;本文主要是想深入理解深浅拷贝的区别和实现,记录手写深浅拷贝的过程;
- 官方
loadsh
库已经做得非常完美了,用起来不香?实际工作中建议直接使用;若不用
lodash
,想实现相对精简的真正意义上的深拷贝,请手写递归!
🚀🚀🚀
都看到这儿了,可以点个赞再🏃♂️
优质参考链接👇:
转载自:https://juejin.cn/post/7188048010511810620