предисловие
Почему назначение js-объектов иногда требует глубокого копирования?
// 基本数据类型赋值
var a = 'aaa';
var b = a;
console.log(a); // 'aaa'
console.log(b); // 'aaa'
b = 'bbb';
console.log(a); // 'aaa'
console.log(b); // 'bbb'
// 引用数据类型赋值
var a = { name: '张三'};
var b = a;
console.log(a); // {name: "张三"}
console.log(b); // {name: "张三"}
b.name = '李四';
b.age = 18;
console.log(a); // {name: "李四", age: 18}
console.log(b); // {name: "李四", age: 18}
Как видно из приведенного выше кода и рисунка, присвоение базового типа данных фактически копирует копию, и значения, идентифицируемые двумя переменными, не имеют к этому никакого отношения. После создания объекта эталонного типа данных объект сохраняется в памяти, в это время адрес, на который указывает переменная а, является адресом объекта в памяти, а затем переменной а присваивается переменная Б. На самом деле это один и тот же объект, поэтому независимо от того, изменяете ли вы переменную a или переменную b, они будут выводить одно и то же содержимое.
метод глубокого копирования
1. JSON.parse( JSON.stringify() ) сериализация и десериализация
// 测试数据
var test = { name: "test"};
var data = { a: "123",
b: 123,
c: true,
d: [43, 2],
e: undefined,
f: null,
g: function() { console.log("g"); },
h: new Set([3, 2, null]),
i: Symbol("fsd"),
j: test,
k: new Map([ ["name", "张三"], ["title", "Author"] ])
};
JSON.stringify(data)
Результаты
Видно, что свойства объекта данных в основном содержат все типы данных, но после стробирования JSON возвращаемое значение отсутствует.Причина в том, что когда JSON выполняет процесс стробирования, он сначала выполнит форматированный JSON для получения безопасное значение JSON, поэтому, если это небезопасное значение JSON, оно будет отброшено. Среди них три типа значений undefined, function и symbol являются небезопасными (включая циклическое присвоение свойства объекта объекту), поэтому после форматирования они отфильтровываются, а объекты в формате данных set и map , он тоже не корректно обрабатывался, а обрабатывался как пустой объект.
Еще один крайний пример// 测试数据
var data = {
name: 'foo',
child: null,
}
data.child = data
Циклическая ссылка на свойства этого объекта формирует замкнутый цикл, выполняет сериализацию и видит результат
Видно, что JSON-сериализация объекта, содержащего замкнутый цикл, привела к ошибке.
Поэтому при использовании сериализации JSON следует обратить внимание на то, чтобы не включать вышеуказанные типы данных.Однако в этом методе есть несколько хороших моментов.Давайте сначала рассмотрим пример.
// 测试数据
var test = { name: "test"};
var data = { a: "123",
b: 123,
c: true,
d: [43, 2],
e: test,
f: {
name: '张三',
age: 18,
likes: {
ball: ['足球','篮球']
}
}
};
JSON.stringify(data)
При работе с такими вложенными объектами или когда значение свойства является ссылкой на другой объект, оно может быть хорошо преобразовано в строку без потери данных, так что это является преимуществом этого подхода.
конкретный метод
function deepCopy(obj){
if(typeof obj === 'function'){
throw new TypeError('请传入正确的数据类型格式')
}
try {
let data = JSON.stringify(obj)
let newData = JSON.parse(data)
return newData
} catch(e) {
console.log(e)
}
}
2. Object.assign(target, source1, source2)
var data = {
a: "123",
b: 123,
c: true,
d: [43, 2],
e: undefined,
f: null,
g: function() { console.log("g"); },
h: new Set([3, 2, null]),
i: Symbol("fsd"),
k: new Map([ ["name", "张三"], ["title", "Author"] ])
};
var newData = Object.assign({},data)
console.log(newData)
Видно, что этот API может полностью скопировать все значения атрибутов типа данных в исходном объекте в новый объект.Это идеальный метод глубокого копирования, который мы ищем? Ответ — нет, можно сказать, что это только частичная глубокая копия или поверхностная копия, почему вы так говорите, а потом смотрите вниз.
var test = { name: '张三' }
var data = {
a: 123,
b: test
}
var newData = Object.assign({},data)
console.log(newData)
// { a: 123, b: { name: '张三' }}
test.age = 18
console.log(newData)
// { a: 123, b: { name: '张三', age: 18 }}
3. Итеративно-рекурсивный метод
Нечего сказать, сначала прикрепите код, который я написал
function deepCopy(data) {
if(typeof data !== 'object' || data === null){
throw new TypeError('传入参数不是对象')
}
let newData = {};
const dataKeys = Object.keys(data);
dataKeys.forEach(value => {
const currentDataValue = data[value];
// 基本数据类型的值和函数直接赋值拷贝
if (typeof currentDataValue !== "object" || currentDataValue === null) {
newData[value] = currentDataValue;
} else if (Array.isArray(currentDataValue)) {
// 实现数组的深拷贝
newData[value] = [...currentDataValue];
} else if (currentDataValue instanceof Set) {
// 实现set数据的深拷贝
newData[value] = new Set([...currentDataValue]);
} else if (currentDataValue instanceof Map) {
// 实现map数据的深拷贝
newData[value] = new Map([...currentDataValue]);
} else {
// 普通对象则递归赋值
newData[value] = deepCopy(currentDataValue);
}
});
return newData;
}
Затем напишите тестовые данные, чтобы проверить их.
// 测试数据项
var data = {
age: 18,
name: "liuruchao",
education: ["小学", "初中", "高中", "大学", undefined, null],
likesFood: new Set(["fish", "banana"]),
friends: [
{ name: "summer", sex: "woman"},
{ name: "daWen", sex: "woman"},
{ name: "yang", sex: "man" } ],
work: {
time: "2019",
project: { name: "test",obtain: ["css", "html", "js"]}
},
play: function() { console.log("玩滑板"); }
}
Результаты
По сути, он может удовлетворить глубокую копию значения часто используемой структуры данных, но поскольку существует множество структур данных объектов js, он не может покрыть их все.Например, new Number(), объект-упаковщик этого базового тип данных, не обрабатывается. Поэтому при его использовании вы можете сначала сделать предварительный выбор объекта для глубокого копирования, чтобы решить, какой метод использовать.
Также попробуйте еще раз, в первом способе этот крайний пример с замкнутыми объектами
// 测试数据
var data = {
name: 'foo',
child: null,
}
data.child = data
deepCopy(data)
Ау, стек сразу переполнился, и стек взорвался! ! Рекурсивный метод действительно нужно использовать с осторожностью, если не обращать внимания, он слишком много запихнет в стек, можно только продолжать оптимизировать эту функцию.
4. Итеративно-рекурсивные методы (решение задач с обратной связью)
function deepCopy(data, hash = new WeakMap()) {
if(typeof data !== 'object' || data === null){
throw new TypeError('传入参数不是对象')
}
// 判断传入的待拷贝对象的引用是否存在于hash中
if(hash.has(data)) {
return hash.get(data)
}
let newData = {};
const dataKeys = Object.keys(data);
dataKeys.forEach(value => {
const currentDataValue = data[value];
// 基本数据类型的值和函数直接赋值拷贝
if (typeof currentDataValue !== "object" || currentDataValue === null) {
newData[value] = currentDataValue;
} else if (Array.isArray(currentDataValue)) {
// 实现数组的深拷贝
newData[value] = [...currentDataValue];
} else if (currentDataValue instanceof Set) {
// 实现set数据的深拷贝
newData[value] = new Set([...currentDataValue]);
} else if (currentDataValue instanceof Map) {
// 实现map数据的深拷贝
newData[value] = new Map([...currentDataValue]);
} else {
// 将这个待拷贝对象的引用存于hash中
hash.set(data,data)
// 普通对象则递归赋值
newData[value] = deepCopy(currentDataValue, hash);
}
});
return newData;
}
По сравнению с предыдущей версией 1.0 существует контейнер WeakMap для хранения объектов.Идея состоит в том, что при первом вызове deepCopy параметр создаст объект структуры WeakMap.Одной из характеристик этой структуры данных является то, что ключ в сохраненной паре ключ-значение должен быть типом объекта.
- При первом вызове weakMap пуст, и приведенный выше оператор if(hash.has()) не будет выполнен.Если в копируемом объекте есть атрибуты, которые также являются объектами, копируемый объект будет хранится в weakMap. И значение, и имя являются ссылками на копируемый объект
- Затем вызовите функцию рекурсивно
- Введите функцию еще раз и передайте ссылку атрибута объекта последнего копируемого объекта и weakMap, в котором хранится ссылка последнего копируемого объекта, потому что, если это замкнутый цикл, созданный циклической ссылкой, то эти две ссылки указывают на один и тот же объект, поэтому он войдет в оператор if(hash.has()), затем вернется и выйдет из функции, поэтому он не будет рекурсивно помещаться в стек, чтобы предотвратить переполнение стека.
Суммировать
Независимо от преимуществ и недостатков вышеперечисленных методов, общим моментом является то, что копировать можно только перечисляемые свойства объекта, а неперечислимые свойства или свойства-прототипы копировать нельзя, но для базового использования этого достаточно. Наконец, если в тексте есть ошибки или упущения или у вас есть предложения получше, добро пожаловать на общение со мной.
Если эта статья вам чем-то помогла, пожалуйста, поставьте палец вверх, это тоже своего рода поощрение для пары 😀!