Поговорите с глубокой копией объекта и мелкой копией

JavaScript

написать впереди

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

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

Первоначальная цель ведения блога также состоит в том, чтобы обобщить знания, которые я изучил. фронтенд разработка.

Основные типы значений и ссылочные типы

Переменные JavaScript содержат два типа значений

  1. базовое значение типаПримитивное значение относится к некоторому простому сегменту данных, хранящемуся в стеке.
let str = 'a';
let num = 1;

В JavaScript основными типами данных являются String, Number, Undefined, Null и Boolean, а в ES6 определен новый базовый тип данных Symbol, поэтому всего существует 6 типов.

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

let str1 = 'a';
let str2 = str1;
str2 = 'b';
console.log(str2); //'b'
console.log(str1); //'a'
  1. значение ссылочного типаЗначение типа ссылочного типа - это экземпляр эталонного типа, который является объектом, хранящимся в памяти кучи. Тип ссылки - это структура данных. Наиболее часто используемыми типами являются объект, массив и функция, а также дату, Regexp, ошибка и т. д., ES6 он также обеспечивает набор, Map2 новые структуры данных

Как JavaScript копирует ссылочные типы

JavaScript назначает примитивные типы и ссылочные типы по-разному

let obj1 = {a:1};
let obj2 = obj1;
obj2.a = 2;
console.log(obj1); //{a:2}
console.log(obj2); //{a:2}

Здесь изменяется только свойство a в obj1, но свойство a в ob1 и obj2 изменяется одновременно

Когда переменная копирует значение ссылочного типа, она также копирует значение переменной в новую переменную, как и значение базового типа, разница в том, что для значения переменной это указатель на объект, хранящийся в куче. памяти (JS предусматривает, что объект, помещенный в память кучи, не может быть доступен напрямую, необходимо получить доступ к адресу объекта в памяти кучи, а затем значение в объекте может быть получено в соответствии с этим адресом, поэтому значение ссылки доступ к типу осуществляется по ссылке)

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

保存在于栈中的变量和堆内存中对象的关系

Другой пример: Сяомин (переменная obj1) знает адрес своего дома (объект {a:1}), а затем Сяомин сообщает Сяогану (переменная obj2) адрес своего дома (переменная-копия), Сяоган знает его в это время. адрес дома Сяомина, а затем Сяоган пошел в дом Сяомина и снес дверь дома Сяомина (объект модификации), когда Сяомин пошел домой и увидел, что двери нет, когда Сяомин и Сяоган пошли по этому адресу, они оба см. Дом без дверей -.- (изменения объекта отражаются в переменных)

мелкая копия

Определение мелкой копии можно понимать как

Создает новый объект с точной копией значений свойств исходного объекта. Если атрибут является примитивным типом, копируется значение примитивного типа, а если атрибут является ссылочным типом, копируется адрес памяти, поэтому, если один объект изменит этот адрес, это повлияет на другой объект.

Вот несколько методов поверхностного копирования, предоставляемых JavaScript.

Object.assign

Способ копирования объекта в ES6, первый принимаемый параметр — цель копии, а остальные параметры — исходный объект копии (их может быть несколько)

Синтаксис: Object.assign (цель, ... источники)

let target = {};
let source = { a: { b: 2 } };
Object.assign(target, source);
console.log(target); // { a: { b: 2 } };

Во-первых, мы сначала копируем исходник в целевой объект через Object.assign, а затем пытаемся изменить свойство b в исходном объекте с 2 до 10.

let target = {};
let source = { a: { b: 2 } };
Object.assign(target, source);
console.log(target); // { a: { b: 10 } };
source.a.b = 10;
console.log(source); // { a: { b: 10 } };
console.log(target); // { a: { b: 10 } };

Через консоль можно обнаружить, что в результатах печати свойства b в трех таргетах стали 10, что доказывает, что Object.assign — поверхностная копия

Object.assign простокорневой атрибут(первый уровень объекта) создает новый объект, но копирует тот же адрес памяти только в том случае, если значением свойства является объект

В Object.assign также есть несколько замечаний:

  1. Не копирует унаследованные свойства объекта
  2. неперечислимое свойство
  3. свойство данных свойства/свойство доступа
  4. Может копировать Тип символа

Таким образом можно понять, что Object.assign будет проходить по всем свойствам исходного объекта (источников) слева направо, а затем использовать=Назначить целевому объекту (цели)

