Как правильно клонировать объект JavaScript? - PullRequest
2787 голосов
/ 08 апреля 2009

У меня есть объект, x. Я хотел бы скопировать его как объект y, чтобы изменения в y не изменяли x. Я понял, что копирование объектов, полученных из встроенных объектов JavaScript, приведет к появлению дополнительных нежелательных свойств. Это не проблема, так как я копирую один из своих объектов, созданных в буквальном смысле.

Как правильно клонировать объект JavaScript?

Ответы [ 63 ]

1442 голосов
/ 08 апреля 2009

Сделать это для любого объекта в JavaScript не будет просто или просто. Вы столкнетесь с проблемой ошибочного выбора атрибутов из прототипа объекта, которые следует оставить в прототипе и не копировать в новый экземпляр. Например, если вы добавляете метод clone к Object.prototype, как показывают некоторые ответы, вам необходимо явно пропустить этот атрибут. Но что, если к Object.prototype или другим промежуточным прототипам добавятся другие дополнительные методы, о которых вы не знаете? В этом случае вы скопируете атрибуты, которые не следует делать, поэтому вам необходимо обнаружить непредвиденные нелокальные атрибуты с помощью метода hasOwnProperty.

Помимо неперечислимых атрибутов, вы столкнетесь с более сложной проблемой при попытке скопировать объекты со скрытыми свойствами. Например, prototype является скрытым свойством функции. Кроме того, на прототип объекта ссылается атрибут __proto__, который также скрыт и не будет скопирован циклом for / in, повторяющимся по атрибутам исходного объекта. Я думаю, что __proto__ может быть специфичным для интерпретатора JavaScript Firefox, и это может быть что-то другое в других браузерах, но вы понимаете. Не все перечислимо. Вы можете скопировать скрытый атрибут, если знаете его имя, но я не знаю, как его обнаружить автоматически.

Еще одним препятствием в поиске элегантного решения является проблема правильной настройки наследования прототипа. Если прототип вашего исходного объекта Object, тогда будет работать просто создание нового общего объекта с {}, но если прототип исходного кода является некоторым потомком Object, то вы пропустите дополнительные члены этого прототипа которые вы пропустили с помощью фильтра hasOwnProperty, или которые были в прототипе, но не были перечислимы в первую очередь. Одним из решений может быть вызов свойства constructor исходного объекта, чтобы получить начальный объект копирования, а затем скопировать атрибуты, но тогда вы все равно не получите неперечислимые атрибуты. Например, объект Date хранит свои данные как скрытый элемент:

function clone(obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}

var d1 = new Date();

/* Executes function after 5 seconds. */
setTimeout(function(){
    var d2 = clone(d1);
    alert("d1 = " + d1.toString() + "\nd2 = " + d2.toString());
}, 5000);

Строка даты для d1 будет на 5 секунд меньше, чем d2. Чтобы сделать один Date таким же, как другой, можно вызвать метод setTime, но это характерно для класса Date. Я не думаю, что есть пуленепробиваемое общее решение этой проблемы, хотя я был бы рад ошибаться!

Когда мне пришлось реализовать общее глубокое копирование, я пошел на компромисс, предполагая, что мне нужно будет только скопировать обычные Object, Array, Date, String, Number или Boolean , Последние 3 типа являются неизменяемыми, поэтому я мог выполнить поверхностное копирование и не беспокоиться о его изменении. Я также предположил, что любые элементы, содержащиеся в Object или Array, также будут одним из 6 простых типов в этом списке. Это можно сделать с помощью кода, подобного следующему:

function clone(obj) {
    var copy;

    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

Вышеприведенная функция будет работать адекватно для 6 упомянутых мною простых типов, если данные в объектах и ​​массивах образуют древовидную структуру. То есть в объекте не более одной ссылки на одни и те же данные. Например:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// Cloning this would cause a stack overflow due to infinite recursion:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

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

868 голосов
/ 03 июня 2012

Если вы не используете Date s, функции, неопределенные или Infinity внутри вашего объекта, очень простой однострочный лайнер будет JSON.parse(JSON.stringify(object)):

const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
}
console.log(a);
console.log(typeof a.date);  // Date object
const clone = JSON.parse(JSON.stringify(a));
console.log(clone);
console.log(typeof clone.date);  // result of .toISOString()

Это работает для всех видов объектов, содержащих объекты, массивы, строки, логические значения и числа.

