предисловие
Объекты являются одним из основных типов в JS и тесно связаны с такими знаниями, как цепочки прототипов и массивы. Будь то в интервью или в реальной разработке, мы столкнемся с проблемой глубокого копирования объектов.
Как следует из названия, глубокая копия — это полная копия объекта из памяти. Таким образом, независимо от того, какой метод используется, открытие нового пространства памяти неизбежно.
Обычно есть два способа реализации глубокого копирования:
- итеративная рекурсия
- сериализация десериализация
Мы протестируем часто используемые методы реализации на тестовом примере и сравним плюсы и минусы:
let test = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
func: function() {
console.log('我是一个函数')
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
err: new Error('我是一个错误')
}
let result = deepClone(test)
console.log(result)
for (let key in result) {
if (isObject(result[key]))
console.log(`${key}相同吗? `, result[key] === test[key])
}
// 判断是否为对象
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
1. Метод итеративной рекурсии
Это самый традиционный метод, и идея очень проста: перебрать объект и сделать рекурсивную глубокую копию каждого его значения.
-
for...inЗакон
// 迭代递归法:深拷贝对象与数组
function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [] : {}
for (let key in obj) {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
}
return cloneObj
}
结果:
我们发现,arr 和 obj 都深拷贝成功了,它们的内存引用已经不同了,但 func、date、reg 和 err 并没有复制成功,因为它们有特殊的构造函数。
-
Метод отражения
// 代理法
function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [...obj] : { ...obj }
Reflect.ownKeys(cloneObj).forEach(key => {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return cloneObj
}
结果:
我们发现,结果和使用 for...in 一样。那么它有什么优点呢?读者可以先猜一猜,答案我们会在下文揭晓。
-
глубокая копия в lodash
Метод cloneDeep в знаменитом lodash также реализован с использованием этого метода, но он поддерживает больше типов объектов.Читатели могут обратиться к конкретному процессу реализации.метод baseClone от lodash.
Мы заменяем функцию глубокого копирования, используемую в тестовом примере, на функцию lodash:
let result = _.cloneDeep(test)
результат:
Мы обнаружили, что глубокая копия arr, obj, date и reg выполнена успешно, но ссылки на память функций func и err остались без изменений.
Почему бы не изменить? Этот вопрос предоставляется читателю для самостоятельного изучения, хе-хе~ Но вы можете напомнить, что это связано с cloneableTags в lodash.
Поскольку во внешнем интерфейсе слишком много типов объектов, lodash также предоставляет пользователям собственный метод глубокого копирования.cloneDeepWith, такие как настраиваемые объекты DOM с глубоким копированием:
function customizer(value) { if (_.isElement(value)) { return value.cloneNode(true); } } var el = _.cloneDeepWith(document.body, customizer); console.log(el === document.body); // => false console.log(el.nodeName); // => 'BODY' console.log(el.childNodes.length); // => 20
2. Метод сериализации и десериализации
Этот метод очень интересен, он сначала сериализует код в данные, а потом десериализует обратно в объект:
// 序列化反序列化法
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}
результат:
Мы обнаружили, что он может только глубоко копировать объекты и массивы, а для других типов объектов он будет искажен. Этот метод больше подходит для использования в обычной разработке, потому что обычно нет необходимости рассматривать другие типы, кроме объектов и массивов.углубленный, углубленный, углубленный
-
Что, если объекты образуют петлю? Мы добавляем ключ loopObj для проверки со значением, указывающим на себя:
test.loopObj = test
На данный момент мы используем реализацию for..in в первом методе, и реализация Reflect переполнит стек:
Использование второго метода также сообщит об ошибке:
Но lodash может получить правильный результат:
Зачем? Давайте перейдем к исходному коду lodash, чтобы увидеть:
Поскольку lodash использует стек для хранения объекта, если есть кольцевой объект, он будет обнаружен из стека, и результат будет возвращен напрямую, обуздав пропасть. Идея этого алгоритма исходит из спецификации HTML5, определеннойАлгоритм структурированного клонирования, что также объясняет, почему lodash не делает копии типов Error и Function.
Конечно, создание хеш-таблицы для хранения скопированных объектов также может достичь той же цели:
function deepClone(obj, hash = new WeakMap()) { if (!isObject(obj)) { return obj } // 查表 if (hash.has(obj)) return hash.get(obj) let isArray = Array.isArray(obj) let cloneObj = isArray ? [] : {} // 哈希表设值 hash.set(obj, cloneObj) let result = Object.keys(obj).map(key => { return { [key]: deepClone(obj[key], hash) } }) return Object.assign(cloneObj, ...result) }
Здесь мы используем WeakMap в качестве хэш-таблицы, потому что на ее ключи слабо ссылаются, а в нашем сценарии ключи являются объектами и нуждаются в слабых ссылках.
-
ключи не строки, а символы
Давайте изменим тестовый пример:
var test = {} let sym = Symbol('我是一个Symbol') test[sym] = 'symbol' let result = deepClone(test) console.log(result) console.log(result[sym] === test[sym])
Запустив глубокую копию реализации for...in, мы находим:
Не удалось скопировать, почему?
так какSymbolЭто особый тип данных, его самая большая особенность уникальна, поэтому его глубокая копия является поверхностной копией.
Но если мы используем версию, реализованную Reflect на данный момент:
удается, потому что for...in не может получить ключ типа Symbol , аReflectдоступен.
Конечно, мы также можем изменить for...in реализацию:
function deepClone(obj) { if (!isObject(obj)) { throw new Error('obj 不是一个对象!') } let isArray = Array.isArray(obj) let cloneObj = isArray ? [] : {} let symKeys = Object.getOwnPropertySymbols(obj) // console.log(symKey) if (symKeys.length > 0) { symKeys.forEach(symKey => { cloneObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey]) : obj[symKey] }) } for (let key in obj) { cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] } return cloneObj }
-
Копировать свойства на прототип
Как мы все знаем, объекты JS разрабатываются на основе цепочки прототипов, поэтому, когда свойства объекта не могут быть найдены, он будет искаться по цепочке прототипов, то есть объекту, не являющемуся конструктором.__proto__Атрибуты.
Мы создаем переменную childTest и позволяем результату быть результатом ее глубокого копирования, в противном случае без изменений:
let childTest = Object.create(test) let result = deepClone(childTest)
На данный момент из четырех реализаций, которые мы изначально предоставили, только реализация for...in может быть скопирована правильно.Почему? Причина все ещеАлгоритм структурированного клонированияВнутри: свойства в цепочке прототипов не будут отслеживаться и копироваться.
Дело в конкретной реализации: for...in будет отслеживать свойства в цепочке прототипов, тогда как остальные три метода (Object.keys, Reflect.ownKeys и метод JSON) не будут отслеживать свойства в цепочке прототипов:
-
Необходимо скопировать неперечислимые свойства
Четвертый случай заключается в том, что нам нужно скопировать неперечислимые свойства, такие как дескрипторы свойств, сеттеры и геттеры, вообще говоря, для их хранения требуется дополнительная неперечислимая коллекция свойств. Аналогично использованию for...in для копирования ключей типов символов во втором случае: Давайте определим дескрипторы свойств obj и arr в тестовой переменной:
Object.defineProperties(test, { 'obj': { writable: false, enumerable: false, configurable: false }, 'arr': { get() { console.log('调用了get') return [1,2,3] }, set(val) { console.log('调用了set') } } })
Затем реализуем нашу версию копирования неперечислимых свойств:
function deepClone(obj, hash = new WeakMap()) { if (!isObject(obj)) { return obj } // 查表,防止循环拷贝 if (hash.has(obj)) return hash.get(obj) let isArray = Array.isArray(obj) // 初始化拷贝对象 let cloneObj = isArray ? [] : {} // 哈希表设值 hash.set(obj, cloneObj) // 获取源对象所有属性描述符 let allDesc = Object.getOwnPropertyDescriptors(obj) // 获取源对象所有的 Symbol 类型键 let symKeys = Object.getOwnPropertySymbols(obj) // 拷贝 Symbol 类型键对应的属性 if (symKeys.length > 0) { symKeys.forEach(symKey => { cloneObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey], hash) : obj[symKey] }) } // 拷贝不可枚举属性,因为 allDesc 的 value 是浅拷贝,所以要放在前面 cloneObj = Object.create( Object.getPrototypeOf(cloneObj), allDesc ) // 拷贝可枚举属性(包括原型链上的) for (let key in obj) { cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key]; } return cloneObj }
результат:
Эпилог
- Для ежедневного глубокого копирования рекомендуются методы сериализации и десериализации.
- Во время интервью интервьюер что-то делает: напишите ключ, который может копировать свой собственный перечислимый, свой неисчислимый, свой собственный ключ типа символа, прототип перечислимого прототипа, прототип неперечислимого, прототип ключа типа символа и циклическую ссылку. функция глубокого копирования:
// 将之前写的 deepClone 函数封装一下
function cloneDeep(obj) {
let family = {}
let parent = Object.getPrototypeOf(obj)
while (parent != null) {
family = completeAssign(deepClone(family), parent)
parent = Object.getPrototypeOf(parent)
}
// 下面这个函数会拷贝所有自有属性的属性描述符,来自于 MDN
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
function completeAssign(target, ...sources) {
sources.forEach(source => {
let descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key)
return descriptors
}, {})
// Object.assign 默认也会拷贝可枚举的Symbols
Object.getOwnPropertySymbols(source).forEach(sym => {
let descriptor = Object.getOwnPropertyDescriptor(source, sym)
if (descriptor.enumerable) {
descriptors[sym] = descriptor
}
})
Object.defineProperties(target, descriptors)
})
return target
}
return completeAssign(deepClone(obj), family)
}
-
Для глубоких копий с особыми потребностями рекомендуется использовать методы lodash copyDeep или copyDeepWith.
Наконец спасибоЗнать об этой проблемеВдохновение, что бы вы ни делали, старайтесь не усложнять простые вещи. Глубокое копирование не обязательно. Проблемы, с которыми оно сталкивается, часто можно решить более элегантным способом, например, с помощью функции для получения объекта. Конечно, во время интервью Возможен толчок.