Серия статей о JavaScript: разберитесь с глубокой и поверхностной копией в JS

внешний интерфейс JavaScript

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

1. Тип данных

1. Основные типы данных

Число, строка, логическое значение, нуль, неопределенное значение, символ, большое число

Bigint — это новый базовый тип данных.

2. Справочные типы данных

Объект, Массив, Функция и т.д.

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

下面是要copy的对象, 之后的代码都会直接使用$obj, 之后不会再次声明
var $obj = {
    func: function () {
        console.log('this is function')
    },
    date: new Date(),	
    symbol: Symbol(),
    a: null,
    b: undefined,
    c: {
        a: 1
    },
    e: new RegExp('regexp'),
    f: new Error('error')
}

$obj.c.d = $obj

2. Поверхностное копирование

1. Что такое мелкое копирование

Одним предложением можно сказать, что: для объекта, если значение его атрибута первого уровня является базовым типом данных, копируется копия данных полностью, а если это ссылочный тип, копируется адрес памяти. Это действительно очень мелкая копия [смеется]

2. Осознайте

Object.assign()

let obj1 = {
    name: 'yang',
    res: {
        value: 123
    }
}

let obj2 = Object.assign({}, obj1)
obj2.res.value = 456
console.log(obj2) // {name: "yang", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}

Развернуть синтаксисSpread

let obj1 = {
    name: 'yang',
    res: {
        value: 123
    }
}

let {...obj2} = obj1
obj2.res.value = 456
console.log(obj2) // {name: "yang", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}

Array.prototype.slice

 const arr1 = [
     'yang',
     {
         value: 123
     }
 ];
 
 const arr2 = arr1.slice(0);
 arr2[1].value = 456;
 console.log(arr2); // ["yang", {value: 456}]
 console.log(arr1); // ["yang", {value: 456}]
 arr2[0] = 'haha';
 console.log(arr2); // ["haha", {value: 456}]
 console.log(arr1); // ["yang", {value: 456}]

Array.prototype.concat

  const arr1 = [
      'yang',
      {
          value: 123
      }
  ];
  
  const arr2 = [].concat(arr1);
  arr2[1].value = 456;
  console.log(arr2); // ["yang", {value: 456}]
  console.log(arr1); // ["yang", {value: 456}]
  arr2[0] = 'haha';
  console.log(arr2); // ["haha", {value: 456}]
  console.log(arr1); // ["yang", {value: 456}]
实际上对于数组来说, 只要不修改原数组, 重新返回一个新数组就可以实现浅拷贝,比如说map、filter、reduce等方法

3. Глубокое копирование

1. Что такое глубокая копия

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

2. Осознайте

насильственная версияJSON.parse(JSON.stringify(object))

let obj = JSON.parse(JSON.stringify($obj))
console.log(obj) 			// 不能解决循环引用
/*
	VM348:1 Uncaught TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
    at <anonymous>:1:17
*/
delete $obj.c.d
let obj = JSON.parse(JSON.stringify($obj))
console.log(obj) 			// 丢失了大部分属性
/*
	{
        a: null
        c: {a: 1}
        date: "2020-04-05T09:51:32.610Z"
        e: {}
        f: {}
  }
*/

Существующие проблемы:

1. будет проигнорированundefined

2. будет игнорироватьsymbol

3. Нельзя сериализовать функции

4. Объекты, которые не могут разрешать циклические ссылки

5, не может быть обработан правильноnew Date()

6, не может справиться с обычным

7. Не удается обработать новую ошибку()

Первое издание Базовое издание

Рекурсивный обход свойств объекта

function deepCopy (obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }
    
    let copy = Array.isArray(obj) ? [] : {}
    Object.keys(obj).forEach(v => {
        copy[key] = deepCopy(obj[key])
    })

    return copy
}