См. Также эту статью о алгоритме структурированного клонирования браузеров , который используется при отправке сообщений работнику и от него. Он также содержит функцию для глубокого клонирования.

750 голосов
/ 02 марта 2011

С помощью jQuery вы можете поверхностное копирование с расширение :

var copiedObject = jQuery.extend({}, originalObject)

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

Или сделать глубокую копию :

var copiedObject = jQuery.extend(true, {}, originalObject)
610 голосов
/ 05 мая 2015

В ECMAScript 6 есть метод Object.assign , который копирует значения всех перечисляемых собственных свойств из одного объекта в другой. Например:

var x = {myProp: "value"};
var y = Object.assign({}, x); 

Но имейте в виду, что вложенные объекты все еще копируются как ссылки.

183 голосов
/ 07 ноября 2016

за MDN :

  • Если вам нужна мелкая копия, используйте Object.assign({}, a)
  • Для "глубокой" копии используйте JSON.parse(JSON.stringify(a))

Нет необходимости во внешних библиотеках, но сначала нужно проверить совместимость браузера .

128 голосов
/ 19 марта 2012

Существует много ответов, но ни один из них не упоминает Object.create из ECMAScript 5, который, по общему признанию, не дает точной копии, но устанавливает источник как прототип нового объекта.

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

  1. Где такое наследство полезно (дух!)
  2. Там, где исходный объект не будет изменен, таким образом, связь между двумя объектами не будет проблемой.

Пример:

var foo = { a : 1 };
var bar = Object.create(foo);
foo.a; // 1
bar.a; // 1
foo.a = 2;
bar.a; // 2 - prototype changed
bar.a = 3;
foo.a; // Still 2, since setting bar.a makes it an "own" property

Почему я считаю это решение превосходным? Он родной, поэтому нет циклов, нет рекурсии. Однако для старых браузеров потребуется полифилл.

117 голосов
/ 15 декабря 2015

Элегантный способ клонировать объект Javascript в одну строку кода

Метод Object.assign является частью стандарта ECMAScript 2015 (ES6) и выполняет именно то, что вам нужно.

var clone = Object.assign({}, obj);

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

Подробнее ...

Полифил для поддержки старых браузеров:

if (!Object.assign) {
  Object.defineProperty(Object, 'assign', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function(target) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert first argument to object');
      }

      var to = Object(target);
      for (var i = 1; i < arguments.length; i++) {
        var nextSource = arguments[i];
        if (nextSource === undefined || nextSource === null) {
          continue;
        }
        nextSource = Object(nextSource);

        var keysArray = Object.keys(nextSource);
        for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
          var nextKey = keysArray[nextIndex];
          var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
          if (desc !== undefined && desc.enumerable) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
      return to;
    }
  });
}
77 голосов
/ 09 июля 2014

Есть несколько проблем с большинством решений в интернете. Поэтому я решил сделать продолжение, которое включает, почему принятый ответ не должен быть принят.

исходная ситуация

Я хочу глубокое копирование Javascript Object со всеми его детьми и их детьми и так далее. Но так как я не нормальный разработчик, мой Object имеет нормальный properties, circular structures и даже nested objects.

Итак, давайте сначала создадим circular structure и nested object.

function Circ() {
    this.me = this;
}

function Nested(y) {
    this.y = y;
}

Давайте соберем все вместе в Object с именем a.

var a = {
    x: 'a',
    circ: new Circ(),
    nested: new Nested('a')
};

Далее мы хотим скопировать a в переменную с именем b и изменить ее.

var b = a;

b.x = 'b';
b.nested.y = 'b';

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

console.log(a, b);

a --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

Теперь давайте найдем решение.

JSON

Первая попытка, которую я попробовал, использовала JSON.

var b = JSON.parse( JSON.stringify( a ) );

b.x = 'b';
b.nested.y = 'b';

Не тратьте на это слишком много времени, вы получите TypeError: Converting circular structure to JSON.

Рекурсивная копия (принятый "ответ")

Давайте посмотрим на принятый ответ.

