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

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

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

Ответы [ 63 ]

4 голосов
/ 05 августа 2016

Используйте глубинную копию от npm. Работает как в браузере, так и в узле как модуль npm ...

https://www.npmjs.com/package/deepcopy

let a = глубокая копия (b)

4 голосов
/ 16 апреля 2013

Обратитесь к http://www.w3.org/html/wg/drafts/html/master/infrastructure.html#safe-passing-of-structured-data за алгоритмом W3C «Безопасная передача структурированных данных», предназначенным для реализации браузерами для передачи данных, например, веб-работникам. Тем не менее, он имеет некоторые ограничения в том, что он не обрабатывает функции. См. https://developer.mozilla.org/en-US/docs/DOM/The_structured_clone_algorithm для получения дополнительной информации, включая альтернативный алгоритм в JS, который поможет вам в этом.

4 голосов
/ 07 июля 2012

Bellow - моя версия глубокого клонирования, охватывающая функции и обработку циклических ссылок

https://github.com/radsimu/UaicNlpToolkit/blob/master/Modules/GGS/GGSEngine/src/main/resources/ro/uaic/info/nlptools/ggs/engine/core/jsInitCode.js#L17

3 голосов
/ 09 ноября 2018

Согласно Руководству по стилю JavaScript для Airbnb с 404 участниками:

Предпочитают оператор растягивания объекта над Object.assign для мелкого копирования объекты. Используйте оператор оставления объекта, чтобы получить новый объект с определенным свойства опущены.

// very bad
const original = { a: 1, b: 2 };
const copy = Object.assign(original, { c: 3 }); // this mutates `original` ಠ_ಠ
delete copy.a; // so does this

// bad
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }

// good
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }

const { a, ...noA } = copy; // noA => { b: 2, c: 3 }

Также я хотел бы предупредить вас, что даже несмотря на то, что Airbnb вряд ли рекомендует подход оператора распространения объекта. Помните, что Microsoft Edge все еще не поддерживает эту функцию 2018 года.

ES2016 + Сравнительная таблица >>

3 голосов
/ 12 декабря 2018

(Следующее было главным образом интеграция @ Мацей Буковски , @ А. Леви , @ Ян Туро , @ Реду и ответы @ LeviRoberts , @ RobG , большое им спасибо !!!)

Глубокая копия ? - ДА! (В основном);
Мелкая копия ? - НЕТ! (кроме Proxy).

Искренне приветствую всех на тестировании clone().
Кроме того, defineProp() предназначен для простого и быстрого (пере) определения или копирования любого типа дескриптора.

Функция

