Как сделать глубокий клон в JavaScript - PullRequest
87 голосов
/ 16 декабря 2010

Как вы глубоко клонируете объект 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.

В такой функции больше не должно быть необходимости в реальном мире. Это просто академический интерес.

Ответы [ 14 ]

132 голосов
/ 29 января 2015

Очень простой способ, может быть, слишком простой:

var cloned = JSON.parse(JSON.stringify(objectToClone));
58 голосов
/ 16 декабря 2010

Это действительно зависит от того, что вы хотели бы клонировать. Это действительно объект 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.

Вы решаете.

32 голосов
/ 06 января 2016

Сочетание 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;
}
11 голосов
/ 27 октября 2016

Вот функция 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
1004 1006

Примечание о множествах и Maps

1009 * Как работать с ключами множеств и карт спорно: эти ключи часто примитивы (и в этом случаеэто не спор), но они могут также быть объектами.В этом случае возникает вопрос: должны ли эти ключи быть клонированы?

Можно утверждать, что это должно быть сделано, чтобы, если эти объекты мутировали в копии, объекты в оригинале не затрагивались, и наоборот.

С другой стороны, одинхотелось бы, чтобы в случае набора / сопоставления has ключа это было бы верно как для оригинала, так и для копии - по крайней мере, до того, как какое-либо изменение будет внесено в любой из них.Было бы странно, если бы копия была Set / Map с ключами, которые никогда не встречались раньше (как они были созданы в процессе клонирования): конечно, это не очень полезно для любого кода, который должен знать, является ли данный объектвведите этот Набор / Карту или нет.

Как вы заметили, я придерживаюсь второго мнения: ключи Наборов и Карт являются значениями (возможно ссылками ).), которые должны остаться прежними.

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

10 голосов
/ 28 мая 2015

Библиотека 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);
3 голосов
/ 26 марта 2018

Lo-Dash , теперь расширенный набор Underscore.js , имеет несколько функций глубокого клонирования:

Из ответа автора Сам:

lodash underscore сборка предоставляется для обеспечения совместимости с последней стабильной версией Underscore.

3 голосов
/ 15 сентября 2017

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

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;
}
3 голосов
/ 20 сентября 2014

Как уже отмечали другие и другие подобные вопросы, клонирование «объекта» в общем смысле сомнительно в 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;
    }
1 голос
/ 22 сентября 2016

Я заметил, что 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;
}
0 голосов
/ 15 марта 2019

Использование неизменныйJS

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

Или lodash / merge

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 }] }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...