function cloneSO(obj) {
    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        var copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        var copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = cloneSO(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        var copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = cloneSO(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

Хорошо выглядит, а? Это рекурсивная копия объекта и также обрабатывает другие типы, такие как Date, но это не было обязательным требованием.

var b = cloneSO(a);

b.x = 'b';
b.nested.y = 'b';

Рекурсия и circular structures плохо работают вместе ... RangeError: Maximum call stack size exceeded

нативный раствор

После спора с моим коллегой, мой начальник спросил нас, что случилось, и он нашел простое решение после некоторого поиска в Google. Это называется Object.create.

var b = Object.create(a);

b.x = 'b';
b.nested.y = 'b';

Это решение было добавлено в Javascript некоторое время назад и даже обрабатывает circular structure.

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

... и вы видите, это не сработало с вложенной структурой внутри.

полифилл для нативного раствора

В старом браузере есть полифилл для Object.create, такой же как IE 8. Это что-то вроде рекомендованного Mozilla, и, конечно, оно не идеально и приводит к той же проблеме, что и нативное решение .

function F() {};
function clonePF(o) {
    F.prototype = o;
    return new F();
}

var b = clonePF(a);

b.x = 'b';
b.nested.y = 'b';

Я поместил F вне области видимости, чтобы мы могли взглянуть на то, что instanceof говорит нам.

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> F {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> true

Та же проблема, что и у нативного решения , но вывод немного хуже.

лучшее (но не идеальное) решение

При копании я нашел похожий вопрос ( В Javascript, при выполнении глубокого копирования, как избежать цикла из-за свойства «this»? ) для этого, но с лучшим решением.

function cloneDR(o) {
    const gdcc = "__getDeepCircularCopy__";
    if (o !== Object(o)) {
        return o; // primitive value
    }

    var set = gdcc in o,
        cache = o[gdcc],
        result;
    if (set && typeof cache == "function") {
        return cache();
    }
    // else
    o[gdcc] = function() { return result; }; // overwrite
    if (o instanceof Array) {
        result = [];
        for (var i=0; i<o.length; i++) {
            result[i] = cloneDR(o[i]);
        }
    } else {
        result = {};
        for (var prop in o)
            if (prop != gdcc)
                result[prop] = cloneDR(o[prop]);
            else if (set)
                result[prop] = cloneDR(cache);
    }
    if (set) {
        o[gdcc] = cache; // reset
    } else {
        delete o[gdcc]; // unset again
    }
    return result;
}

var b = cloneDR(a);

b.x = 'b';
b.nested.y = 'b';

А давайте посмотрим на вывод ...

console.log(a, b);

a --> Object {
    x: "a",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "a"
    }
}

b --> Object {
    x: "b",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> false

Требования соответствуют, но все еще есть некоторые небольшие проблемы, включая изменение instance из nested и circ на Object.

Структура деревьев, имеющих общий лист, не будет скопирована, они станут двумя независимыми листьями:

        [Object]                     [Object]
         /    \                       /    \
        /      \                     /      \
      |/_      _\|                 |/_      _\|  
  [Object]    [Object]   ===>  [Object]    [Object]
       \        /                 |           |
        \      /                  |           |
        _\|  |/_                 \|/         \|/
        [Object]               [Object]    [Object]

вывод

Последнее решение, использующее рекурсию и кэш, может быть не лучшим, но это настоящая глубокая копия объекта. Он обрабатывает простые properties, circular structures и nested object, но при клонировании он испортит их экземпляр.

jsfiddle

76 голосов
/ 14 июня 2012

Если вы в порядке с мелкой копией, в библиотеке underscore.js есть метод clone .

y = _.clone(x);

или вы можете расширить его как

copiedObject = _.extend({},originalObject);
46 голосов
/ 06 июля 2017

ОК, представьте, что у вас есть этот объект ниже и вы хотите его клонировать:

let obj = {a:1, b:2, c:3}; //ES6

или

var obj = {a:1, b:2, c:3}; //ES5

ответ в основном зависит от того, какой ECMAscript вы используете, в ES6+, вы можете просто использовать Object.assign, чтобы сделать клон:

let cloned = Object.assign({}, obj); //new {a:1, b:2, c:3};

или используя оператор распространения, например:

let cloned = {...obj}; //new {a:1, b:2, c:3};

Но если вы используете ES5, вы можете использовать несколько методов, кроме JSON.stringify, просто убедитесь, что вы не используете большой кусок данных для копирования, но во многих случаях это может быть удобная однострочная строка как то так:

let cloned = JSON.parse(JSON.stringify(obj)); 
//new {a:1, b:2, c:3};, can be handy, but avoid using on big chunk of data over and over
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...