Каков наиболее эффективный способ глубокого клонирования объекта в JavaScript? - PullRequest
5052 голосов
/ 23 сентября 2008

Какой самый эффективный способ клонировать объект JavaScript? Я видел, как используется obj = eval(uneval(o));, но это нестандартно и поддерживается только Firefox .

Я делал такие вещи, как obj = JSON.parse(JSON.stringify(o));, но ставлю под сомнение эффективность.

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

Ответы [ 74 ]

4283 голосов
/ 23 сентября 2008

Примечание: Это ответ на другой ответ, а не правильный ответ на этот вопрос. Если вы хотите провести быстрое клонирование объектов, следуйте совету Корбана в ответе на этот вопрос.


Хочу отметить, что метод .clone() в jQuery только клонирует элементы DOM. Чтобы клонировать объекты JavaScript, вы должны сделать:

// Shallow copy
var newObject = jQuery.extend({}, oldObject);

// Deep copy
var newObject = jQuery.extend(true, {}, oldObject);

Дополнительную информацию можно найти в документации jQuery .

Я также хочу отметить, что глубокая копия на самом деле намного умнее, чем показано выше - она ​​способна избежать многих ловушек (например, пытаясь глубоко расширить элемент DOM). Он часто используется в ядре jQuery и в плагинах.

2166 голосов
/ 17 марта 2011

Оформить заказ: http://jsben.ch/#/bWfk9

В моих предыдущих тестах, где скорость была главной проблемой, я обнаружил

JSON.parse(JSON.stringify(obj))

- самый быстрый способ глубокого клонирования объекта (он выбивает jQuery.extend с глубоким флагом, установленным на 10-20%).

jQuery.extend работает довольно быстро, когда для флага Deep установлено значение false (мелкий клон). Это хороший вариант, потому что он включает некоторую дополнительную логику для проверки типа и не копирует неопределенные свойства и т. Д., Но это также немного замедлит работу.

Если вы знаете структуру объектов, которые вы пытаетесь клонировать, или можете избежать вложенных массивов, вы можете написать простой цикл for (var i in obj) для клонирования вашего объекта при проверке hasOwnProperty, и он будет намного быстрее, чем jQuery.

Наконец, если вы пытаетесь клонировать известную структуру объекта в горячем цикле, вы можете получить НАМНОГО БОЛЬШЕ ЭФФЕКТИВНОСТИ, просто вставив процедуру клонирования и вручную создав объект.

Механизмы трассировки JavaScript не работают при оптимизации циклов for..in, а проверка hasOwnProperty также замедлит работу. Ручное клонирование, когда скорость абсолютно необходима.

var clonedObject = {
  knownProp: obj.knownProp,
  ..
}

Осторожно, используя метод JSON.parse(JSON.stringify(obj)) для Date объектов - JSON.stringify(new Date()) возвращает строковое представление даты в формате ISO, которое JSON.parse() не преобразует обратно в Date объект. Подробнее см. В этом ответе .

Кроме того, обратите внимание, что, по крайней мере, в Chrome 65, собственное клонирование не подходит. Согласно этого JSPerf выполнение нативного клонирования путем создания новой функции почти на 800x медленнее, чем с использованием JSON.stringify, который невероятно быстр во всех направлениях.

450 голосов
/ 04 января 2011

Предполагая, что в вашем объекте есть только переменные, а не какие-либо функции, вы можете просто использовать:

var newObject = JSON.parse(JSON.stringify(oldObject));
332 голосов
/ 06 июня 2012

Структурированное клонирование

Стандарт HTML включает в себя внутренний структурированный алгоритм клонирования / сериализации , который может создавать глубокие клоны объектов. Он по-прежнему ограничен определенными встроенными типами, но в дополнение к нескольким типам, поддерживаемым JSON, он также поддерживает Dates, RegExps, Карты, Наборы, BLOB-объекты, FileLists, ImageDatas, разреженные массивы, Typed Arrays и, возможно, больше в будущем. , Он также сохраняет ссылки в клонированных данных, что позволяет поддерживать циклические и рекурсивные структуры, которые могут вызвать ошибки для JSON.

Поддержка в Node.js: экспериментальная ?

