«Конфиденциальность интервью» — добиться глубокого копирования

опрос

предисловие

Функция глубокого копирования часто используется при разработке, особенно при работе с данными ссылочного типа, глубокая копия обычно назначается переменной, а затем используется для предотвращения ее влияния на другие места, где используются данные.

Как добиться глубокого копирования, частота появления в интервью была высокой. Потому что в процессе реализации глубокой копии видно, что способности заявителя во многих аспектах.

В этой колонке рассказывается, как достичь глубокой копии от бронзы до короля, и о соответствующих способностях каждого ранга.

Бронзовый ранг

JSON.parse(JSON.stringify(data))

Этот способ написания очень прост и подходит для большинства сценариев приложений, но у него есть серьезные недостатки. Если вы не знаете, что у него есть эти недостатки, и эта реализация не отражает ни одной из ваших способностей, значит, эта реализация относится к бронзовому уровню.

  • Глубокое копирование не может быть реализовано правильно, если в объекте есть циклические ссылки.
const a = {
    b: 1,
}
a.c = a;
JSON.parse(JSON.stringify(a));

  • еслиdataВ нем есть объект времени, тогдаJSON.stringifyпозжеJSON.parseВ результате время будет просто в виде строки. а не объект времени.
const a = {
    b: new Date(1536627600000),
}
console.log(JSON.parse(JSON.stringify(a)))

  • еслиdataЕсли есть объекты RegExp и Error, результатом сериализации будут только пустые объекты;
const a = {
    b: new RegExp(/\d/),
    c: new Error('错误')
}
console.log(JSON.parse(JSON.stringify(a)))

  • еслиdataесть функции,undefined, сериализованный результат сделает функцию неопределенной или потерянной;
const a = {
    b: function (){
        console.log(1)
    },
    c:1,
	d:undefined
}
console.log(JSON.parse(JSON.stringify(a)))

  • еслиdataЕсли есть NaN, Infinity и -Infinity, сериализованный результат станет нулевым.
const a = {
    b: NaN,
    c: 1.7976931348623157E+10308,
    d: -1.7976931348623157E+10308,
}
console.log(JSON.parse(JSON.stringify(a)))

серебряный уровень

Ядром глубокой копии является копирование данных ссылочного типа.

function deepClone(target){
    if(target !== null && typeof target === 'object'){
        let result = {}
        for (let k in target){
            if (target.hasOwnProperty(k)) {
                result[k] = deepClone(target[k])
            }
        }
        return result;
    }else{
        return target;
    }
}

В приведенном выше кодеdeepCloneаргументы функцииtargetэто данные для глубокого копирования.

воплощать в жизньtarget !== null && typeof target === 'object'судитьtargetне является ссылочным типом.

Если нет, вернитесь напрямуюtarget.

Если да, создайте переменнуюresultВ результате глубокого копированияtarget,воплощать в жизньdeepClone(target[k])ПучокtargetПосле глубокого копирования значения каждого атрибута присвойте его атрибуту, соответствующему результату глубокого копирования.result[k]Вкл, возврат после траверсаresult.

в исполненииdeepClone(target[k]), встретимся сноваtarget[k]Выполните суждение о типе, повторите описанный выше процесс и сформируйте рекурсивный вызов.deepCloneфункциональный процесс. Вы можете перемещаться по копируемым данным слой за слоем, независимо от того, сколько вложенных атрибутов имеют копируемые данные, если тип значения вложенного атрибута является ссылочным типом, он будет вызыватьdeepCloneФункция назначает глубокую копию свойству, соответствующему результату глубокой копии.

дополнительно использоватьfor...inПри циклическом просмотре свойств объекта будут доступны все свойства в его цепочке прототипов.Если вам нужно пройти только свойства самого объекта, но не свойства, унаследованные от цепочки прототипов, вам нужно использоватьhasOwnPropertyметод фильтрации.

Здесь вы можете показать интервьюеру свои три способности в программировании.

  • Возможность судить о примитивных и эталонных данных.
  • Способность применять рекурсивное мышление.
  • Глубокое пониманиеfor...inПрименение.

золотой ранг

Код серебряного ранга рассматривает только случай, когда данные ссылочного типа являются объектом, опуская случай, когда данные ссылочного типа представляют собой массив.

function deepClone(target){
    if(target !== null && typeof target === 'object'){
        let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
        for (let k in target){
            if (target.hasOwnProperty(k)) {
                result[k] = deepClone(target[k])
            }
        }
        return result;
    }else{
        return target;
    }
}

В приведенном выше коде добавляются только дополнительные параметрыtargetЯвляется ли это решением массива. воплощать в жизньObject.prototype.toString.call(target) === "[object Array]"судитьtargetЭто массив, если это массив, то переменнаяresultдля[], если не массив, то переменнаяresultдля{}.

