Питання Який найефективніший спосіб глибокого клонування об'єкта в JavaScript?


Який найефективніший спосіб клонувати об'єкт JavaScript? Я бачив obj = eval(uneval(o)); використовується, але це нестандартно і підтримується лише Firefox.

 Я зробив щось подібне obj = JSON.parse(JSON.stringify(o)); але питання ефективності.

 Я також бачив функції рекурсивного копіювання з різними вадами.
Я здивований, канонічне рішення не існує.


4548
2018-06-06 14:59


походження


Еваль не є злом. Використання eval погано є. Якщо ви боїтеся своїх побічних ефектів, ви використовуєте це неправильно. Побічні ефекти, які ви боїтеся, є причинами його використання. Хтось, до речі, відповідає на ваше запитання? - Prospero
Клонування об'єктів - складний бізнес, особливо з митними об'єктами довільних колекцій. Котрий певно чому там немає ніякого out-of-короля шлях зробити це. - b01
eval() це, як правило, погана ідея, тому що Оптимізатори движка Javascript повинні вимикатися при роботі з змінами, які встановлюються через eval. Тільки що eval() у вашому коді може призвести до погіршення продуктивності. - user568458
Можливі дублікати Найбільш елегантний спосіб клонувати об'єкт JavaScript - John Slegers
Ось порівняння ефективності найпоширеніших типів клонування об'єктів: jsben.ch/#/t917Z - EscapeNetscape


Відповіді:


Примітка: Це відповідь на іншу відповідь, а не на відповідну відповідь на це питання. Якщо ви хочете скористатися швидким клонуванням об'єкта, будь ласка, дотримуйтесь Порада Корбана у відповідь на це питання.


Я хочу зауважити, що .clone() метод в jQuery тільки клони елементів DOM. Для того, щоб клонувати об'єкти JavaScript, ви б:

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

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

Більше інформації можна знайти в jQuery документація.

Я також хочу зауважити, що глибокий копія насправді набагато розумніша, ніж показано вище - це дозволяє уникнути багатьох ловушок (наприклад, намагаючись глибоко розширити елемент DOM). Він часто використовується в ядрі jQuery, а в плагінах - до великого ефекту.


4067



Для тих, хто не зрозумів, відповідь Джона Резіга була, мабуть, передбачена як своєрідна відповідь / роз'яснення Відповідь ConroyP, а не прямий відповідь на питання. - S. Kirby
@ Тиф мастер github.com/jquery/jquery/blob/master/src/core.js на рядку 276 (є трохи коду, що робить щось інше, але код "як зробити це в JS" там :) - Rune FS
Ось JS-код за копією jQuery, для всіх бажаючих: github.com/jquery/jquery/blob/master/src/core.js#L265-327 - Alex W
Вауа! Просто бути надто зрозумілим: немає ідеї, чому ця відповідь була вибрана як правильна відповідь, це була відповідь на відповіді, наведені нижче: stackoverflow.com/a/122190/6524 (який рекомендував .clone(), який не є правильним кодом для використання в цьому контексті). На жаль, це питання пройшло так багато змін, перше обговорення вже не видно! Просто слідкуйте за порадою Корбана та напишіть цикл або скопіюйте властивості безпосередньо над новим об'єктом, якщо ви хочете швидкість. Або перевірити це самостійно! - John Resig
Як це зробити, не використовуючи jQuery? - Awesomeness01


Оформити замовлення за цим еталоном: http://jsben.ch/#/bWfk9

У моїх попередніх тестах, де швидкість була основною проблемою я знайшов

JSON.parse(JSON.stringify(obj))

бути найшвидшим способом глибокого клонування об'єкта (він виграє jQuery.extend з глибоким прапором встановлюється на 10-20%).

jQuery.extend досить швидко, коли глибокий прапорець встановлений на 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()  не робить перетворити назад в a Date об'єкт Щоб отримати докладнішу інформацію, див. Цю відповідь.

