Страницы

Поиск по вопросам

понедельник, 9 декабря 2019 г.

Основы JavaScript, копия объектов

#javascript


Я заметил, что если есть какой-то объект, и если создать новый, присвоив ему значение
этого объекта, и поменять в нем какое-то поле, то в старом оно тоже поменяется:



var a = {prop: 1};
document.getElementById("1").textContent = a.prop + '';
var b = a;
b.prop = 2;
document.getElementById("2").textContent = a.prop + '';

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


Ответы

Ответ 1



Весь смысл в том, что в Javascript все объекты присваиваются по ссылке без исключений, это означает, что скольким бы переменным вы не присвоили объект, он всегда будет оставаться в памяти в единственном числе. var a = {prop: 1} var b = a; var c = b; (c===b&&b===a&&c===a) // true Пока хоть одна переменная ссылается на объект, он продолжает существовать. Когда последняя переменная, ссылающаяся на объект удаляется, удаляется и сам объект. var a = {prop: 1}; var b = a; a.prop = 2; delete a; (b.prop===2); // true В javascript нету оператора, позволяющего сделать копию объекта, поэтому копирование объектов в Javascript сводится к созданию нового с точно такими же свойствами, как у первого. var a = {prop: 1}; var b = {}; b.prop = a.prop; Однако нужно учитывать тот факт, что свойство объекта может содержать другой объект, который так же будет передан по ссылке, в случае присваивания, поэтому следующий пример создаст не правильный клон: var a = { prop: { sub: 1 } } var b = {}; b.prop = a.prop; Потому что свойство sub будет одним и тем же для обоих объектов. a.prop.sub = 2; (b.prop.sub===1) // False Поэтому при создании копии объекта необходимо проверять не являются ли его свойства объектами, и в этом случае производить с ними ту же операцию, что и мы провернули с a и b. В целом функция создания клона объекта будет выглядеть так: function makeClone(obj) { let clone = {}; // Создаем новый пустой объект for (let prop in obj) { // Перебираем все свойства копируемого объекта if (obj.hasOwnProperty(prop)) { // Только собственные свойства if ("object"===typeof obj[prop]) // Если свойство так же объект clone[prop] = makeClone(obj[prop]); // Делаем клон свойства else clone[prop] = obj[prop]; // Или же просто копируем значение } } return clone; } Однако эта функция не универсальна и сработает правильно не во всех случаях, так как в Javascript помимо простых объектов, есть так же встроенные объекты и объекты созданные из прототипов, которые уже не получится просто так взять и скопировать. Это экземпляры Image, Date, HTMLElement, кастомные классы и пр. Кроме того бывают случаи когда вложенные свойства содержат ссылки на объекты, косвенно возвращающие нас к самим же себе. Например: var a = {}, b = {prop: a}; a.prop = b; Если вы запустите функцию makeClone для переменной а, то процесс создания клона будет происходить бесконечно, что вызовет "Maximum call stack size exceeded". Так что существует очень много нюансов клонирования объекта, поэтому лучше не изобретать велосипед, а воспользоваться готовыми решениями такими как jQuery.extend: var a = {prop: 1}; var clone = jQuery.extend(true, {}, a); Кроме того stand-alone аналоги этой функции можно найти на npmjs. ЗЫ: По поводу Object.assign - это экспериментальный метод и пока он не поддерживается браузерами, и кажется babel без специальных плагинов его тоже не воспринимает.

Ответ 2



Мы все знаем, что объекты передаются в виде ссылок, и просто присвоить значение к свойству объекта-клона - значит всего лишь привязать эту ссылку к его свойству. К тому же объект - это не только функция, объект или массив, что также усложняет задачу. Для определения того, что это за объект, применяют различные способы: старый добрый typeofопределяет функцию, как function, но массив он определяет как object, ровно как и сам объект; используют встроенные методы типа Array.isArray(), instanceof и т.д. используют "утиную типизацию", определяя тип объекта за счет его возможных методов, свойств и поведения: к примеру, arr.length, arr.slice(), в зависимости от фантазии... Я же открыл для себя *.constructor.name, что позволило мне четко получить тип от строки - String, от массива - Array, от объекта - Object и т.д... Это позволило использовать одно решение для определения любых типов данных. Итак мы знаем, что заходит в функцию, и если это примитив, то возращаем его, а если объект, то у нас в распоряжении масса описываемых способов: это и slice, и Object.assign и даже JSON.parse(JSON.stringify(obj)), что я бы не рекомендовал (во всяком случае, свойство объекта в виде функции теряется и не возращается при парсинге...). Но и здесь есть ограничение в поверхностном клонировании свойств. А мы хотели бы клонировать объекты любого типа и с любой вложенностью. И тут мы прибегаем к различного рода циклам, пробегаясь по значениям свойств объекта и проверяя их, имеют ли они вложенности, а если имеют, то какого типа... Я также склонен к рекурсии в решении этой задачи, тем более, что рекурсия с такими данными не будет глубокой... Рекурсия продолжается до тех пор, пока не произойдет возврат примитивного типа значения. И здесь я предлагаю избавиться от всяких switch и даже от if, а задействовать объектный подход... Вот пример сложного объекта, свойствами которого являются и примитивы, и объекты, и функции: const obj = { name: 'Name', innerFunc: function() {console.log("this is the innerFunc")}, innerArr: ['one', 'two', 'three'], innerObj: { 'one': { 'innOne': 'innOne', 'innTwo': ['innArr1', 'innArr2', 'innArr3'], 'innThree': function() { console.log("this is the innThree.func"); } }, 'two': 'bebe', 'three': 'zzz' } }; В функции клонирования мы создаем объект, свойствами которого будут функции, работающие с входящим объектом. Причем, именами свойств этого объекта будут типы данных. Определяя тип данных входящего в функцию значения, мы запускаем ту или иную функцию, которая будет с ним работать: function dClone(el) { const funcsObj = { "Object": () => { let clObj = {}; for(let prop in el) { clObj[prop] = dClone(el[prop]); } return clObj; }, "Array": () => { return el.map((i) => { return dClone(i); }); } }; if (el.constructor.name in funcsObj) { return funcsObj[el.constructor.name](); } else { return el; } } Рекурсия вызывается до тех пор, пока значение становится примитивом.... Теперь можно сделать клонирование любой вложенности и, условно, любых типов... let clone = dClone(obj); Конечно, сюда стоит добавить работу с Датой и другими объектами: для этого всего лишь добавляем в наш объект функций нужный тип. Можно даже объединить работу с массивами и объектами одной функцией: ведь суть ее лишь в передаче значения в рекурсию... Функция клонирования углубляется в клонируемый объект до элементарных типов данных... а в качестве switch case или if используется объектный подход... Рад буду любым оптимизациям и замечаниям...

Комментариев нет:

Отправить комментарий