Здесь вы можете показать интервьюеру две свои способности в программировании.

  • Способность правильно понимать концепцию ссылочных типов.
  • Возможность точного определения типов данных.

Платиновый уровень

Предположим, вы хотите глубоко скопировать следующие данныеdata

let data = {
    a: 1
};
data.f=data

воплощать в жизньdeepClone(data), вы обнаружите, что консоль сообщает об ошибке, и сообщение об ошибке выглядит следующим образом.

Это связано с тем, что рекурсия входит в бесконечный цикл и память стека переполняется. Основная причинаdataСуществует циклическая ссылка на данные, то есть свойство объекта ссылается на себя косвенно или прямо.

function deepClone(target) {
    function clone(target, map) {
        if (target !== null && typeof target === 'object') {
            let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
            if (map[target]) {
                return map[target];
            }
            map[target] = result;
            for (let k in target) {
                if (target.hasOwnProperty(k)) {
                    result[k] = clone(target[k],map)
                }
            }
            return result;
        } else {
            return target;
        }
    }
    let map = new Map();
    const result = clone(target, map);
    map.clear();
    map = null;
    return result
}

В приведенном выше коде используются дополнительные переменныеmapЧтобы сохранить соответствующую связь между текущим объектом и скопированным объектом, когда текущий объект необходимо скопировать, перейдите кmapУзнайте, копировали ли вы когда-либо этот объект, если есть, верните его напрямую, если нет, продолжайте копировать, чтобы гениально решить проблему циклической ссылки.

Причина, по которой используется структура данных Map в ES6, заключается в том, что по сравнению с обычной структурой Object значение ее ключа не ограничивается строками, и в качестве ключей могут использоваться различные типы значений (включая объекты). Другими словами. Структура Object обеспечивает соответствие "строка-значение", а структура Map обеспечивает соответствие "значение-значение". Если используется структура Object, когдаtargetКогда это не строка, то все ее ключевые значения[Object Object], вызовет путаницу.

Наконец нужно выполнитьmap.clear();map = null;, освободите память, чтобы предотвратить утечку памяти.

Здесь вы можете показать интервьюеру свои три способности в программировании.

  • Понимание циклических ссылок и способность решать проблемы, вызванные циклическими ссылками.
  • Знаком с концепцией и применением структуры данных Map в ES6.
  • Осведомленность об утечках памяти и возможность избежать утечек.

ряд каменной кладки

В этом разделе должны быть рассмотрены вопросы производительности. Вышеприведенное использует Map для решения проблемы циклических ссылок. Но в конце концов это должно быть сделаноmap.clear();map = null;, освободите память, чтобы предотвратить утечку памяти.

Вы также можете использовать WeakMap для разрешения циклических ссылок. Между структурой данных WeakMap и структурой данных Map есть два отличия:

  • WeakMap принимает в качестве ключей только объекты (кроме null), и не принимает в качестве ключей другие типы значений. ноtargetОн просто соответствует требованиям, поэтому не влияет на него.

  • Объект, на который указывает имя ключа WeakMap, не включается в механизм сборки мусора. Здесь поясняется на следующем примере.

let map = new WeakMap();
let obj = {a : 1}
map.set(obj , 1);

Современная стратегия повторного использования сборщика мусора в браузере использует количество ссылок, подлежащих повторному использованию, то есть количество ссылок на объект равно 1, потому что имя ключа в картеobjи объектobjСсылка между является слабой ссылкой, то есть не учитывается в числе ссылок, объектobjКоличество цитирований по-прежнему 1. при исполненииobj = nullПосле выполнения сбора GC объектobjКоличество ссылок становится равным 0, поэтомуobjЗанятая память освобождается.

Но если карта представляет собой структуру данных карты, поскольку объект, на который ссылается имя ключа карты, является строгой ссылкой, даже если выполнениеobj = nullПосле этого выполните восстановление GC, имя ключа Картыobjк указанному объектуobjЕсть еще ссылки, тоobjКоличество цитирований по-прежнему равно 1, поэтомуobjЗанятая память не может быть освобождена.

Таким образом, использование WeakMap позволяет не забыть установить карту наnullчто приводит к утечке памяти. Однако является ли эта слабая ссылка на WeakMap только для предотвращения утечек памяти? Он также имеет функцию, которая при выполненииobj = null, эквивалентный объектobjВосстановлен GC, но выполняетсяmap.get(obj)Результирующее значение по-прежнему равно 1. Мы можем использовать эту функцию для оптимизации производительности.

в глубоко скопированном коде. Скопированный объект используется в качестве имени ключа.Представьте, что когда копируемый объект очень большой, это вызоветmapБудет занимать много памяти.