Крім того, будь ласка, зауважте, що в Chrome 65 принаймні, нативне клонування не є способом. Відповідно до це JSPerf, виконуючи нативний клонування, створюючи нову функцію, майже 800x повільніше, ніж за допомогою JSON.stringify, що є неймовірно швидким на всій платі.


1877



@trysis Object.create не клонує об'єкт, використовує об'єкт прототипу ... jsfiddle.net/rahpuser/yufzc1jt/2 - rahpuser
Цей метод також видаляє keys від твого object, які є functions як їх цінності, тому що JSON не підтримує функції. - Karlen Kishmiryan
Також майте на увазі, що використовуючи JSON.parse(JSON.stringify(obj)) Об'єкти Date також повернуть дату назад UTC в рядок представлення в ISO8601 формат - dnlgmzddr
Підхід JSON також дросель на кругових посиланнях. - rich remer
@velop, Object.assign ({}, objToClone) здається, що він робить неглибокий клон, - використовуючи його під час гри в консолі інструментів Dev, об'єкт клону все-таки вказує на посилання клонованого об'єкта. Тому я не думаю, що це дійсно застосовується тут. - Garrett Simpson


Припускаючи, що у вашому об'єкті є лише змінні, а не функції, ви можете просто скористатись:

var newObject = JSON.parse(JSON.stringify(oldObject));

402



con як цей підхід, як я тільки що знайшов, якщо ваш об'єкт має будь-які функції (у мене є внутрішні геттери та встановлювачі), то вони втрачаються при стропті .. Якщо все це потрібно, цей метод є добре .. - Markive
@ Джейсон, Причина, чому цей метод працює повільніше, ніж мізерне копіювання (на глибокому об'єкті), полягає в тому, що цей метод, за визначенням, глибоко копіює. Але з тих пір, як JSON реалізований у власному коді (у більшості браузерів), це буде значно швидше, ніж за допомогою будь-якого іншого рішення для глибокого копіювання на основі JavaScript, і може іноді бути швидше, ніж методика мелкого копіювання на базі JavaScript (див. jsperf.com/cloning-an-object/79) - MiJyn
JSON.stringify({key: undefined}) //=> "{}" - Web_Designer
ця техніка також знищить всіх Date об'єкти, що зберігаються всередині об'єкта, перетворення їх у строкову форму. - fstab
Вона не зможе копіювати будь-що, що не є частиною специфікації JSON (json.org) - cdmckay


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

HTML5 визначає внутрішній "структурований" алгоритм клонування що може створити глибокі клони об'єктів. Він як і раніше обмежується деякими вбудованими типами, але на додаток до кількох типів, підтримуваних JSON, він також підтримує дати, RegExps, карти, набори, крапки, файлові списки, ImageDatas, розріджені масиви, Введені масиви, і, можливо, ще більше в майбутньому. Він також зберігає посилання в клонованих даних, що дозволяє йому підтримувати циклічні та рекурсивні структури, які можуть призвести до помилок для JSON.

Пряма підтримка браузерів: незабаром?

Браузери наразі не забезпечують прямий інтерфейс для алгоритму структурованого клонування, але глобальний structuredClone() Функція активно обговорюється в whatwg / html # 793 на GitHub і може бути скоро! Як пропонується в даний час, його використання для більшості цілей буде настільки ж простим, як:

const clone = structuredClone(original);

Доки ця версія не буде відправлена, структуровані клони веб-переглядачів реалізуються лише косвенно.

Асинхронний обхід: корисний.

Нижчий накладний спосіб створити структурований клон із існуючими API-інтерфейсами полягає в публікації даних через один порт a MessageChannels. Інший порт буде випускати a 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 тимчасово не відповідає.

The 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();


274



@rynah Я просто переглянув специфікації знову, і ви маєте рацію: history.pushState() і history.replaceState() методи синхронно встановлюються history.state до структурованого клону свого першого аргументу. Трохи дивно, але це працює. Я оновлюю свою відповідь зараз. - Jeremy
Це просто так неправильно! Цей API не повинен використовуватися таким способом. - Fardin
Як хлопець, який реалізував pushState в Firefox, я відчуваю дивне поєднання гордості та відродження на цьому зламані. Добре, хлопці. - Justin L.


Якщо не було жодного вбудованого, можна спробувати:

    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;
    }


