глубокая копия

JavaScript

Автор: Конард Ли
Ссылка: 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)) для создания глубокой копии