JS深拷贝与浅拷贝
2021-09-14 本文已影响0人
隔壁老王z
拷贝对象时,浅拷贝只解决了第一层的问题,拷贝第一层的 基本类型值,以及第一层的 引用类型地址。
浅拷贝
浅拷贝的方法:
1、Object.assign()
2、展开语法 Spread
3、Array.prototype.slice()
深拷贝
JSON.parse(JSON.stringify(object))
-
JSON.parse(JSON.stringify(object))
仍然有缺陷:- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
- 不能正确处理 new Date()
- 不能处理正则
undefined、symbol 和函数这三种情况,会直接忽略
let obj = {
name: 'muyiy',
a: undefined,
b: Symbol('muyiy'),
c: function() {}
}
console.log(obj);
// {
// name: "muyiy",
// a: undefined,
// b: Symbol(muyiy),
// c: ƒ ()
// }
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}
循环引用情况下,会报错
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON
new Date
情况下,转换结果不正确
new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)
JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""
JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"
解决方法转成字符串或者时间戳就好了:
let date = (new Date()).valueOf();
// 1545620645915
JSON.stringify(date);
// "1545620673267"
JSON.parse(JSON.stringify(date));
// 1545620658688
正则会变成空对象
let obj = {
name: "muyiy",
a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy", a: {}}
如何模拟实现一个 Object.assign
:
- 判断 Object 是否支持该函数,如果不存在的话创建一个函数 assign ,并使用 Object.defineProperty 将该函数绑定到 Object 上;
- 判断参数是否正确(目标对象不能为空,我们可以直接设置 {} 传递进去,但必须设置值);
- 使用 Object() 转成对象,并保存为 to,最后返回这个对象 to;
- 使用 for...in 循环遍历出所有可枚举的自有属性,并复制给新的目标对象(使用 hasOwnProperty 获取自有属性,即非原型链上的属性)。
if(typeof Object.assign !== 'function') {
// Attention 1
Object.defineProperty(Object, 'assign', {
value: function(target) {
'use strict'
// undefined 和 null时 undefined == null (true)
if(target == null) { // Attention 2
throw new TypeError('Cannot convert undefined of null to object')
}
// Attention 3
var to = Object(target)
for(var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index]
if(nextSource != null) { // Attention 2
// Attention 4
// 为什么用hasOwnProperty不用in? in会检查原型链,hasOwnProperty不会:'toString' in {} // true
// 但是直接使用 myObject.hasOwnProperty(..) 是有问题的,因为有的对象可能没有连接到 Object.prototype 上(比如通过 Object.create(null) 来创建),这种情况下,使用 myObject.hasOwnProperty(..) 就会失败。
for(var nextKey in nextSource) {
if(Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey]
}
}
}
}
return to
},
writable: true,
configurable: true,
enumerable: false
})
}
实现一个深拷贝的一些问题
考虑 对象、数组、循环引用、引用丢失、Symbol 和递归爆栈等情况
简单实现: 递归 + 浅拷贝(浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作)
// 浅拷贝
function cloneShallow(source) {
var target = {};
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
return target;
}
// 深拷贝
function cloneDeep1(source) {
var target = {};
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object') {
target[key] = cloneDeep1(source[key]); // 注意这里
} else {
target[key] = source[key];
}
}
}
return target;
}
这个实现方式的问题:
- 没有对传入参数进行校验,传入 null 时应该返回 null 而不是 {}
- 对于对象的判断不严谨,因为 typeof null === 'object'
- 没有考虑数组的兼容
- 拷贝数组
首先需要进行数据格式的校验,递归操作需要排除非引用类型的 source。
function isObject(data) {
return typeof data === 'object' && data !== null
}
function cloneDeep2(source) {
// 非对象返回自身
if (!isObject(source)) return source;
// 兼容数组
var target = Array.isArray(source) ? [] : {};
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep2(source[key]); // 注意这里
} else {
target[key] = source[key];
}
}
}
return target;
}
- 循环引用
// 1. 使用哈希表解决:
// 其实就是循环检测,我们设置一个数组或者哈希表存储已拷贝的对象,当检测到当前对象在哈希表中存在时,即取出来并返回
function cloneDeep3(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
var target = Array.isArray(source) ? [] : {};
hash.set(source, target); // 新增代码,哈希表设值
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep3(source[key], hash); // 新增代码,传入哈希表
} else {
target[key] = source[key];
}
}
}
return target;
}
// 2. 使用数组, 在 ES5 中没有 weakMap 这种数据格式,所以在 ES5 中使用数组进行代替
function cloneDeep3(source, uniqueList) {
if (!isObject(source)) return source;
if (!uniqueList) uniqueList = []; // 新增代码,初始化数组
var target = Array.isArray(source) ? [] : {};
// ============= 新增代码
// 数据已经存在,返回保存的数据
var uniqueData = find(uniqueList, source);
if (uniqueData) {
return uniqueData.target;
};
// 数据不存在,保存源数据,以及对应的引用
uniqueList.push({
source: source,
target: target
});
// =============
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep3(source[key], uniqueList); // 新增代码,传入数组
} else {
target[key] = source[key];
}
}
}
return target;
}
// 新增方法,用于查找
function find(arr, item) {
for(var i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}
- 拷贝 Symbol
Symbol 在 ES6 下才有,我们需要一些方法来检测出 Symbol 类型。
方法一:Object.getOwnPropertySymbols(...)
// 查找一个给定对象的符号属性时返回一个 ?symbol 类型的数组。注意,每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性。
var obj = {};
var a = Symbol("a"); // 创建新的symbol类型
var b = Symbol.for("b"); // 从全局的symbol注册?表设置和取得symbol
obj[a] = "localSymbol";
obj[b] = "globalSymbol";
var objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols.length); // 2
console.log(objectSymbols) // [Symbol(a), Symbol(b)]
console.log(objectSymbols[0]) // Symbol(a)
方法二:Reflect.ownKeys(...)
// 返回一个由目标对象自身的属性键组成的数组。它的返回值等同于
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
Reflect.ownKeys([]); // ["length"]
var sym = Symbol.for("comet");
var sym2 = Symbol.for("meteor");
var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
[sym2]: 0, "-1": 0, "8": 0, "second str": 0};
Reflect.ownKeys(obj);
// [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
// 注意顺序
// Indexes in numeric order,
// strings in insertion order,
// symbols in insertion order
方法一
思路就是先查找有没有 Symbol
属性,如果查找到则先遍历处理 Symbol
情况,然后再处理正常情况,多出来的逻辑就是下面的新增代码。
function cloneDeep4(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source);
let target = Array.isArray(source) ? [] : {};
hash.set(source, target);
// ============= 新增代码
let symKeys = Object.getOwnPropertySymbols(source); // 查找
if (symKeys.length) { // 查找成功
symKeys.forEach(symKey => {
if (isObject(source[symKey])) {
target[symKey] = cloneDeep4(source[symKey], hash);
} else {
target[symKey] = source[symKey];
}
});
}
// =============
for(let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep4(source[key], hash);
} else {
target[key] = source[key];
}
}
}
return target;
}
方法二
function cloneDeep4(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source);
let target = Array.isArray(source) ? [] : {};
hash.set(source, target);
Reflect.ownKeys(source).forEach(key => { // 改动
if (isObject(source[key])) {
target[key] = cloneDeep4(source[key], hash);
} else {
target[key] = source[key];
}
});
return target;
}
这里使用了 Reflect.ownKeys()
获取所有的键值,同时包括 Symbol
,对 source 遍历赋值即可。
写到这里已经差不多了,我们再延伸下,对于 target
换一种写法,改动如下。
function cloneDeep4(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source);
let target = Array.isArray(source) ? [...source] : { ...source }; // 改动 1
hash.set(source, target);
Reflect.ownKeys(target).forEach(key => { // 改动 2
if (isObject(source[key])) {
target[key] = cloneDeep4(source[key], hash);
} // 改动3 else里面赋值也不需要了
});
return target;
}
- 递归爆栈问题
上面使用的都是递归方法,但是有一个问题在于会爆栈,错误提示如下。
// RangeError: Maximum call stack size exceeded
那应该如何解决呢?其实我们使用循环就可以了,代码如下。
function cloneDeep5(x) {
const root = {};
// 栈
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 广度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
参考文章:木易杨前端进阶