273



JQuery-рішення буде працювати для елементів DOM, але не тільки для будь-якого об'єкта. Mootools має таку ж межу. Бажаю, щоб вони мали загальний "клон" для простого об'єкта ... Рекурсивне рішення повинно працювати на що завгодно. Ймовірно, це шлях. - jschrab
Ця функція зламається, якщо клонований об'єкт має конструктор, який вимагає параметрів. Схоже, ми можемо змінити його на "var temp = new Object ()" і мати роботу в кожному випадку, ні? - Andrew Arnott
Андрій, якщо ви зміните його на var temp = new Object (), то ваш клон не матиме того самого прототипу, що й оригінальний об'єкт. Спробуйте скористатись: 'var newProto = function () {}; newProto.prototype = obj.constructor; var temp = new newProto (); ' - limscoder
Як і відповідь limscoder, див. Мою відповідь нижче, як це зробити, не викликаючи конструктора: stackoverflow.com/a/13333781/560114 - Matt Browne
Для об'єктів, що містять посилання на підрозділи (наприклад, мережі об'єктів), це не спрацьовує: якщо дві посилання вказують на один і той же суб-об'єкт, копія містить дві різні копії. І якщо є рекурсивні посилання, функція ніколи не закінчиться (ну, принаймні, не так, як вам це потрібно :-) Для цих загальних випадків вам слід додати словник об'єктів, вже скопійованих, і перевірити, чи вже ви скопіювали це ... Програмування складне, коли ви використовуєте просту мову - virtualnobi


Ефективний спосіб клонувати (не глибоко клонувати) об'єкт в одному рядку коду

Ан Object.assign Метод є частиною стандарту ECMAScript 2015 (ES6) і робить саме те, що вам потрібно.

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

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

Читати далі ...

The поліфіль для підтримки старих веб-переглядачів:

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;
    }
  });
}

132



Це не рекурсивно копіює, але насправді не дозволяє вирішити проблему клонування об'єкта. - mwhite
Цей метод працював, хоча я тестував декілька і _.extend ({} (, obj)) був найшвидшим: на 20x швидше, ніж JSON.parse і на 60% швидше, ніж Object.assign, наприклад. Він копіює всі суб-об'єкти досить добре. - Nico
@ ми маємо різницю між клоном і глибоким клоном. Ця відповідь насправді є клонуванням, але вона не глибоко клонувати. - Meirion Hughes
Оп запитав про глибокий клон. це не робить глибокого клону. - user566245
Вау так багато Object.assign відповіді, навіть не читаючи op's питання ... - martin8768


Код:

// 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);

91



а як на рахунок var obj = {} і obj.a = obj - neaumusic
Я не розумію цю функцію. Припустимо from.constructor є Date наприклад. Як буде третій if випробування можна досягти, коли 2-й if тест буде успішним і призведе до повернення функції (оскільки Date != Object && Date != Array)? - Adam McKee
@AdamMcKee тому що Передача аргументу javascript та присвоєння змінної є складним. Цей підхід чудово працює, включаючи дати (які дійсно обробляються другим тестом) - скрипти для тестування тут: jsfiddle.net/zqv9q9c6. - brichins
* Я мав на увазі "призначення параметра" (в межах тіла функції), а не "призначення змін". Хоча це допомагає добре розуміти і ретельно. - brichins
@NickSweet: спробуйте - можливо, це працює. Якщо ні - виправте це та оновіть відповідь. Ось як працює тут в спільноті :) - Kamarey