let obj = {
url: '/api/list',
method: 'GET',
cache: false,
timeout: 1000,
key: Symbol('KEY'),
big: 10n,
n: null,
u: undefined,
headers: {
'Content-Type': 'application/json',
post: {
'X-Token': 'xxx'
}
},
arr: [10, 20, 30],
reg: /^\d+$/,
time: new Date(),
fn: function() {
console.log(this);
},
err: new Error('xxx')
};
obj.obj = obj;
复制代码
假设有以上一个对象,包含各种各样类型的属性,并且存在循环引用,那么分别对以上对象进行深浅拷贝会出现什么样的问题?
浅拷贝
仅仅复制对象的第一层。
对象的第一层如果还是一个对象,那么存的是对象的地址,所以如果使用浅拷贝,从第二层开始,就还是原来对象的属性,如果对拷贝完的对象的第二层对象属性进行修改,那么原来的对象的相应属性也会改变
例如对以上对象进行浅拷贝,那么 headers
属性在两个对象上都引用的是同一个内存地址
浅拷贝几个方法
Object.assign
let new_obj = Object.assign({}, obj);
复制代码
扩展运算符 ...
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()
的功能相同。
let new_obj = {
...obj
};
复制代码
遍历
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
};
复制代码
数组的浅拷贝
数组本质上也是对象的一种,也有几个可以进行浅拷贝的简单方法
扩展运算符 ...
let arr = [1, 3, {
username: ' kobe'
}];
let arr2 = [...arr]
arr2[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
复制代码
Array.prototype.concat()
/ Array.prototype.slice()
原理本质上是遍历
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
复制代码
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.slice();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
复制代码
深拷贝
深拷贝是将一个对象从内存中完整的拷贝一份出来,不仅仅拷贝对象的引用,还拷贝各个层级的对象的引用。从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
实现深拷贝的简单方法
let newObj = JSON.parse(JSON.stringify(obj))
复制代码
JSON.stringify
:把对象/数组变为JSON字符串
JSON.parse
:把JSON字符串变为对象/数组(浏览器需要重新开辟所有内存)
弊端:
-
不允许出现循环引用,不然会直接报错
-
属性值不能是
BigInt
,会直接报错'Uncaught TypeError: Do not know how to serialize a BigInt'
-
只要属性值是
symbol
/undefined
/function
这些类型的,会直接丢失 -
还有信息不准确的,例如:正则->空对象,Error对象->空对象,日期对象->字符串
手写一个深拷贝
原理:遍历所有属性,遇到对象就递归处理,遇到基本类型就直接复制,特殊类型特殊处理
递归实现
原理在注释
function deepClone(obj) {
if (obj === null) return obj; // 如果是null直接返回
if (obj instanceof Date) return new Date(obj); //date特殊处理
if (obj instanceof RegExp) return new RegExp(obj); //正则特殊处理
if (obj instanceof Error) return new Error(obj.message);//Error 特殊处理
//如果是函数的话是不需要深拷贝,如果非要处理:
if (typeof obj === "function") {
return function () {
return obj.apply(this, arguments);
};
}
//原始值类型的值处理 原始值类型直接返回(包括Symbal)
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
let obj = {
name: 1,
address: {
x: 100
}
};
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
复制代码
解决循环引用
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回对应的拷贝的引用,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
将解决循环引用的思路单独写出来
- 首先将要拷贝的对象和已拷贝的对象做一个引用的映射
map.set(target, cloneTarget)
(需要拷贝的对象的引用<—>已经拷贝对象的引用) - 然后判断。当传入的引用是自己的时候,那么可以根据
map.get(target)
判断出已经有拷贝的映射 - 返回那个映射的已拷贝对象的引用
return map.get(target)
function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = {};
if (map.get(target)) { //步骤3判断出应用的使自己
return map.get(target);
}
map.set(target, cloneTarget); //步骤1
for (const key in target) {
cloneTarget[key] = clone(target[key], map); //步骤2,这里传入了自己的引用
}
return cloneTarget;
} else {
return target;
}
};
const target = {};
target.xxx = target;
console.log(clone(target))
复制代码
知识点:
只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
知识点二:为什么要用 WeakMap
?参考
了解原理之后的代码:
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Error) return new Error(obj.message);
if (typeof obj === "function") {
return function () {
return obj.apply(this, arguments);
};
}
if (typeof obj !== "object") return obj;
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = {
name: 1,
address: {
x: 100
}
};
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
复制代码