function clone(object) {
  /*
    Deep copy objects by value rather than by reference,
    exception: `Proxy`
  */

  const seen = new WeakMap()

  return (function clone(object) {
    if (object !== Object(object)) return object /*
    —— Check if the object belongs to a primitive data type */

    if (object instanceof Node) return object.cloneNode(true) /*
    —— Clone DOM trees */

    let _object // The clone of object

    switch (object.constructor) {
      case Object:
      case Array:
        _object = cloneObject(object)
        break

      case Date:
        _object = new Date(+object)
        break

      case Function:
        const fnStr = String(object)

        _object = new Function("return " +
          (/^(?!function |[^{]+?=>)[^(]+?\(/.test(fnStr)
            ? "function " : ""
          ) + fnStr
        ).call(object)

        Object.defineProperties(_object,
          Object.getOwnPropertyDescriptors(object)
        )
        break

      default:
        switch (Object.prototype.toString.call(object.constructor)) {
          //                              // Stem from:
          case "[object Function]":       // `class`
          case "[object Undefined]":      // `Object.create(null)`
            _object = cloneObject(object)
            break

          default:                        // `Proxy`
            _object = object
        }
    }

    return _object


    function cloneObject(object) {
      if (seen.has(object)) return seen.get(object) /*
      —— Handle recursive references (circular structures) */

      const _object = Array.isArray(object)
        ? []
        : Object.create(Object.getPrototypeOf(object)) /*
          —— Assign [[Prototype]] for inheritance */

      seen.set(object, _object) /*
      —— Make `_object` the associative mirror of `object` */

      Reflect.ownKeys(object).forEach(key =>
        defineProp(_object, key, { value: clone(object[key]) }, object)
      )

      return _object
    }
  })(object)
}


function defineProp(object, key, descriptor = {}, copyFrom = {}) {
  const prevDesc = Object.getOwnPropertyDescriptor(object, key)
    || { configurable: true, writable: true }
    , copyDesc = Object.getOwnPropertyDescriptor(copyFrom, key)
      || { configurable: true, writable: true } // Custom…
      || {} // …or left to native default settings

  const { configurable: _configurable, writable: _writable } = prevDesc
    , test = _writable === undefined
      ? _configurable // Can redefine property
      : _configurable && _writable // Can assign to property

  if (!test || arguments.length <= 2) return test;

  ["get", "set", "value", "writable", "enumerable", "configurable"]
    .forEach(k =>
      descriptor[k] === undefined && (descriptor[k] = copyDesc[k])
    )

  const { get, set, value, writable, enumerable, configurable }
    = descriptor

  return Object.defineProperty(object, key, {
    enumerable, configurable, ...get || set
      ? { get, set } // Accessor descriptor
      : { value, writable } // Data descriptor
  })
}

// Тесты

"use strict"
const obj0 = {

  u: undefined,

  nul: null,

  t: true,

  n: 9,

  str1: "string",

  str2: "",

  sym: Symbol("symbol"),

  [Symbol("e")]: Math.E,

  f: {
    getAccessorStr(object) {
      return []
        .concat(...
          Object.values(Object.getOwnPropertyDescriptors(object))
            .filter(desc => desc.writable === undefined)
            .map(desc => Object.values(desc))
        )
        .filter(prop => typeof prop === "function")
        .map(String)
    },
    f0: function f0() { },
    f1: function () { },
    f2: a => a / (a + 1),
    f3: () => 0,
    f4(params) { return param => param + params },
    f5: (a, b) => ({ c = 0 } = {}) => a + b + c
  },

  o: {
    n: 0,
    o: {
      f: function (...args) { }
    }
  },

  arr: [[0], [1, 2]],

  d: new Date(),

  get g() { return 0 }
}

defineProp(obj0, "s", {
  set(v) { this._s = v }
})
defineProp(obj0.arr, "tint", {
  value: { is: "non-enumerable" }
})
obj0.arr[0].name = "nested array"


let obj1 = clone(obj0)
obj1.o.n = 1
obj1.o.o.g = function g(a = 0, b = 0) { return a + b }
obj1.arr[1][1] = 3
obj1.d.setTime(+obj0.d + 60 * 1000)
obj1.arr.tint.is = "enumerable? no"
obj1.arr[0].name = "a nested arr"
defineProp(obj1, "s", {
  set(v) { this._s = v + 1 }
})

console.log("\n\n" + "-".repeat(2 ** 6))




console.log(">:>: Test - Routinely")

console.log("obj0:\n ", JSON.stringify(obj0))
console.log("obj1:\n ", JSON.stringify(obj1))
console.log()

console.log("obj0:\n ", obj0)
console.log("obj1:\n ", obj1)
console.log()

console.log("obj0\n ",
  ".arr.tint:", obj0.arr.tint, "\n ",
  ".arr[0].name:", obj0.arr[0].name
)
console.log("obj1\n ",
  ".arr.tint:", obj1.arr.tint, "\n ",
  ".arr[0].name:", obj1.arr[0].name
)
console.log()

console.log("Accessor-type descriptor\n ",
  "of obj0:", obj0.f.getAccessorStr(obj0), "\n ",
  "of obj1:", obj1.f.getAccessorStr(obj1), "\n ",
  "set (obj0 & obj1) .s :", obj0.s = obj1.s = 0, "\n ",
  "  → (obj0 , obj1) ._s:", obj0._s, ",", obj1._s
)

console.log("—— obj0 has not been interfered.")

console.log("\n\n" + "-".repeat(2 ** 6))




console.log(">:>: Test - Circular structures")

obj0.o.r = {}
obj0.o.r.recursion = obj0.o
obj0.arr[1] = obj0.arr

obj1 = clone(obj0)
console.log("obj0:\n ", obj0)
console.log("obj1:\n ", obj1)

console.log("Clear obj0's recursion:",
  obj0.o.r.recursion = null, obj0.arr[1] = 1
)
console.log(
  "obj0\n ",
  ".o.r:", obj0.o.r, "\n ",
  ".arr:", obj0.arr
)
console.log(
  "obj1\n ",
  ".o.r:", obj1.o.r, "\n ",
  ".arr:", obj1.arr
)
console.log("—— obj1 has not been interfered.")


console.log("\n\n" + "-".repeat(2 ** 6))




console.log(">:>: Test - Classes")

class Person {
  constructor(name) {
    this.name = name
  }
}

class Boy extends Person { }
Boy.prototype.sex = "M"

const boy0 = new Boy
boy0.hobby = { sport: "spaceflight" }

const boy1 = clone(boy0)
boy1.hobby.sport = "superluminal flight"

boy0.name = "one"
boy1.name = "neo"

console.log("boy0:\n ", boy0)
console.log("boy1:\n ", boy1)
console.log("boy1's prototype === boy0's:",
  Object.getPrototypeOf(boy1) === Object.getPrototypeOf(boy0)
)

Ссылки

  1. Object.create() | MDN
  2. Object.defineProperties() | MDN
  3. Перечислимость и владение недвижимостью | MDN
  4. TypeError: значение циклического объекта | MDN

Используемые языковые трюки

  1. Условно добавить объект на объект
3 голосов
/ 31 января 2015

Клонировать объект на основе «шаблона». Что вы делаете, если вам не нужна точная копия, но вам нужна надежность какой-либо надежной операции клонирования, но вам нужны только клонированные биты или вы хотите убедиться, что можете контролировать существование или формат каждого значения атрибута клонировали?

Я помогаю, потому что это полезно для нас, и мы создали это, потому что мы не смогли найти что-то подобное. Вы можете использовать его для клонирования объекта, основанного на объекте 'template', который указывает, какие атрибуты объекта я хочу клонировать, а шаблон позволяет функциям преобразовывать эти атрибуты в нечто иное, если их нет в исходном объекте. или как бы вы ни хотели справиться с клоном. Если это бесполезно, я уверен, что кто-то может удалить этот ответ.

   function isFunction(functionToCheck) {
       var getType = {};
       return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
   }

   function cloneObjectByTemplate(obj, tpl, cloneConstructor) {
       if (typeof cloneConstructor === "undefined") {
           cloneConstructor = false;
       }
       if (obj == null || typeof (obj) != 'object') return obj;

       //if we have an array, work through it's contents and apply the template to each item...
       if (Array.isArray(obj)) {
           var ret = [];
           for (var i = 0; i < obj.length; i++) {
               ret.push(cloneObjectByTemplate(obj[i], tpl, cloneConstructor));
           }
           return ret;
       }

       //otherwise we have an object...
       //var temp:any = {}; // obj.constructor(); // we can't call obj.constructor because typescript defines this, so if we are dealing with a typescript object it might reset values.
       var temp = cloneConstructor ? new obj.constructor() : {};

       for (var key in tpl) {
           //if we are provided with a function to determine the value of this property, call it...
           if (isFunction(tpl[key])) {
               temp[key] = tpl[key](obj); //assign the result of the function call, passing in the value
           } else {
               //if our object has this property...
               if (obj[key] != undefined) {
                   if (Array.isArray(obj[key])) {
                       temp[key] = [];
                       for (var i = 0; i < obj[key].length; i++) {
                           temp[key].push(cloneObjectByTemplate(obj[key][i], tpl[key], cloneConstructor));
                       }
                   } else {
                       temp[key] = cloneObjectByTemplate(obj[key], tpl[key], cloneConstructor);
                   }
               }
           }
       }

       return temp;
   }

Простой способ назвать это было бы так:

var source = {
       a: "whatever",
       b: {
           x: "yeah",
           y: "haha"
       }
   };
   var template = {
       a: true, //we want to clone "a"
       b: {
           x: true //we want to clone "b.x" too
       }
   }; 
   var destination = cloneObjectByTemplate(source, template);

Если вы хотите использовать функцию, чтобы убедиться, что атрибут возвращен или чтобы убедиться, что это определенный тип, используйте такой шаблон. Вместо использования {ID: true} мы предоставляем функцию, которая по-прежнему просто копирует атрибут ID исходного объекта, но при этом гарантирует, что это число, даже если его нет в исходном объекте.

 var template = {
    ID: function (srcObj) {
        if(srcObj.ID == undefined){ return -1; }
        return parseInt(srcObj.ID.toString());
    }
}

Массивы отлично клонируются, но если вы хотите, вы можете иметь свою собственную функцию, которая обрабатывает и эти отдельные атрибуты, и делать что-то особенное, например:

 var template = {
    tags: function (srcObj) {
        var tags = [];
        if (process.tags != undefined) {
            for (var i = 0; i < process.tags.length; i++) {

                tags.push(cloneObjectByTemplate(
                  srcObj.tags[i],
                  { a : true, b : true } //another template for each item in the array
                );
            }
        }
        return tags;
    }
 }

Таким образом, в приведенном выше примере наш шаблон просто копирует атрибут 'tags' исходного объекта, если он существует (он считается массивом), и для каждого элемента в этом массиве вызывается функция клонирования, чтобы индивидуально клонировать его на основе второго шаблона, который просто копирует атрибуты «a» и «b» каждого из этих элементов тега.

Если вы забираете объекты в узел и выходите из него и хотите контролировать, какие атрибуты этих объектов клонируются, это отличный способ управлять этим в файле node.js, а код также работает в браузере.

Вот пример его использования: http://jsfiddle.net/hjchyLt1/

3 голосов
/ 17 января 2014

Вы можете использовать функциональное закрытие, чтобы получить все преимущества глубокой копии без глубокой копии. Это совсем другая парадигма, но работает хорошо. Вместо того, чтобы пытаться скопировать существующий объект, просто используйте функцию для создания экземпляра нового объекта, когда он вам нужен.

Сначала создайте функцию, которая возвращает объект

function template() {
  return {
    values: [1, 2, 3],
    nest: {x: {a: "a", b: "b"}, y: 100}
  };
}

Затем создайте простую функцию поверхностного копирования

function copy(a, b) {
  Object.keys(b).forEach(function(key) {
    a[key] = b[key];
  });
}

Создайте новый объект и скопируйте на него свойства шаблона

var newObject = {}; 
copy(newObject, template());

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

var newObject = template();

Теперь, когда у вас есть новый объект, проверьте, каковы его свойства:

console.log(Object.keys(newObject));

Отображается:

["values", "nest"]

Да, это собственные свойства newObject, а не ссылки на свойства другого объекта. Давайте просто проверим:

console.log(newObject.nest.x.b);

Отображается:

"b"

newObject получил все свойства объекта шаблона, но не содержит никакой цепочки зависимостей.

http://jsbin.com/ISUTIpoC/1/edit?js,console

Я добавил этот пример, чтобы спровоцировать некоторые дебаты, поэтому, пожалуйста, добавьте несколько комментариев:)

3 голосов
/ 11 августа 2013

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

Стандарт 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();
2 голосов
/ 28 октября 2014

Я пробовал это в случае скалярного объекта, и он работает для меня:

function binder(i) {
  return function () {
    return i;
  };
}

a=1;
b=binder(a)(); // copy value of a into b

alert(++a); // 2
alert(b); // still 1

Привет.

2 голосов
/ 19 августа 2016

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

И недооцененный Слабая карта сводится к проблеме циклов, в которой хранение пар ссылок на старый и новый объект может помочь нам довольно легко воссоздать все дерево.

Я предотвратил глубокое клонирование элементов DOM, возможно, вы не хотите клонировать всю страницу:)

function deepCopy(object) {
    const cache = new WeakMap(); // Map of old - new references

    function copy(obj) {
        if (typeof obj !== 'object' ||
            obj === null ||
            obj instanceof HTMLElement
        )
            return obj; // primitive value or HTMLElement

        if (obj instanceof Date) 
            return new Date().setTime(obj.getTime());

        if (obj instanceof RegExp) 
            return new RegExp(obj.source, obj.flags);

        if (cache.has(obj)) 
            return cache.get(obj);

        const result = obj instanceof Array ? [] : {};

        cache.set(obj, result); // store reference to object before the recursive starts

        if (obj instanceof Array) {
            for(const o of obj) {
                 result.push(copy(o));
            }
            return result;
        }

        const keys = Object.keys(obj); 

        for (const key of keys)
            result[key] = copy(obj[key]);

        return result;
    }

    return copy(object);
}

Некоторые тесты:

// #1
const obj1 = { };
const obj2 = { };
obj1.obj2 = obj2;
obj2.obj1 = obj1; // Trivial circular reference

var copy = deepCopy(obj1);
copy == obj1 // false
copy.obj2 === obj1.obj2 // false
copy.obj2.obj1.obj2 // and so on - no error (correctly cloned).

// #2
const obj = { x: 0 }
const clone = deepCopy({ a: obj, b: obj });
clone.a == clone.b // true

// #3
const arr = [];
arr[0] = arr; // A little bit weird but who cares
clone = deepCopy(arr)
clone == arr // false;
clone[0][0][0][0] == clone // true;

ПРИМЕЧАНИЕ. Я использую константы для цикла, оператор => и WeakMaps для создания более существенного кода. Этот синтаксис (ES6) поддерживается современными браузерами

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...