Модуль v8 в Node.js в настоящее время (по состоянию на Узле 11) напрямую предоставляет структурированный API сериализации , но эта функциональность по-прежнему помечена как «экспериментальная» и может быть изменена или удалена в будущие версии. Если вы используете совместимую версию, клонирование объекта так же просто, как:

const v8 = require('v8');

const structuredClone = obj => {
  return v8.deserialize(v8.serialize(obj));
};

Прямая поддержка в браузерах: возможно, в конце концов? ?

Браузеры в настоящее время не предоставляют прямой интерфейс для алгоритма структурированного клонирования, но глобальная функция structuredClone() обсуждалась в whatwg / html # 793 на GitHub . Как предлагается в настоящее время, использовать его для большинства целей так же просто, как:

const clone = structuredClone(original);

Если это не сделано, реализации структурированных клонов в браузерах предоставляются только косвенно.

Асинхронный обходной путь: можно использовать. ?

Более простой способ создания структурированного клона с существующими API-интерфейсами - это отправка данных через один порт MessageChannels . Другой порт будет генерировать событие message со структурированным клоном присоединенного .data. К сожалению, прослушивание этих событий обязательно асинхронно, а синхронные альтернативы менее практичны.

class StructuredCloner {
  constructor() {
    this.pendingClones_ = new Map();
    this.nextKey_ = 0;

    const channel = new MessageChannel();
    this.inPort_ = channel.port1;
    this.outPort_ = channel.port2;

    this.outPort_.onmessage = ({data: {key, value}}) => {
      const resolve = this.pendingClones_.get(key);
      resolve(value);
      this.pendingClones_.delete(key);
    };
    this.outPort_.start();
  }

  cloneAsync(value) {
    return new Promise(resolve => {
      const key = this.nextKey_++;
      this.pendingClones_.set(key, resolve);
      this.inPort_.postMessage({key, value});
    });
  }
}

const structuredCloneAsync = window.structuredCloneAsync =
    StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner);

Пример использования:

const main = async () => {
  const original = { date: new Date(), number: Math.random() };
  original.self = original;

  const clone = await structuredCloneAsync(original);

  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));

  console.log("Assertions complete.");
};

main();

Синхронные обходные пути: Ужасно! ?

Нет хороших вариантов для синхронного создания структурированных клонов. Вот пара непрактичных взломов.

history.pushState() и history.replaceState() оба создают структурированный клон своего первого аргумента и присваивают это значение history.state. Вы можете использовать это для создания структурированного клона любого объекта, подобного этому:

const structuredClone = obj => {
  const oldState = history.state;
  history.replaceState(obj, null);
  const clonedObj = history.state;
  history.replaceState(oldState, null);
  return clonedObj;
};

Пример использования:

'use strict';

const main = () => {
  const original = { date: new Date(), number: Math.random() };
  original.self = original;

  const clone = structuredClone(original);
  
  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));
  
  console.log("Assertions complete.");
};

const structuredClone = obj => {
  const oldState = history.state;
  history.replaceState(obj, null);
  const clonedObj = history.state;
  history.replaceState(oldState, null);
  return clonedObj;
};

main();

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

Конструктор Notification создает структурированный клон связанных с ним данных. Он также пытается отобразить уведомление в браузере для пользователя, но это молча завершится ошибкой, если вы не запросили разрешение на уведомление. Если у вас есть разрешение на другие цели, мы немедленно закроем созданное нами уведомление.

const structuredClone = obj => {
  const n = new Notification('', {data: obj, silent: true});
  n.onshow = n.close.bind(n);
  return n.data;
};

Пример использования:

'use strict';

const main = () => {
  const original = { date: new Date(), number: Math.random() };
  original.self = original;

  const clone = structuredClone(original);
  
  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));
  
  console.log("Assertions complete.");
};

const structuredClone = obj => {
  const n = new Notification('', {data: obj, silent: true});
  n.close();
  return n.data;
};

main();
308 голосов
/ 23 сентября 2008

Если бы не было встроенного, вы можете попробовать:

function clone(obj) {
    if (obj === null || typeof (obj) !== 'object' || 'isActiveClone' in obj)
        return obj;

    if (obj instanceof Date)
        var temp = new obj.constructor(); //or new Date(obj);
    else
        var temp = obj.constructor();

    for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            obj['isActiveClone'] = null;
            temp[key] = clone(obj[key]);
            delete obj['isActiveClone'];
        }
    }
    return temp;
}
152 голосов
/ 15 декабря 2015