let obj1 = {
    a:{
        b:1
    },
    sym:Symbol(1)
};
Object.defineProperty(obj1,'innumerable',{
    value:'不可枚举属性',
    enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1); 
console.log('obj2',obj2); 

Видно, что тип Symbol может быть скопирован корректно, но игнорируются неперечислимые свойства и изменяется значение obj1.ab, а также изменится значение obj2.ab, указывая на то, что тот же самый объект в куче доступ к памяти по-прежнему осуществляется. вопрос

Тема: В Object.assgin параметр target, source, если базовые типы данных упакованы в первичный тип пакета, см. описание подробнееMDN

спред оператор

Используйте оператор распространения для клонирования или копирования свойств при создании буквального объекта.

Синтаксис: let cloneObj = { ...obj };

let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

Тот же недостаток есть у оператора распространения Object.assign().Для свойств, значением которых является объект, его нельзя полностью скопировать в 2 разных объекта, но если свойства все являются значениями базовых типов, то удобнее использовать оператор спреда

Array.prototype.slice

Метод slice() возвращает новый объект массива, который является неглубокой копией исходного массива, определяемого началом и концом (исключая конец). Исходный массив не будет изменен.

Синтаксис: arr.slice(начало, конец);

До ES6 остаточного оператора не было, а когда Array.from, вы можете использовать Array.prototype.slice для преобразования массива класса аргументов в реальный массив, который возвращает новый массив с мелкой копией.

Array.prototype.slice.call({0: "aaa", length: 1}) //["aaa"]

let arr = [1,2,3,4]
console.log(arr.slice() === arr); //false

Array.prototype.concat

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

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

Поверхностная копия только создает новый объект в памяти кучи по корневому атрибуту, копируя значение базового типа, но сложный тип данных, то есть объект, копирует тот же адрес, а глубокая копия — для сложных типов данных в адрес памяти открывается в памяти кучи для хранения скопированного объекта и копирования исходного объекта.Эти два объекта независимы друг от друга, то есть два разных адреса

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

простая глубокая копия

let obj1 = {
    a: {
        b: 1
    },
    c: 1
};
let obj2 = {};

obj2.a = {}
obj2.c = obj1.c
obj2.a.b = obj1.a.b;
console.log(obj1); //{a:{b:1},c:1};
console.log(obj2); //{a:{b:1},c:1};
obj1.a.b = 2;
console.log(obj1); //{a:{b:2},c:1};
console.log(obj2); //{a:{b:1},c:1};

В приведенном выше коде мы создаем новый объект obj2, и в соответствии со свойством a объекта obj1 является ссылочным типом, мы также создаем новый объект для значения obj2.a (то есть открывается новый адрес памяти в памяти), затем скопируйте значение номер 1 свойства obj1.ab в obj2.ab, так как номер 1 является значением базового типа, поэтому после изменения значения obj1.ab, obj2.a не повлияет , потому что их ссылка полностью 2 отдельный объект, который завершает простую глубокую копию

JSON.stringify

JSON.stringify() — это наиболее часто используемый метод глубокого копирования в текущем процессе разработки внешнего интерфейса.Принцип заключается в сериализации объекта в строку JSON, преобразовании содержимого объекта в строку и сохранении его на диске, а также затем используйте JSON .parse() десериализует строку JSON в новый объект

let obj1 = {
    a:1,
    b:[1,2,3]
}
let str = JSON.stringify(obj1)
let obj2 = JSON.parse(str)
console.log(obj2); //{a:1,b:[1,2,3]}
obj1.a = 2
obj1.b.push(4);
console.log(obj1); //{a:2,b:[1,2,3,4]}
console.log(obj2); //{a:1,b:[1,2,3]}

Несколько замечаний о глубоком копировании через JSON.stringify

  1. Если в значении скопированного объекта есть функция, неопределенная или символ, пара ключ-значение исчезнет в строке JSON, сериализованной с помощью JSON.stringify().
  2. Невозможно скопировать неперечислимые свойства, невозможно скопировать цепочку прототипов объекта
  3. Копирование типа ссылки Date превратит его в строку
  4. Копирование ссылочного типа RegExp станет пустым объектом
  5. Объект содержит nan, бесконечность и -бесконечность, и результат сериализации станет нулевым.
  6. Циклическое приложение, которое не может копировать объекты (например, obj[key] = obj)
function Obj() {
    this.func = function () {
        alert(1) 
    };
    this.obj = {a:1};
    this.arr = [1,2,3];
    this.und = undefined;
    this.reg = /123/;
    this.date = new Date(0);
    this.NaN = NaN
    this.infinity = Infinity
    this.sym = Symbol(1)
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
    enumerable:false,
    value:'innumerable'
})
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);

Результат напечатан следующим образом

Видно, что за исключением объектов и массивов Object, остальные принципиально отличаются от исходных: конструктором obj1 является конструктор Obj, а конструктор obj2 указывает на Object, а для циклических ссылок сообщается об ошибке напрямую.

Хотя есть много функций, которые невозможно реализовать путем глубокого копирования объектов с помощью метода JSON.stringify(), для повседневных нужд разработки (объектов и массивов) использование этого метода является самым простым и быстрым.

