2258 字
11 分钟
浅拷贝和深拷贝

什么是拷贝?#

let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;
console.log(arr);//[100, 2, 3]

这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。

浅拷贝#

上面的那个例子进行浅拷贝

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;
console.log(arr);//[1, 2, 3]

当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间!!!

这就是浅拷贝!

但是这又会带来一个潜在的问题:

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]

咦!不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。

这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。

接下来,我们来研究一下JS中实现浅拷贝到底有多少种方式?

浅拷贝API#

1. Object.assign#

但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }

2. concat浅拷贝数组#

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]

3. slice#

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]

4. …展开运算符#

let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果

手动实现浅拷贝#

const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}

深拷贝#

1. JSON一套组合拳#

JSON.parse(JSON.stringify());

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

  1. 无法解决循环引用的问题。举个例子:
const a = {val:2};
a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  1. 无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map等
  2. 无法拷贝函数(划重点)

2. 简单实现#

只需将上面手写的浅拷贝骚做修改

function deepClone(target) {
if (typeof target === "object" && target !== null) {
let temp = Array.isArray(target) ? [] : {};
for (const key in target) {
if (Object.hasOwnProperty.call(target, key)) {
const element = target[key];
temp[key] = deepClone(element);
}
}
return temp;
}
return target;
}

3. 细节以及问题处理最终实现#

如上所述,JSON.parse(JSON.stringfy(obj))有三个问题,我们根据这三个问题一个个解决

1.循环引用问题解决#

假如是这样的情况下,a对象中的一个属性有引用了自己

const a = {
name: "weng",
age: 12,
};
a.target = a;

这里我们可以使用一个Map,记录下已经拷贝过了的对象,如果已经拷贝过了,就直接返回这个对象

function deepClone(target, map = new Map()) {
if (map.get(target)) {
return target;
}
if (
(typeof target === "object" || typeof target === "function") &&
target !== null
) {
map.set(target, true);
let temp = Array.isArray(target) ? [] : {};
for (const key in target) {
if (Object.hasOwnProperty.call(target, key)) {
const element = target[key];
temp[key] = deepClone(element, map);
}
}
return temp;
}
return target;
}

好了咱们试一下

const a = {
name: "weng",
age: 12,
};
a.target = a;
console.log(deepClone(a));

image-20210619073628733

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:

在计算机程序设计中,弱引用与强引用相对,

是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 —百度百科

说的有一点绕,我用大白话解释一下,被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。

稍微改造一下即可:

function deepClone(target, map = new Map()) {
.....
}

2. 特殊对象的拷贝#

对于特殊的对象,我们使用以下方式来鉴别:

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么结果:

"object Map"
"object WeakMap"
"object Set"
"object WeakSet"
"object Array"
"object Object"
"object Arguments"

以及不可便利的对象

"object Boolean"
"object Symbol"
"object Number"
"object String"
"object Date"
"object Error"
"object RegExp"
"object Function"

对于不同的对象有不同的处理方案,但是很多是相互类似的

之后改进之后的代码:

function getType(target) {
return Object.prototype.toString.call(target);
}
function isObject(target) {
return typeof target === "object" && target !== null;
}
function deepClone(target, map = new WeakMap()) {
if (!isObject(target)) {
return target;
}
if (map.get(target)) {
return target;
}
// 获取target是Object的哪种衍生类型
const targetType = getType(target);
let cloneTarget;
map.set(target, true);
const structFn = Object.getPrototypeOf(target).constructor;
switch (targetType) {
case "[object RegExp]":
const { source, flags } = target;
cloneTarget = new structFn(source, flags);
break;
case "[object Function]":
// 函数这块独立出来处理
break;
case "[object Map]":
case "[object WeakMap]":
cloneTarget = new structFn();
target.forEach((value, key) => {
cloneTarget.set(deepClone(key), deepClone(value));
});
break;
case "[object Set]":
case "[object WeakSet]":
cloneTarget = new structFn();
target.forEach((value) => {
cloneTarget.add(deepClone(value));
});
break;
case "[object Array]":
case "[object Object]":
cloneTarget = new structFn();
for (const key in target) {
if (Object.hasOwnProperty.call(target, key)) {
const element = target[key];
cloneTarget[key] = deepClone(element, map);
}
}
break;
default:
// case "[object String]":
// case "[object Number]":
// case "[object Boolean]":
// case "[object Error]":
// case "[object Date]":
cloneTarget = new structFn(Object.prototype.valueOf.call(target));
break;
}
return cloneTarget;
}

其中这行代码 const structFn = Object.getPrototypeOf(target).constructor;是为了获取衍生对象或者对象的构造函数,是为了防止丢失原型的情况,之后利用构造函数去整活。

3. 处理函数类型#

虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。

提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要 处理普通函数的情况,箭头函数直接返回它本身就好了。

那么如何来区分两者呢?

答案是: 利用原型。箭头函数是不存在原型的。

处理代码如下:

const handleFunc = (func) => {
// 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}

4. 优化代码#

在上面的代码中,我们在处理一些”基本类型“对象的时候,我们去拿他们的构造函数,然后给他new出来,但是ES6中不推荐这样直接new String, 或者 new Number的写法,并且Symbol类型是无法被new操作符调用的,所以我们改成 new Object的形式并且传他们的value值

cloneTarget = new Object(Object.prototype.valueOf.call(target));

最终代码#

function getType(target) {
return Object.prototype.toString.call(target);
}
function isObject(target) {
return typeof target === "object" && target !== null;
}
function deepClone(target, map = new WeakMap()) {
if (!isObject(target)) {
return target;
}
if (map.get(target)) {
return target;
}
// 获取target是Object的哪种衍生类型
const targetType = getType(target);
let cloneTarget;
map.set(target, true);
const structFn = Object.getPrototypeOf(target).constructor;
switch (targetType) {
case "[object RegExp]":
const { source, flags } = target;
cloneTarget = new structFn(source, flags);
break;
case "[object Function]":
// 函数这块独立出来处理
cloneTarget = handleFunc(target);
break;
case "[object Map]":
case "[object WeakMap]":
cloneTarget = new structFn();
target.forEach((value, key) => {
cloneTarget.set(deepClone(key), deepClone(value));
});
break;
case "[object Set]":
case "[object WeakSet]":
cloneTarget = new structFn();
target.forEach((value) => {
cloneTarget.add(deepClone(value));
});
break;
case "[object Array]":
case "[object Object]":
cloneTarget = new structFn();
for (const key in target) {
if (Object.hasOwnProperty.call(target, key)) {
const element = target[key];
cloneTarget[key] = deepClone(element, map);
}
}
break;
default:
cloneTarget = new Object(Object.prototype.valueOf.call(target));
break;
}
return cloneTarget;
}
const handleFunc = (func) => {
// 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
浅拷贝和深拷贝
https://nollieleo.github.io/posts/浅拷贝和深拷贝/
作者
翁先森
发布于
2021-06-17
许可协议
CC BY-NC-SA 4.0