Эффективный способ клонирования (не глубокого клонирования) объекта в одну строку кода

Метод 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;
    }
  });
}
96 голосов
/ 25 июня 2009

Код:

// extends 'from' object with members from 'to'. If 'to' is null, a deep clone of 'from' is returned
function extend(from, to)
{
    if (from == null || typeof from != "object") return from;
    if (from.constructor != Object && from.constructor != Array) return from;
    if (from.constructor == Date || from.constructor == RegExp || from.constructor == Function ||
        from.constructor == String || from.constructor == Number || from.constructor == Boolean)
        return new from.constructor(from);

    to = to || new from.constructor();

    for (var name in from)
    {
        to[name] = typeof to[name] == "undefined" ? extend(from[name], null) : to[name];
    }

    return to;
}

Тест:

var obj =
{
    date: new Date(),
    func: function(q) { return 1 + q; },
    num: 123,
    text: "asdasd",
    array: [1, "asd"],
    regex: new RegExp(/aaa/i),
    subobj:
    {
        num: 234,
        text: "asdsaD"
    }
}

var clone = extend(obj);
89 голосов
/ 12 декабря 2009

Вот что я использую:

function cloneObject(obj) {
    var clone = {};
    for(var i in obj) {
        if(typeof(obj[i])=="object" && obj[i] != null)
            clone[i] = cloneObject(obj[i]);
        else
            clone[i] = obj[i];
    }
    return clone;
}
76 голосов
/ 19 сентября 2014

Глубокая копия по производительности: Рейтинг от лучшего к худшему

  • Переназначение "=" (строковые массивы, только числовые массивы)
  • Срез (строковые массивы, только числовые массивы)
  • Конкатенация (строковые массивы, только числовые массивы)
  • Пользовательская функция: для цикла или рекурсивного копирования
  • jQuery's $ .extend
  • JSON.parse (строковые массивы, числовые массивы, только объектные массивы)
  • Underscore.js '_.clone (строковые массивы, только числовые массивы)
  • _.cloneDeep Lo-Dash

Глубокое копирование массива строк или чисел (один уровень - без ссылочных указателей):

Когда массив содержит числа и строки - такие функции, как .slice (), .concat (), .splice (), оператор присваивания "=" и функция клона Underscore.js; создаст глубокую копию элементов массива.

Там, где переназначение выполняется быстрее всего:

var arr1 = ['a', 'b', 'c'];
var arr2 = arr1;
arr1 = ['a', 'b', 'c'];

И .slice () имеет лучшую производительность, чем .concat (), http://jsperf.com/duplicate-array-slice-vs-concat/3

var arr1 = ['a', 'b', 'c'];  // Becomes arr1 = ['a', 'b', 'c']
var arr2a = arr1.slice(0);   // Becomes arr2a = ['a', 'b', 'c'] - deep copy
var arr2b = arr1.concat();   // Becomes arr2b = ['a', 'b', 'c'] - deep copy

Глубокое копирование массива объектов (два или более уровня - ссылочные указатели):

var arr1 = [{object:'a'}, {object:'b'}];

Написать пользовательскую функцию (имеет более высокую производительность, чем $ .extend () или JSON.parse):

function copy(o) {
   var out, v, key;
   out = Array.isArray(o) ? [] : {};
   for (key in o) {
       v = o[key];
       out[key] = (typeof v === "object" && v !== null) ? copy(v) : v;
   }
   return out;
}

copy(arr1);

Использование сторонних утилит:

$.extend(true, [], arr1); // Jquery Extend
JSON.parse(arr1);
_.cloneDeep(arr1); // Lo-dash

Где jQuery $ .extend имеет лучшую производительность:

62 голосов
/ 26 декабря 2009
var clone = function() {
    var newObj = (this instanceof Array) ? [] : {};
    for (var i in this) {
        if (this[i] && typeof this[i] == "object") {
            newObj[i] = this[i].clone();
        }
        else
        {
            newObj[i] = this[i];
        }
    }
    return newObj;
}; 

Object.defineProperty( Object.prototype, "clone", {value: clone, enumerable: false});
...