Глубокое копирование объектов с помощью сторонних библиотек

1.lodash

2.jQuery

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

Реализуйте функцию глубокого копирования самостоятельно

рекурсия

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

let obj1 = {
    a:{
        b:1
    }
};
function deepClone(obj) {
    let cloneObj = {}; //在堆内存中新建一个对象
    for(let key in obj){ //遍历参数的键
       if(typeof obj[key] ==='object'){ 
          cloneObj[key] = deepClone(obj[key]) //值是对象就再次调用函数
       }else{
           cloneObj[key] = obj[key] //基本类型直接复制值
       }
    }
    return cloneObj 
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); //{a:{b:1}}

Но проблем еще много

  • Прежде всего, эта функция deepClone не может копировать неперечислимые свойства и типы символов.
  • Это всего лишь итерация цикла для значения ссылочного типа Object, а ссылочные типы Array, Date, RegExp, Error, Function не могут быть правильно скопированы
  • Объекты образуют цикл, т. е. циклические ссылки (например: obj1.a = obj)

Я резюмирую метод глубокого копирования

Прочитав много блогов о глубоком копировании, я подытожил метод, который может глубоко копировать ECMAScript.родной ссылочный типМетоды

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {

    if (obj.constructor === Date) return new Date(obj);   //日期对象就返回一个新的日期对象
    if (obj.constructor === RegExp) return new RegExp(obj);  //正则对象就返回一个新的正则对象

    //如果成环了,参数obj = obj.loop = 最初的obj 会在WeakMap中找到第一次放入的obj提前返回第一次放入WeakMap的cloneObj
    if (hash.has(obj)) return hash.get(obj)

    let allDesc = Object.getOwnPropertyDescriptors(obj);     //遍历传入参数所有键的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc); //继承原型链

    hash.set(obj, cloneObj)

    for (let key of Reflect.ownKeys(obj)) {   //Reflect.ownKeys(obj)可以拷贝不可枚举属性和符号类型
        // 如果值是引用类型(非函数)则递归调用deepClone
        cloneObj[key] =
            (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ?
                deepClone(obj[key], hash) : obj[key];
    }
    return cloneObj;
};

let obj = {
    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'),
    [Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
    enumerable: false,
    value: '不可枚举属性'
});

obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))

obj.loop = obj

let cloneObj = deepClone(obj);

console.log('obj', obj);
console.log('cloneObj', cloneObj);

for (let key of Object.keys(cloneObj)) {
    if (typeof cloneObj[key] === 'object' || typeof cloneObj[key] === 'function') {
        console.log(`${key}相同吗? `, cloneObj[key] === obj[key])
    }
}

Есть несколько важных моментов, касающихся этой функции.

  1. Используя метод Reflect.ownKeys, можно пройти по неперечислимым свойствам и типам символов объектов.
  2. Когда параметр имеет значение Date, тип RegExp напрямую создает новый экземпляр.
  3. Используйте Object.getOwnPropertyDescriptors, чтобы получить свойства, соответствующие всем свойствам объекта, и объедините Object.create, чтобы создать новый объект, чтобы наследовать цепочку прототипов, переданную в исходном объекте.
  4. Используя тип WeakMap в качестве хэш-таблицы, WeakMap может эффективно предотвращать утечки памяти, поскольку это слабая ссылка. Это очень полезно для обнаружения циклических ссылок. Если есть циклическая ссылка, он напрямую возвращает значение, хранящееся в WeakMap.

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

Типы Null и Function в приведенной выше функции глубокого копирования по-прежнему относятся к одному и тому же объекту, поскольку функция deepClone возвращает значение напрямую, когда значение объекта является функцией или нулевым значением.Здесь нет функции глубокого копирования.Если вам нужна глубокая копия функции, вы можете рассмотреть возможность использования функции конструктора функций или eval?

Суммировать

  1. Хотя инкапсулированный метод deepClone может реализовывать копирование собственных ссылочных типов ECMAScript, его область применения слишком широка для объектов, и по-прежнему есть много объектов, которые невозможно точно скопировать (например, узлы DOM), но в повседневной разработке это обычно не так. необходимо копировать многие специальные для ссылочных типов глубокое копирование объектов с помощью JSON.stringify по-прежнему является одним из самых удобных способов (конечно, нужно понимать недостатки JSON.stringify)

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

  3. Для углубленного изучения принципа глубокого копирования полезно понять характеристики ссылочных типов JavaScript и решить связанные с ними специальные проблемы.Очень полезно улучшить основы JavaScript~~~

Спасибо за просмотр

использованная литература

Углубленный объект глубокого копирования JS

Расширенное программирование на JavaScript, 3-е издание