еслиmapИспользуя структуру данных карты, поскольку объект, на который ссылается имя ключа карты, является строгой ссылкой, когда браузер периодически выполняет перезапуск GC, память, занятая скопированным объектом, может быть освобождена, если только пара ключ-значение карты не очищается вручную.

еслиmapИспользуя структуру данных WeakMap, поскольку объект, на который ссылается имя ключа WeakMap, является слабой ссылкой, браузер будет периодически выполнять перезапуск GC и напрямую освобождать память, занятую скопированным объектом, поэтому это уменьшает использование памяти и косвенно оптимизирована производительность выполнения кода.

Также в приведенном выше коде мы перебираем как массив, так и объект, используяfor...inТаким образом, на самом делеfor...inЭффективность при обходе очень низкая, поэтому эффективнее использоватьwhileпройти.

function deepClone(target) {
    /**
     * 遍历数据处理函数
     * @array 要处理的数据
     * @callback 回调函数,接收两个参数 value 每一项的值 index 每一项的下标或者key。
    */
    function handleWhile(array, callback) {
        const length = array.length;
        let index = -1;
        while (++index < length) {
            callback(array[index], index)
        }
    }
    function clone(target, map) {
        if (target !== null && typeof target === 'object') {
            let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
            // 解决循环引用
            if (map.has(target)) {
                return map.get(target);
            }
            map.set(target, result);
            
            const keys = Object.prototype.toString.call(target) === "[object Array]" ? undefined : Object.keys(
                target);

            function callback(value, key) {
                if (keys) {
                    // 如果keys存在则说明value是一个对象的key,不存在则说明key就是数组的下标。
                    key = value;
                }
                result[key] = clone(target[key], map)
            }
            handleWhile(keys || target, callback)
            return result;
        } else {
            return target;
        }
    }
    let map = new WeakMap();
    const result = clone(target,map);
    map = null;
    return result
}

использоватьwhileПройденная глубокая копия обозначается какdeepClone, использоватьfor ... inПройденная глубокая копия обозначается какdeepClone1. использоватьconsole.time()а такжеconsole.timeEnd()для расчета времени выполнения.

let arr = [];
for (let i = 0; i < 1000000; i++) {
    arr.push(i)
}
let data = {
    a: arr
};
console.time();
const result = deepClone(data);
console.timeEnd();
console.time();
const result1 = deepClone1(data);
console.timeEnd();

Из приведенного выше рисунка видно, чтоwhileПроизводительность глубокой копии обхода намного выше, чем при использованииfor ... inГлубокая копия обхода.

Вот пять навыков программирования, которые вы можете показать интервьюеру.

  • Знаком с концепцией и применением структуры данных WeakMap в ES6.
  • Имеет возможность оптимизировать производительность выполнения кода.
  • Способность понять эффективность обхода.
  • К пониманию++iа такжеi++разница.
  • Способность кода абстрагироваться.

Звёздный ранг

На этом этапе следует учитывать строгость логики кода. Хотя код в приведенном выше абзаце удовлетворил потребности нормальной разработки, все еще есть несколько мест, где логика не является строгой.

  • Немедленный возврат, если установлено, что данные не относятся к ссылочному типу.target, но в исходном типе также есть специальный тип данных, Symbol, потому что каждый Symbol уникален и требует дополнительной обработки копирования и не может быть возвращен напрямую.

  • Это не является строгим при оценке того, являются ли данные ссылочным типом или нет.typeof target === function'суждение.

  • Учитывается только обработка данных ссылочного типа Array и Object.Данные ссылочного типа также включают функцию Function, дату Date, регулярность RegExp, структуру данных Map и организацию данных Set, среди которых Map и Set относятся к ES6.

Не много ерунды, вставьте весь код напрямую, в коде есть комментарии.