deepCopy($obj)
/*
VM601:23 Uncaught RangeError: Maximum call stack size exceeded
    at <anonymous>:23:30
    at Array.forEach (<anonymous>)
    at deepCopy (<anonymous>:23:22)
*/
delete $obj.c.d
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1}
    date: {}
    e: {}
    f: {}
    func: ƒ ()
    symbol: Symbol()
}
*/

Проблема в:

1. Объекты, которые не могут разрешать циклические ссылки

2, не может быть обработан правильноnew Date()

3, не может справиться с обычным

4. Не удается обработать новую ошибку()

Второе издание, разрешающее циркулярные ссылки

Сначала решите проблему обхода цикла.Решение состоит в том, чтобы сохранить объект и свойства объекта в массиве, чтобы проверить, есть ли объекты, которые были пройдены при следующем обходе, и вернуться напрямую, если они есть, в противном случае продолжить обход

function deepCopy (obj, cache = []) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1, d: {…}}
    date: {}
    e: {}
    f: {}
    func: ƒ ()
    symbol: Symbol()
}

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

Третье издание, решающее специальные значения

За обработку нескольких последних объектов можно судить о типе, а можно просто вернуть новый.

function deepCopy (obj, cache = []) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }
    
    if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
    if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
    if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
    
    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1, d: {…}}
    date: Fri Apr 10 2020 20:06:08 GMT+0800 (中国标准时间) {}
    e: /regexp/
    f: Error: Error: error at deepCopy (<anonymous>:8:74) at <anonymous>:19:21 at Array.forEach (<anonymous>) at deepCopy (<anonymous>:18:22) at <anonymous>:24:1
    func: ƒ ()
    symbol: Symbol()
}
*/
除此之外Promise、Set、Map、WeakSet、WeakMap也可以用相同的办法处理

Четвертое издание разрешает ту же ссылку на функцию

Основные функции здесь вроде реализованы, но есть еще проблема, то есть функция ссылается на один и тот же адрес памяти, для этой проблемы большая часть онлайн-возвратов идет напрямую или возвращается как объекты, в том числе и lodash.

 const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

Итак, чтобы решить эту проблему, необходимо использовать функцию eval.Хотя эта функция устарела, она все же может решить проблему. Существует два типа функций:普通函数а также箭头函数, различие между ними нужно только увидеть, есть лиprototype, имеютprototypeСвойства — это обычные функции, и ни одна из них не является стрелочной функцией.

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

// lmran
function copyFunction(func) {
	let fnStr = func.toString()
	return func.prototype ? eval(`(${fnStr})`) : eval(fnStr)
}

function deepCopy (obj, cache = []) {
    if (typeof obj === 'function') {
        return copyFunction(obj)
    }
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
    if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
    if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
   
    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj).func === $obj.func // false

Суммировать

До сих пор были реализованы все глубокие и мелкие копии, но обучение бесконечно, мы также можем рассмотреть возможность использованияProxyУлучшена производительность глубокого копирования за счет перехватаsetа такжеgetдобиться, конечноObject.defineProperty()Также может. Заинтересованные студенты могут проверить эту статью, статья очень подробнаяИнтервьюер Toutiao: Вы знаете, как реализовать глубокую копию высокопроизводительной версии?

развеять сомнения

В ответ на ту же проблему, поднятую @Brota при поиске ссылок на функции, статья была исправлена, но если функция является встроенным кодом функции, проблема все еще существует, и теперь дается решение. встроенная функция, она возвращается напрямую. Но он возвращается к той же ссылке на функцию, есть копия самой встроенной функции, так что проблем с тем же адресом ссылки нет.

function copyFunction(func) {
	let fnStr = func.toString()
	if (fnStr === `function ${func.name}() { [native code] }`) {
		return func
	}
	return func.prototype ? eval(`(${fnStr})`) : eval(fnStr)
}
目前还未实现的功能:闭包的处理, 暂未找到处理办法,有知道的可以在下面留言评论

Если в статье есть какие-либо проблемы, вы можете активно указать на них, большое спасибо! ! !

Ссылаться на