Автор: Конард Ли
Ссылка: https://juejin.cn/post/6844903929705136141
Нищий издание
В случае неиспользования сторонних библиотек мы хотим глубоко скопировать объект, и наиболее часто используется следующий метод.
JSON.parse(JSON.stringify());
Этот способ написания очень прост и подходит для большинства сценариев приложений, но у него все еще есть большие недостатки, такие как копирование других типов ссылок, копирование функций и циклических ссылок.
Очевидно, что вы не пройдете собеседование, если будете говорить только таким способом.
Далее давайте реализуем метод глубокого копирования вручную.
базовая версия
Если это поверхностная копия, мы можем легко написать следующий код:
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
};
Создайте новый объект, обойдите объект, который нужно клонировать, добавьте свойства клонированного объекта к новому объекту по очереди и вернитесь.
Если это глубокая копия, учитывая, что объект, который мы хотим скопировать, не знает, сколько слоев в глубину, мы можем использовать рекурсию для решения проблемы и немного переписать приведенный выше код:
- Если это примитивный тип, не нужно продолжать копирование, возвращайтесь напрямую
- Если это ссылочный тип, создайте новый объект, перейдите к объекту, который нужно клонировать, и выполните свойства клонированного объекта.После глубокого копированияДобавляйте к новым объектам по очереди.
Легко понять, что если есть более глубокие объекты, которые могут продолжать рекурсию до тех пор, пока свойство не станет примитивным типом, то мы выполнили простейшую глубокую копию:
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
Это глубокая копия самой базовой версии, но очевидно, что в ней еще много недочетов, например, она не считает массивы.
Рассмотрим массив
В приведенной выше версии наши результаты инициализации учитывают только обычныеobject
, нам нужно только немного изменить код инициализации, чтобы сделать массив совместимым:
module.exports = function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
Прецедент:
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
Результаты:
Хорошо, нет проблем, ваш код на один маленький шаг ближе к прохождению.
циклическая ссылка
Мы следуем такому тестируемому случаю:
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
target.target = target;
Вы можете увидеть следующие результаты:
Очевидно, стек переполняется, потому что рекурсия входит в бесконечный цикл.
Причина в том, что указанный выше объект имеет циклическую ссылку, то есть свойство объекта ссылается на себя косвенно или напрямую:
Чтобы решить проблему циклической ссылки, мы можем открыть дополнительное пространство для хранения для хранения соответствующей связи между текущим объектом и скопированным объектом.Когда текущий объект необходимо скопировать, сначала перейдите в пространство для хранения, чтобы узнать, есть ли объект был скопирован, и если это так, сразу верните, если нет, продолжите копирование, чтобы решить проблему циклической ссылки.
Это место для хранения должно иметь возможность хранитьkey-value
данные в формеkey
Может быть ссылочным типом, мы можем выбратьMap
Эта структура данных:
- экзамен
map
Есть ли клонированные объекты? - да - вернуться напрямую
- Нет — текущий объект рассматривается как
key
, клонировать объект какvalue
хранить - продолжать клонировать
function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
};
Затем выполните приведенный выше тестовый пример:
Видно, что ошибки в выполнении нет, иtarget
атрибут, становитсяCircular
Типа, то есть смысл кругового применения.
Далее мы можем использовать,WeakMap
ПриливMap
сделать код последним штрихом.
function clone(target, map = new WeakMap()) {
// ...
};
почему ты хочешь сделать это? , давайте сначала посмотримWeakMap
Роль:
Объект WeakMap — это набор пар ключ/значение, где на ключи слабо ссылаются. Его ключи должны быть объектами, а значения могут быть произвольными.
Что такое слабая ссылка?
В компьютерном программировании слабая ссылка противоположна сильной ссылке и относится к ссылке, которая не может гарантировать, что объект, на который она ссылается, не будет утилизирован сборщиком мусора. Объект считается недоступным (или слабодоступным), если на него ссылаются только слабые ссылки, и поэтому его можно собрать в любое время.
Мы создаем объект по умолчанию:const obj = {}
, строго ссылочный объект создается по умолчанию, нам нужно только вручнуюobj = null
, он будет собран механизмом сборки мусора.Если это объект слабой ссылки, механизм сборки мусора автоматически поможет нам его собрать.
Например:
если мы используемMap
Если это так, то между объектами существует сильная ссылочная связь:
let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;
Хотя мы вручнуюobj
, отпустить, тоtarget
все еще правobj
Существует сильная референтная связь, поэтому эта часть памяти все еще не может быть освобождена.
увидеть сноваWeakMap
:
let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;
еслиWeakMap
если,target
а такжеobj
Существует слабая ссылочная связь, и эта память будет освобождена при выполнении следующего механизма сборки мусора.
Представьте, что если объект, который мы хотим скопировать, очень большой, используйтеMap
Это вызовет очень большой дополнительный расход памяти, и нам нужно очистить ее вручнуюMap
свойство освобождать эту память, иWeakMap
Это поможет нам решить эту проблему с умом.
Я также часто вижу, как люди используют в каком-то кодеWeakMap
решить проблему циклической ссылки, но объяснение неоднозначно, когда вы не совсем понимаетеWeakMap
реальный эффект. Я предлагаю вам не писать такой код на собеседовании, вы можете только выкопать яму для себя, даже если вы готовитесь к собеседованию, каждая строка кода, которую вы пишете, должна быть хорошо продумана и понята.
Можете принять во внимание проблему циклических ссылок, вы показали интервьюеру, насколько всесторонне вы думаете о проблеме, если вы все еще можете использоватьWeakMap
Решите проблему и четко объясните интервьюеру цель этого, тогда ваш код будет считаться квалифицированным в глазах интервьюера.
оптимизация производительности
В приведенном выше коде мы перебираем как массив, так и объект, используяfor in
Таким образом, на самом делеfor in
КПД при обходе очень низкий, давайте сравним три обычных циклаfor、while、for in
Эффективность производительности:
можно увидеть,while
Эффективность наилучшая, поэтому мы можем найти способ поставитьfor in
обход изменен наwhile
траверс.
мы сначала используемwhile
реализовать общийforEach
траверс,iteratee
является функцией обратного вызова обхода, она может получать обходvalue
а такжеindex
Два параметра:
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
ниже для нашегоcloen
Переписана функция: при обходе массива использовать ее напрямуюforEach
Для обхода при обходе объекта используйтеObject.keys
убрать всеkey
обход, то при обходеforEach
функция вызоваvalue
так какkey
использовать:
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone2(target[key], map);
});
return cloneTarget;
} else {
return target;
}
}
Далее выполняемclone4.test.jsПротестируйте предыдущую функцию клонирования и переписанную функцию клонирования соответственно:
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};
target.target = target;
console.time();
const result = clone1(target);
console.timeEnd();
console.time();
const result2 = clone2(target);
console.timeEnd();
Результаты:
Понятно, что наша оптимизация производительности эффективна.
На этом этапе вы показали интервьюеру, что думаете об эффективности программы при написании кода и что у вас есть возможность абстрагировать общие функции.
другие типы данных
В приведенном выше коде мы на самом деле рассматриваем только обычныеobject
а такжеarray
Два типа данных, на самом деле всех ссылочных типов гораздо больше, чем этих двух, их гораздо больше, давайте сначала попробуем получить точный тип объекта.
Разумное суждение об эталонном типе
Во-первых, чтобы определить, является ли это ссылочным типом, нам также необходимо рассмотретьfunction
а такжеnull
Два специальных типа данных:
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
if (!isObject(target)) {
return target;
}
// ...
получить тип данных
мы можем использоватьtoString
чтобы получить точный ссылочный тип:
Каждый ссылочный тип имеет
toString
метод по умолчанию,toString()
метод каждогоObject
Наследование объекта. Если этот метод не переопределен в пользовательском объекте, toString()
вернуть"[object type]"
, где тип — это тип объекта.
Обратите внимание, что выше упомянуто, что если этот метод не переопределен в пользовательском объекте,toString
Достигнет желаемых результатов, на самом деле, большинство эталонных типов, таких какArray、Date、RegExp
Это все переписаноtoString
метод.
Мы можем напрямую позвонитьObject
Раскрыто на прототипеtoString()
метод, используяcall
изменитьthis
Точка для достижения эффекта, который мы хотим.
function getType(target) {
return Object.prototype.toString.call(target);
}
Ниже мы извлекаем некоторые часто используемые типы данных для последующего использования:
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
В приведенных выше централизованных типах мы просто делим их на две категории:
- Типы, по которым можно продолжить обход
- Типы, которые нельзя пройти дальше
Делаем отдельные копии для каждого из них.
Типы, по которым можно продолжить обход
Выше мы рассмотрелиobject
,array
Это все типы, по которым можно продолжать обход, потому что их память также может хранить данные других типов данных, и естьMap
,Set
И т. д. - это все типы, которые можно продолжать обходить. Здесь мы рассмотрим только эти четыре типа. Если вам интересно, вы можете продолжить изучение других типов.
Заказанные эти типы также должны продолжать рекурсию, нам сначала нужно получить их данные инициализации, такие как выше[]
а также{}
, мы можем получитьconstructor
способ получить общий доступ.
Например:const target = {}
то естьconst target = new Object()
синтаксический сахар. Кроме того, у этого метода есть еще одно преимущество: поскольку мы также используем метод построения исходного объекта, он может сохранять данные о прототипе объекта, если мы напрямую используем обычный{}
, то прототип должен быть потерян.
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
Далее мы переписываемclone
Функция, которая обрабатывает типы данных, которые можно продолжать просматривать:
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
}
// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value,map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value,map));
});
return cloneTarget;
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
мы выступаемclone5.test.jsПротестируйте следующий тестовый пример:
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
empty: null,
map,
set,
};
Результаты:
Нет проблем, все готово, идем дальше, давайте перейдем к другим типам:
Типы, которые нельзя продолжать обходить
Для остальных оставшихся типов мы классифицируем их как необрабатываемые типы данных и обрабатываем их по очереди:
Bool
,Number
,String
,String
,Date
,Error
Для этих типов мы можем создать новый объект напрямую с помощью конструктора и исходных данных:
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
default:
return null;
}
}
клонSymbol
Типы:
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
克隆正则:
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
На самом деле существует множество типов данных, о которых я здесь не писал, если вам интересно, вы можете продолжить их изучение и реализацию.
Можете написать это, интервьюер увидел строгость вашего рассмотрения проблемы, ваше понимание переменных и типов, правильноJS API
Профессионализм, я полагаю, что интервьюер начал видеть вас.
функция клонирования
Наконец-то вынес функцию клонирования отдельно.На самом деле функция клонирования не имеет практического сценария применения.Нет проблемы в использовании функции по одному и тому же адресу в памяти для двух объектов.Специально посмотрел.lodash
Обработка функций:
const isFunc = typeof value == 'function'
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
Можно видеть, что если будет обнаружено, что это функция, она вернется сразу без специальной обработки, но я обнаружил, что многие интервьюеры все еще стремятся задать этот вопрос, и, насколько я знаю, очень немногие могут быть решены. написано. . .
На самом деле этот способ не сложный, главное проверить, хорошо ли вы усвоили основы.
Во-первых, мы можем пройтиprototype
Чтобы отличить функцию стрелки вниз от обычной функции, функция стрелки неprototype
из.
мы можем напрямую использоватьeval
и строка функции для повторного создания функции стрелки, обратите внимание, что этот метод не работает для обычных функций.
Мы можем использовать регулярные выражения для обработки обычных функций:
Используйте обычный, чтобы вынуть тело функции и параметры функции, а затем используйтеnew Function ([arg1[, arg2[, ...argN]],] functionBody)
Конструктор реконструирует новую функцию:
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
console.log('普通函数');
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
Наконец, давайте выполнимclone6.test.jsПротестируйте следующий тестовый пример:
const map = new Map();
map.set('key', 'value');
map.set('ConardLi', 'code秘密花园');
const set = new Set();
set.add('ConardLi');
set.add('code秘密花园');
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
empty: null,
map,
set,
bool: new Boolean(true),
num: new Number(2),
str: new String(2),
symbol: Object(Symbol(1)),
date: new Date(),
reg: /\d+/,
error: new Error(),
func1: () => {
console.log('code秘密花园');
},
func2: function (a, b) {
return a + b;
}
};
Результаты:
наконец
Для лучшего чтения мы используем диаграмму, чтобы показать весь приведенный выше код:
Полный код:GitHub.com/con AR DL i/co ...
Видно, что небольшая глубокая копия по-прежнему скрывает много очков знаний.
Не задавайте себе самый минимум, если вы имеете дело только с одним вопросом на собеседовании, то вы, вероятно, будете готовиться к самому элементарному методу глубокого копирования, описанному выше.
Но цель интервьюера, исследующего вас, состоит в том, чтобы изучить ваши мыслительные способности во всех аспектах.Если вы напишете приведенный выше код, он может отражать ваши многомерные способности:
- Базовая реализация
- рекурсивная способность
- циклическая ссылка
- Учитывайте комплексность проблемы
- Понять истинное значение слабой карты
- много типов
- Учитывайте серьезность проблемы
- Методы создания различных типов ссылок, владение JS API
- Точно оценить тип данных и понять тип данных
- Общий обход:
- Написание кода может учитывать оптимизацию производительности
- Понимание эффективности централизованного обхода
- Возможность абстрагирования кода
- Функция копирования:
- Отличие стрелочных функций от обычных функций
- Знание регулярных выражений
в апплете
Функция клонирования завершена, но есть проблема с использованием, ее нельзя использовать в апплете, поэтому используйте JSON.parse(JSON.stringify(target)) для создания глубокой копии