Как сделать глубокий клон в JavaScript
Как вы глубоко клонируете объект Javascript?
Я знаю, что есть различные функции, основанные на фреймворках, таких как JSON.parse(JSON.stringify(o))
а также $.extend(true, {}, o)
но я не хочу использовать такую структуру.
Какой самый элегантный или эффективный способ создания глубокого клона.
Мы заботимся о крайних случаях, таких как клонирование массива. Не ломать цепи прототипов, занимаясь самостоятельной ссылкой.
Мы не заботимся о поддержке копирования объектов DOM и тому подобное, потому что .cloneNode
существует по этой причине.
Поскольку я в основном хочу использовать глубокие клоны в node.js
использование функций ES5 двигателя V8 является приемлемым.
[Редактировать]
Прежде чем кто-либо предложит, позвольте мне упомянуть, что есть четкая разница между созданием копии путем прототипического наследования от объекта и клонированием. Первый делает беспорядок в цепи прототипа.
[Дальнейшее редактирование]
Прочитав ваш ответ, я пришел к досадному открытию, что клонирование целых объектов - очень опасная и сложная игра. Возьмем для примера следующий объект на основе замыкания
var o = (function() {
var magic = 42;
var magicContainer = function() {
this.get = function() { return magic; };
this.set = function(i) { magic = i; };
}
return new magicContainer;
}());
var n = clone(o); // how to implement clone to support closures
Есть ли способ написать функцию клонирования, которая клонирует объект, имеет то же состояние во время клонирования, но не может изменить состояние o
без написания парсера JS в JS.
В такой функции больше не должно быть необходимости в реальном мире. Это просто академический интерес.
26 ответов
Это действительно зависит от того, что вы хотели бы клонировать. Это действительно объект JSON или просто какой-либо объект в JavaScript? Если вы захотите сделать какой-нибудь клон, у вас могут возникнуть проблемы. Какая проблема? Я объясню это ниже, но сначала приведу пример кода, который клонирует литералы объектов, любые примитивы, массивы и узлы DOM.
function clone(item) {
if (!item) { return item; } // null, undefined values check
var types = [ Number, String, Boolean ],
result;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (item instanceof type) {
result = type( item );
}
});
if (typeof result == "undefined") {
if (Object.prototype.toString.call( item ) === "[object Array]") {
result = [];
item.forEach(function(child, index, array) {
result[index] = clone( child );
});
} else if (typeof item == "object") {
// testing that this is DOM
if (item.nodeType && typeof item.cloneNode == "function") {
result = item.cloneNode( true );
} else if (!item.prototype) { // check that this is a literal
if (item instanceof Date) {
result = new Date(item);
} else {
// it is an object literal
result = {};
for (var i in item) {
result[i] = clone( item[i] );
}
}
} else {
// depending what you would like here,
// just keep the reference, or create new object
if (false && item.constructor) {
// would not advice to do that, reason? Read below
result = new item.constructor();
} else {
result = item;
}
}
} else {
result = item;
}
}
return result;
}
var copy = clone({
one : {
'one-one' : new String("hello"),
'one-two' : [
"one", "two", true, "four"
]
},
two : document.createElement("div"),
three : [
{
name : "three-one",
number : new Number("100"),
obj : new function() {
this.name = "Object test";
}
}
]
})
А теперь давайте поговорим о проблемах, которые могут возникнуть при начале клонирования объектов REAL. Я говорю сейчас об объектах, которые вы создаете, делая что-то вроде
var User = function(){}
var newuser = new User();
Конечно, вы можете их клонировать, это не проблема, каждый объект предоставляет свойство конструктора, и вы можете использовать его для клонирования объектов, но это не всегда будет работать. Вы также можете сделать простой for in
на этом объекты, но он идет в том же направлении - неприятности. Я также включил функцию клонирования внутри кода, но это исключено if( false )
заявление.
Итак, почему клонирование может быть болью? Ну, во-первых, у каждого объекта / экземпляра может быть какое-то состояние. Вы никогда не можете быть уверены, что ваши объекты не имеют, например, приватных переменных, и если это так, клонируя объект, вы просто нарушаете состояние.
Представьте, что нет государства, это нормально. Тогда у нас все еще есть другая проблема. Клонирование с помощью метода "конструктор" даст нам еще одно препятствие. Это зависимость аргументов. Вы никогда не можете быть уверены, что тот, кто создал этот объект, не сделал, какой-то
new User({
bike : someBikeInstance
});
Если это так, то вам не повезло, возможно, что someBikeInstance был создан в каком-то контексте, и этот контекст неизвестен для метода клонирования.
Так что делать? Вы все еще можете сделать for in
решение и обрабатывать такие объекты как обычные литералы объекта, но, может быть, это идея не клонировать такие объекты вообще, а просто передать ссылку на этот объект?
Другое решение - вы можете установить соглашение, согласно которому все объекты, которые должны быть клонированы, должны реализовывать эту часть самостоятельно и предоставлять соответствующий метод API (например, cloneObject). Что-то, что cloneNode
делает для DOM.
Вам решать.
Очень простой способ, может быть, слишком простой:
var cloned = JSON.parse(JSON.stringify(objectToClone));
JSON.parse(JSON.stringify())
Сочетание глубокого копирования объектов Javascript является неэффективным хаком, потому что JSON не поддерживает значенияundefined
а такжеfunction () {}
, и поэтомуJSON.stringify
будет игнорировать эти разделы кода при "строковом преобразовании" (маршалинге) объекта Javascript в JSON.
Следующая функция будет глубоко копировать объекты и не требует сторонней библиотеки (jQuery, LoDash и т. Д.).
function copy(aObject) {
if (!aObject) {
return aObject;
}
let v;
let bObject = Array.isArray(aObject) ? [] : {};
for (const k in aObject) {
v = aObject[k];
bObject[k] = (typeof v === "object") ? copy(v) : v;
}
return bObject;
}
мы можем добиться глубокого клонирования, используя StructuredClone()
const original = { name: "stack overflow" };
// Clone it
const clone = structuredClone(original);
Вот функция ES6, которая также будет работать для объектов с циклическими ссылками:
function deepClone(obj, hash = new WeakMap()) {
if (Object(obj) !== obj) return obj; // primitives
if (obj instanceof Set) return new Set(obj); // See note about this!
if (hash.has(obj)) return hash.get(obj); // cyclic reference
const result = obj instanceof Date ? new Date(obj)
: obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
: obj.constructor ? new obj.constructor()
: Object.create(null);
hash.set(obj, result);
if (obj instanceof Map)
Array.from(obj, ([key, val]) => result.set(key, deepClone(val, hash)) );
return Object.assign(result, ...Object.keys(obj).map (
key => ({ [key]: deepClone(obj[key], hash) }) ));
}
// Sample data
var p = {
data: 1,
children: [{
data: 2,
parent: null
}]
};
p.children[0].parent = p;
var q = deepClone(p);
console.log(q.children[0].parent.data); // 1
Примечание о наборах и картах
Как бороться с ключами множеств и карт спорно: эти ключи часто примитивов (в этом случае нет никаких дебатов), но они также могут быть объектами. В этом случае возникает вопрос: должны ли эти ключи быть клонированы?
Можно утверждать, что это должно быть сделано, так что, если эти объекты мутируют в копии, объекты в оригинале не затрагиваются, и наоборот.
С другой стороны, хотелось бы этого, если бы Set/Map has
ключ, это должно быть верно как в оригинале, так и в копии - по крайней мере, до того, как какое-либо изменение будет внесено в любой из них. Было бы странно, если бы копия была Set/Map с ключами, которые никогда не встречались раньше (как они были созданы во время процесса клонирования): конечно, это не очень полезно для любого кода, который должен знать, является ли данный объект введите этот набор / карту или нет.
Как вы заметили, я скорее придерживаюсь второго мнения: ключи Наборов и Карт - это значения (возможно, ссылки), которые должны оставаться неизменными.
Такой выбор часто также появляется на поверхности других (возможно, пользовательских) объектов. Не существует общего решения, так как многое зависит от того, как клонированный объект будет вести себя в вашем конкретном случае.
В библиотеке библиотеки Underscore.js contrib есть функция snapshot, которая глубоко клонирует объект
фрагмент из источника:
snapshot: function(obj) {
if(obj == null || typeof(obj) != 'object') {
return obj;
}
var temp = new obj.constructor();
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = _.snapshot(obj[key]);
}
}
return temp;
}
как только библиотека связана с вашим проектом, вызовите функцию, просто используя
_.snapshot(object);
Как уже отмечали другие и другие подобные вопросы, клонирование "объекта" в общем смысле сомнительно в JavaScript.
Тем не менее, есть класс объектов, которые я называю "объектами данных", то есть объектами, построенными просто из { ... }
литералы и / или простые присваивания свойств или десериализованные из JSON, для которых целесообразно клонировать. Только сегодня я хотел искусственно раздувать данные, полученные от сервера, в 5 раз, чтобы проверить, что происходит с большим набором данных, но объект (массив) и его дочерние элементы должны были быть отдельными объектами, чтобы вещи функционировали правильно. Клонирование позволило мне сделать это, чтобы умножить мой набор данных:
return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta));
Другое место, где я в конечном итоге клонирую объекты данных, - это отправка данных обратно на хост, где я хочу убрать поля состояния из объекта в модели данных перед отправкой. Например, я мог бы хотеть удалить все поля, начинающиеся с "_" из объекта, поскольку он клонирован.
Вот код, который я в итоге написал, чтобы сделать это в общем, включая поддержку массивов и селектор для выбора, какие элементы клонировать (который использует строку "путь" для определения контекста):
function clone(obj,sel) {
return (obj ? _clone("",obj,sel) : obj);
}
function _clone(pth,src,sel) {
var ret=(src instanceof Array ? [] : {});
for(var key in src) {
if(!src.hasOwnProperty(key)) { continue; }
var val=src[key], sub;
if(sel) {
sub+=pth+"/"+key;
if(!sel(sub,key,val)) { continue; }
}
if(val && typeof(val)=='object') {
if (val instanceof Boolean) { val=Boolean(val); }
else if(val instanceof Number ) { val=Number (val); }
else if(val instanceof String ) { val=String (val); }
else { val=_clone(sub,val,sel); }
}
ret[key]=val;
}
return ret;
}
Самое простое и разумное решение для глубокого клонирования, предполагающее ненулевой корневой объект и без выбора элемента:
function clone(src) {
var ret=(src instanceof Array ? [] : {});
for(var key in src) {
if(!src.hasOwnProperty(key)) { continue; }
var val=src[key];
if(val && typeof(val)=='object') { val=clone(val); }
ret[key]=val;
}
return ret;
}
Lo-Dash, теперь расширенный набор http://underscorejs.org/, имеет несколько глубоких функций клонирования:
_.cloneDeep(object)
_.cloneDeepWith(object, (val) => {if(_.isElement(val)) return val.cloneNode(true)})
второй параметр - это функция, которая вызывается для получения клонированного значения.
lodash underscore
Для обеспечения совместимости с последней стабильной версией Underscore предоставляется build.
structuredClone
теперь поддерживается большинством браузеров
его основное ограничение касается функций НЕ совладания. Для копирования/перемещения вручную потребуется дополнительная работа.
По крайней мере, мы можем легко скопировать классы , добавив прототипы позже.
const proto = Object.getPrototypeOf(object)
const newObject = structuredClone(object)
Object.setPrototypeOf(newObject, proto)
Глубокое клонирование объекта можно выполнить несколькими способами, но каждый из них имеет свои ограничения, как указано ниже. Следовательно, я предлагаю вам использовать алгоритм StructuredClone.
-
JSON.parse(JSON.stringify(object))
- не будет копировать функции, даты, неопределенные значения и многое другое.
Приведенная ниже функция является наиболее эффективным способом глубокого клонирования объектов JavaScript.
function deepCopy(obj){
if (!obj || typeof obj !== "object") return obj;
var retObj = {};
for (var attr in obj){
var type = obj[attr];
switch(true){
case (type instanceof Date):
var _d = new Date();
_d.setDate(type.getDate())
retObj[attr]= _d;
break;
case (type instanceof Function):
retObj[attr]= obj[attr];
break;
case (type instanceof Array):
var _a =[];
for (var e of type){
//_a.push(e);
_a.push(deepCopy(e));
}
retObj[attr]= _a;
break;
case (type instanceof Object):
var _o ={};
for (var e in type){
//_o[e] = type[e];
_o[e] = deepCopy(type[e]);
}
retObj[attr]= _o;
break;
default:
retObj[attr]= obj[attr];
}
}
return retObj;
}
var obj = {
string: 'test',
array: ['1'],
date: new Date(),
object:{c: 2, d:{e: 3}},
function: function(){
return this.date;
}
};
var copyObj = deepCopy(obj);
console.log('object comparison', copyObj === obj); //false
console.log('string check', copyObj.string === obj.string); //true
console.log('array check', copyObj.array === obj.array); //false
console.log('date check', copyObj2.date === obj.date); //false
console.log('object check', copyObj.object === obj.object); //false
console.log('function check', copyObj.function() === obj.function()); //true
Избегайте использования этого метода
let cloned = JSON.parse(JSON.stringify(objectToClone));
Зачем? этот метод преобразует функцию undefined вnull
const myObj = [undefined, null, function () {}, {}, '', true, false, 0, Symbol];
const IsDeepClone = JSON.parse(JSON.stringify(myObj));
console.log(IsDeepClone); //[null, null, null, {…}, "", true, false, 0, null]
попробуйте использовать функцию deepClone. Их несколько выше
Это метод глубокого клонирования, который я использую, я думаю, что это здорово. Надеюсь, вы сделаете предложения
function deepClone (obj) {
var _out = new obj.constructor;
var getType = function (n) {
return Object.prototype.toString.call(n).slice(8, -1);
}
for (var _key in obj) {
if (obj.hasOwnProperty(_key)) {
_out[_key] = getType(obj[_key]) === 'Object' || getType(obj[_key]) === 'Array' ? deepClone(obj[_key]) : obj[_key];
}
}
return _out;
}
В такой функции больше не должно быть необходимости в реальном мире. Это просто академический интерес.
Это просто более функциональное упражнение. Это расширение ответа @tfmontague, так как я предложил добавить туда блок защиты. Но, учитывая, что я чувствую себя обязанным к ES6 и функционализирую все, вот моя версия с сутенерами. Это усложняет логику, так как вам приходится отображать массив и уменьшать объект, но избегает любых мутаций.
function cloner(x) {
const recurseObj = x => typeof x === 'object' ? cloner(x) : x
const cloneObj = (y, k) => {
y[k] = recurseObj(x[k])
return y
}
// Guard blocks
// Add extra for Date / RegExp if you want
if (!x) {
return x
}
if (Array.isArray(x)) {
return x.map(recurseObj)
}
return Object.keys(x).reduce(cloneObj, {})
}
const tests = [
null,
[],
{},
[1,2,3],
[1,2,3, null],
[1,2,3, null, {}],
[new Date('2001-01-01')], // FAIL doesn't work with Date
{x:'', y: {yx: 'zz', yy: null}, z: [1,2,3,null]},
{
obj : new function() {
this.name = "Object test";
}
} // FAIL doesn't handle functions
]
tests.map((x,i) => console.log(i, cloner(x)))
Мое решение, глубокое клонирование объектов, массивов и функций.
let superClone = (object) => {
let cloning = {};
Object.keys(object).map(prop => {
if(Array.isArray(object[prop])) {
cloning[prop] = [].concat(object[prop])
} else if(typeof object[prop] === 'object') {
cloning[prop] = superClone(object[prop])
} else cloning[prop] = object[prop]
})
return cloning
}
пример
let obj = {
a: 'a',
b: 'b',
c: {
deep: 'try and copy me',
d: {
deeper: 'try me again',
callDeeper() {
return this.deeper
}
},
arr: [1, 2, 3]
},
hi() {
return this.a
}
};
const cloned = superClone(obj)
obj.a = 'A'
obj.c.deep = 'i changed'
obj.c.arr = [45,454]
obj.c.d.deeper = 'i changed'
console.log(cloned) // unchanged object
Если ваши объекты содержат методы, не использующие JSON для глубокого клонирования, глубокое клонирование JSON не клонирует методы.
Если вы посмотрите на это, возразите
person2
только клонирует имя, а не
person1
Приветственный метод.
const person1 = {
name: 'John',
greet() {
return `HI, ${this.name}`
}
}
const person2 = JSON.parse(JSON.stringify(person1))
console.log(person2) // { name: 'John' }
Мое дополнение ко всем ответам
deepCopy = arr => {
if (typeof arr !== 'object') return arr
if(arr.pop) return [...arr].map(deepCopy)
const copy = {}
for (let prop in arr)
copy[prop] = deepCopy(arr[prop])
return copy
}
Это работает для массивов, объектов и примитивов. Вдвойне рекурсивный алгоритм, который переключается между двумя методами обхода:
const deepClone = (objOrArray) => {
const copyArray = (arr) => {
let arrayResult = [];
arr.forEach(el => {
arrayResult.push(cloneObjOrArray(el));
});
return arrayResult;
}
const copyObj = (obj) => {
let objResult = {};
for (key in obj) {
if (obj.hasOwnProperty(key)) {
objResult[key] = cloneObjOrArray(obj[key]);
}
}
return objResult;
}
const cloneObjOrArray = (el) => {
if (Array.isArray(el)) {
return copyArray(el);
} else if (typeof el === 'object') {
return copyObj(el);
} else {
return el;
}
}
return cloneObjOrArray(objOrArray);
}
Я заметил, что Map требует особой обработки, поэтому со всеми предложениями в этой теме код будет:
function deepClone( obj ) {
if( !obj || true == obj ) //this also handles boolean as true and false
return obj;
var objType = typeof( obj );
if( "number" == objType || "string" == objType ) // add your immutables here
return obj;
var result = Array.isArray( obj ) ? [] : !obj.constructor ? {} : new obj.constructor();
if( obj instanceof Map )
for( var key of obj.keys() )
result.set( key, deepClone( obj.get( key ) ) );
for( var key in obj )
if( obj.hasOwnProperty( key ) )
result[key] = deepClone( obj[ key ] );
return result;
}
Этот, используя круговую ссылку, работает для меня
//a test-object with circular reference :
var n1 = { id:0, text:"aaaaa", parent:undefined}
var n2 = { id:1, text:"zzzzz", parent:undefined }
var o = { arr:[n1,n2], parent:undefined }
n1.parent = n2.parent = o;
var obj = { a:1, b:2, o:o }
o.parent = obj;
function deepClone(o,output){
if(!output) output = {};
if(o.______clone) return o.______clone;
o.______clone = output.______clone = output;
for(var z in o){
var obj = o[z];
if(typeof(obj) == "object") output[z] = deepClone(obj)
else output[z] = obj;
}
return output;
}
console.log(deepClone(obj));
Вот самое элегантное решение для глубокого копирования, которое я мог бы создать без использования инфраструктуры (синтаксис ES6).
Наслаждайтесь!
/**
* Deep copy an array or object
* @param {array|object} a
*/
const deepCopy = (a) => {
if (Array.isArray(a)) return [...a];
if (a !== null && typeof a === 'object') return Object.assign({}, a);
return a;
}
Используйте immutableJS
import { fromJS } from 'immutable';
// An object we want to clone
let objA = {
a: { deep: 'value1', moreDeep: {key: 'value2'} }
};
let immB = fromJS(objA); // Create immutable Map
let objB = immB.toJS(); // Convert to plain JS object
console.log(objA); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }
console.log(objB); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }
// objA and objB are equalent, but now they and their inner objects are undependent
console.log(objA === objB); // false
console.log(objA.a === objB.a); // false
console.log(objA.moreDeep === objB.moreDeep); // false
Или лодаш / слияние
import merge from 'lodash/merge'
var objA = {
a: [{ 'b': 2 }, { 'd': 4 }]
};
// New deeply cloned object:
merge({}, objA );
// We can also create new object from several objects by deep merge:
var objB = {
a: [{ 'c': 3 }, { 'e': 5 }]
};
merge({}, objA , objB ); // Object { a: [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
Var newDate = новая дата (this.oldDate); Я передавал oldDate функции и генерировал newDate из this.oldDate, но он также менял this.oldDate, поэтому я использовал это решение, и оно сработало.
Это решение позволит избежать проблем с рекурсией при использовании [...target] или {...target}
function shallowClone(target) {
if (typeof a == 'array') return [...target]
if (typeof a == 'object') return {...target}
return target
}
/* set skipRecursion to avoid throwing an exception on recursive references */
/* no need to specify refs, or path -- they are used interally */
function deepClone(target, skipRecursion, refs, path) {
if (!refs) refs = []
if (!path) path = ''
if (refs.indexOf(target) > -1) {
if (skipRecursion) return null
throw('Recursive reference at ' + path)
}
refs.push(target)
let clone = shallowCopy(target)
for (i in target) target[i] = deepClone(target, refs, path + '.' + i)
return clone
}
Здравствуйте, я просто хотел опубликовать свой ответ, так как я думаю, что он более читабелен. Примечание: это не распространяется на классы, так как я их не использую, но вы можете легко добавить условие для этого.
/** Copies any type of object/array of objects
* @param obj The object to be copied
* @param customKeys A list of keys that are to be excluded from deepCopy (optional)
*/
export function deepCopyObject(obj: any, customKeys?: Array<string|number|symbol>) {
if (obj == undefined)
return;
if (typeof obj !== 'object')
return obj;
if (typeof obj === 'function')
return obj;
const isArray = obj.length > -1;
if (isArray)
return copyArray(obj);
const isObjectDate = obj instanceof Date;
if(isObjectDate)
return new Date(obj);
const isDOM = obj.nodeType && typeof obj.cloneNode == "function";
if (isDOM)
return obj.cloneNode(true);
const isHtmlComponent = obj.$$typeof != undefined; // you can pass html/react components and maybe setup a custom function to copy them
if (isHtmlComponent)
return obj;
const newObject = <typeof obj>{};
const keys = Object.keys(obj);
keys.forEach((key: keyof (typeof obj)) => {
newObject[key] = copyKeysOfTypeObject(obj, key, customKeys);
})
const cantAccessObjectKeys = keys.lenght ==0; // ex: window.navigator
if (cantAccessObjectKeys)
return obj;
return newObject
}
function copyArray(arr: any) {
const newArr = new Array(0);
arr.forEach((obj: any) => {
newArr.push(deepCopyObject(obj));
})
return newArr;
}
function copyKeysOfTypeObject(obj: any, key: string | number | symbol, customKeys?: Array<string | number | symbol>) {
if (!key)
return;
if (customKeys && customKeys.includes(key))
return obj[key];
return deepCopyObject(obj[key]);
}
Мы можем использовать рекурсию для создания deepCopy. Он может создать копию массива, объекта, массива объекта, объекта с функцией. если вы хотите, вы можете добавить функцию для другого типа структуры данных, такой как карта и т. д.
function deepClone(obj) {
var retObj;
_assignProps = function(obj, keyIndex, retObj) {
var subType = Object.prototype.toString.call(obj[keyIndex]);
if(subType === "[object Object]" || subType === "[object Array]") {
retObj[keyIndex] = deepClone(obj[keyIndex]);
}
else {
retObj[keyIndex] = obj[keyIndex];
}
};
if(Object.prototype.toString.call(obj) === "[object Object]") {
retObj = {};
for(key in obj) {
this._assignProps(obj, key, retObj);
}
}
else if(Object.prototype.toString.call(obj) == "[object Array]") {
retObj = [];
for(var i = 0; i< obj.length; i++) {
this._assignProps(obj, i, retObj);
}
};
return retObj;
};
let obj1 = {
a: 100,
b: {
c: 200,
d: [1, 2, 3],
e: () => {}
}
}
function deepClone(obj) {
let newObj = {};
for (let key in obj) {
let val = obj[key];
if (val instanceof Array) {
newObj[key] = [...val]
} else if (typeof val === 'object') {
newObj[key] = deepClone(val)
} else {
newObj[key] = val;
}
}
return newObj;
}
obj2 = deepClone(obj1);
obj1.b.c = 300;
console.log(obj1);
console.log(obj2);