数组直接赋值、浅拷贝、深拷贝
在开发过程中,对于一个后端返回结果,我们可能需要存成两个变量,用于做一些修改和复原,其中可能遇到的问题是:
将数组list 直接赋值给list1和list2.修改了list2导致list1也变了。
造成错误的原因是:
数组是引用类型(对象),使用type of 判断类型返回的是object。因此直接将一个数组赋值给多个变量时,这些变量会共享同一个数组引用。修改其中一个变量会导致其他变量也受到影响,因为它们指向同一个数组。
示例代码
let list = [1, 2, 3];
let list1 = list;
let list2 = list;
list2.push(4);
console.log(list1); // 输出: [1, 2, 3, 4]
console.log(list2); // 输出: [1, 2, 3, 4]
在这个例子中,list1 和 list2 都指向同一个数组 list,所以修改 list2 会导致 list1 也发生变化。
解决方法:
如果你希望 list1 和 list2 是两个独立的数组,可以使用以下方法:
1. 使用 slice() 方法
slice() 方法会返回数组的一个浅拷贝。
let list = [1, 2, 3];
let list1 = list.slice();
let list2 = list.slice();
list2.push(4);
console.log(list1); // 输出: [1, 2, 3]
console.log(list2); // 输出: [1, 2, 3, 4]
2. 使用扩展运算符 (...)
扩展运算符可以快速创建一个数组的浅拷贝。
let list = [1, 2, 3];
let list1 = [...list];
let list2 = [...list];
list2.push(4);
console.log(list1); // 输出: [1, 2, 3]
console.log(list2); // 输出: [1, 2, 3, 4]
3. 使用 Array.from()
Array.from() 也可以创建一个数组的浅拷贝。
let list = [1, 2, 3];
let list1 = Array.from(list);
let list2 = Array.from(list);
list2.push(4);
console.log(list1); // 输出: [1, 2, 3]
console.log(list2); // 输出: [1, 2, 3, 4]
4. 使用 JSON.parse(JSON.stringify())(深拷贝)
如果数组包含嵌套对象或数组,可以使用 JSON.parse(JSON.stringify()) 进行深拷贝。
let list = [1, 2, { a: 3 }];
let list1 = JSON.parse(JSON.stringify(list));
let list2 = JSON.parse(JSON.stringify(list));
list2[2].a = 4;
console.log(list1); // 输出: [1, 2, { a: 3 }]
console.log(list2); // 输出: [1, 2, { a: 4 }]
注意事项
浅拷贝:slice()、扩展运算符 (…) 和 Array.from()
都是浅拷贝,只能复制一层。如果数组包含嵌套对象或数组,嵌套部分仍然是引用。深拷贝:JSON.parse(JSON.stringify()) 可以实现深拷贝,但它无法处理函数、undefined 和循环引用的情况。
JSON.parse(JSON.stringify()) 深拷贝方法的局限性
无法正确处理以下情况:
1. 函数(Function)
JavaScript 中的函数无法被 JSON.stringify() 序列化。如果对象中包含函数,JSON.stringify() 会直接忽略它们。
示例
let obj = {
name: "Alice",
greet: function() {
console.log("Hello!");
}
};
let copy = JSON.parse(JSON.stringify(obj));
console.log(copy); // 输出: { name: "Alice" }
原对象中的 greet 函数在拷贝后的对象中丢失了。
2. undefined
JSON.stringify() 会忽略值为 undefined 的属性。
示例
let obj = {
name: "Alice",
age: undefined
};
let copy = JSON.parse(JSON.stringify(obj));
console.log(copy); // 输出: { name: "Alice" }
原对象中的 age 属性(值为 undefined)在拷贝后的对象中丢失了。
3. 循环引用
循环引用是指对象属性直接或间接地引用自身。JSON.stringify() 无法处理循环引用,会直接抛出错误。
示例
let obj = {
name: "Alice"
};
obj.self = obj; // 循环引用
try {
let copy = JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error(error); // 报错: TypeError: Converting circular structure to JSON
}
由于 obj 引用了自身,JSON.stringify() 无法将其转换为字符串,导致报错。
4. 其他特殊情况
NaN 和 Infinity:JSON.stringify() 会将 NaN 和 Infinity 转换为 null。
Date 对象:JSON.stringify() 会将 Date 对象转换为字符串,反序列化后不会恢复为 Date 对象。
RegExp 对象:JSON.stringify() 会将其转换为空对象 {}。
示例
let obj = {
date: new Date(),
regex: /abc/g,
nan: NaN,
infinity: Infinity
};
let copy = JSON.parse(JSON.stringify(obj));
console.log(copy);
// 输出:
// {
// date: "2023-10-10T12:00:00.000Z", // Date 对象被转换为字符串
// regex: {}, // RegExp 对象被转换为空对象
// nan: null, // NaN 被转换为 null
// infinity: null // Infinity 被转换为 null
// }
如何解决这些问题?
如果需要处理函数、undefined、循环引用等特殊情况,可以使用以下方法:
1. 使用深拷贝库
一些第三方库(如 Lodash 的 _.cloneDeep)可以正确处理这些特殊情况。
let _ = require('lodash');
let obj = {
name: "Alice",
greet: function() {
console.log("Hello!");
},
self: null
};
obj.self = obj; // 循环引用
let copy = _.cloneDeep(obj);
console.log(copy); // 正确拷贝,包括函数和循环引用
2. 手动实现深拷贝
可以编写递归函数来实现深拷贝,处理函数、undefined 和循环引用等特殊情况。
function deepClone(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj; // 基本类型直接返回
}
if (cache.has(obj)) {
return cache.get(obj); // 解决循环引用
}
let result = Array.isArray(obj) ? [] : {};
cache.set(obj, result);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key], cache);
}
}
return result;
}
let obj = {
name: "Alice",
greet: function() {
console.log("Hello!");
},
self: null
};
obj.self = obj; // 循环引用
let copy = deepClone(obj);
console.log(copy); // 正确拷贝,包括函数和循环引用
总结
JSON.parse(JSON.stringify()) 是一种简单但有限的深拷贝方法。
如果需要处理函数、undefined、循环引用等特殊情况,可以使用第三方库(如 Lodash)或手动实现深拷贝。