function deepClone(target) {
    // 获取数据类型
    function getType(target) {
        return Object.prototype.toString.call(target)
    }
    //判断数据是不是引用类型
    function isObject(target) {
        return target !== null && (typeof target === 'object' || typeof target === 'function');
    }
    //处理不需要遍历的应引用类型数据
    function handleOherData(target) {
        const type = getType(target);
        switch (type) {
            case "[object Date]":
                return new Date(target)
            case "[object RegExp]":
                return cloneReg(target)
            case "[object Function]":
                return cloneFunction(target)

        }
    }
    //拷贝Symbol类型数据
    function cloneSymbol(targe) {
        const a = String(targe); //把Symbol字符串化
        const b = a.substring(7, a.length - 1); //取出Symbol()的参数
        return Symbol(b); //用原先的Symbol()的参数创建一个新的Symbol
    }
    //拷贝正则类型数据
    function cloneReg(target) {
        const reFlags = /\w*$/;
        const result = new target.constructor(target.source, reFlags.exec(target));
        result.lastIndex = target.lastIndex;
        return result;
    }
    //拷贝函数
    function cloneFunction(targe) {
        //匹配函数体的正则
        const bodyReg = /(?<={)(.|\n)+(?=})/m;
        //匹配函数参数的正则
        const paramReg = /(?<=\().+(?=\)\s+{)/;
        const targeString = targe.toString();
        //利用prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的
        if (targe.prototype) { //普通函数
            const param = paramReg.exec(targeString);
            const body = bodyReg.exec(targeString);
            if (body) {
                if (param) {
                    const paramArr = param[0].split(',');
                    //使用 new Function 重新构造一个新的函数
                    return new Function(...paramArr, body[0]);
                } else {
                    return new Function(body[0]);
                }
            } else {
                return null;
            }
        } else { //箭头函数
            //eval和函数字符串来重新生成一个箭头函数
            return eval(targeString);
        }
    }
    /**
     * 遍历数据处理函数
     * @array 要处理的数据
     * @callback 回调函数,接收两个参数 value 每一项的值 index 每一项的下标或者key。
     */
    function handleWhile(array, callback) {
        let index = -1;
        const length = array.length;
        while (++index < length) {
            callback(array[index], index);
        }
    }
    function clone(target, map) {
        if (isObject(target)) {
            let result = null;
            if (getType(target) === "[object Array]") {
                result = []
            } else if (getType(target) === "[object Object]") {
                result = {}
            } else if (getType(target) === "[object Map]") {
                result = new Map();
            } else if (getType(target) === "[object Set]") {
                result = new Set();
            }

            // 解决循环引用
            if (map.has(target)) {
                return map.get(target);
            }
            map.set(target, result);

            if (getType(target) === "[object Map]") {
                target.forEach((value, key) => {
                    result.set(key, clone(value, map));
                });
                return result;
            } else if (getType(target) === "[object Set]") {
                target.forEach(value => {
                    result.add(clone(value, map));
                });
                return result;
            } else if (getType(target) === "[object Object]" || getType(target) === "[object Array]") {
                const keys = getType(target) === "[object Array]" ? undefined : Object.keys(target);

                function callback(value, key) {
                    if (keys) {
                        // 如果keys存在则说明value是一个对象的key,不存在则说明key就是数组的下标。
                        key = value
                    }
                    result[key] = clone(target[key], map)
                }
                handleWhile(keys || target, callback)
            } else {
                result = handleOherData(target)
            }
            return result;
        } else {
            if (getType(target) === "[object Symbol]") {
                return cloneSymbol(target)
            } else {
                return target;
            }
        }
    }
    let map = new WeakMap;
    const result = clone(target, map);
    map = null;
    return result
}

Здесь вы можете показать интервьюеру свои шесть способностей в программировании.

  • Строгость логики кода.
  • Возможность получить представление о типах данных.
  • Умение использовать JS API на профессиональном уровне.
  • Изучите разницу между стрелочными функциями и обычными функциями.
  • Умение пользоваться регулярными выражениями.
  • Возможность модульной разработки

королевский ранг

В приведенном выше коде много копий типов данных, которые не реализованы, если вам интересно, вы можете реализовать это в комментариях, король принадлежит вам!

Суммировать

Подводя итог, интервьюер просит вас добиться глубокого копирования, по сути, изучить ваши способности во всех аспектах. Например

  • серебряный уровень
    • Возможность судить о примитивных и эталонных данных.
    • Способность применять рекурсивное мышление.
  • золотой ранг
    • Способность правильно понимать концепцию ссылочных типов.
    • Возможность точного определения типов данных.
  • Платиновый уровень
    • Понимание циклических ссылок и способность решать проблемы, вызванные циклическими ссылками.
    • Знаком с концепцией и применением структуры данных Map в ES6.
    • Осведомленность об утечках памяти и возможность избежать утечек.
  • ряд каменной кладки
    • Знаком с концепцией и применением структуры данных WeakMap в ES6.
    • Имеет возможность оптимизировать производительность выполнения кода.
    • Способность понять эффективность обхода.
    • К пониманию++iа такжеi++разница.
    • Способность кода абстрагироваться.
  • Звёздный ранг
    • Строгость логики кода.
    • Возможность получить представление о типах данных.
    • Умение использовать JS API на профессиональном уровне.
    • Изучите разницу между стрелочными функциями и обычными функциями.
    • Умение пользоваться регулярными выражениями.
    • Возможность модульной разработки

Так что вместо того, чтобы запоминать некоторые написанные от руки вопросы для собеседования по коду, лучше написать их самостоятельно и посмотреть, достигли ли вы этого ранга.