Pregunta ¿Cuál es la forma más eficiente de clonar un objeto en JavaScript?


¿Cuál es la forma más eficiente de clonar un objeto de JavaScript? He visto obj = eval(uneval(o)); siendo usado, pero eso no es estándar y solo es compatible con Firefox.

 He hecho cosas como obj = JSON.parse(JSON.stringify(o)); pero cuestiona la eficiencia.

 También he visto funciones de copia recursivas con varios defectos.
Me sorprende que no exista una solución canónica.


4538
2018-06-06 14:59


origen


Respuestas:


Nota: Esta es una respuesta a otra respuesta, no una respuesta adecuada a esta pregunta. Si desea tener clonación rápida de objetos, siga El consejo de Corban en su respuesta a esta pregunta.


Quiero señalar que el .clone() método en jQuery solo clona elementos DOM. Para clonar objetos JavaScript, harías:

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

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

Se puede encontrar más información en documentación jQuery.

También quiero señalar que la copia profunda es en realidad mucho más inteligente que lo que se muestra arriba; es capaz de evitar muchas trampas (tratando de extender en profundidad un elemento DOM, por ejemplo). Se usa frecuentemente en jQuery core y en complementos para un gran efecto.


4062



Verifique este punto de referencia: http://jsben.ch/#/bWfk9

En mis pruebas anteriores donde la velocidad era una preocupación principal que encontré

JSON.parse(JSON.stringify(obj))

ser la forma más rápida de clonar un objeto en profundidad (supera a jQuery.extend con bandera profunda establecida verdadera en 10-20%).

jQuery.extend es bastante rápido cuando el indicador de profundidad se establece en falso (clon superficial). Es una buena opción, ya que incluye un poco de lógica adicional para la validación de tipo y no copia sobre propiedades indefinidas, etc., pero esto también lo desacelerará un poco.

Si conoce la estructura de los objetos que intenta clonar o puede evitar matrices anidadas profundas, puede escribir un simple for (var i in obj) para clonar su objeto mientras comprueba hasOwnProperty y será mucho más rápido que jQuery.

Por último, si intenta clonar una estructura de objetos conocida en un bucle caliente, puede obtener MUCHO MÁS RENDIMIENTO simplemente alineando el procedimiento de clonación y construyendo manualmente el objeto.

Los motores de rastreo de JavaScript aspiran a optimizar for..in los bucles y comprobar hasOwnProperty también lo ralentizará. Clonar manualmente cuando la velocidad es una necesidad absoluta.

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

Ten cuidado al usar JSON.parse(JSON.stringify(obj)) método en Date objetos - JSON.stringify(new Date()) devuelve una representación de cadena de la fecha en formato ISO, que JSON.parse()  no lo hace convertir de nuevo a una Date objeto. Vea esta respuesta para más detalles.

Además, tenga en cuenta que, al menos en Chrome 65, la clonación nativa no es el camino a seguir. De acuerdo a este JSPerf, realizar clonación nativa creando una nueva función es casi 800x más lento que usar JSON.stringify, que es increíblemente rápido en todos los ámbitos.


1868



Suponiendo que solo tiene variables y no tiene funciones en su objeto, puede usar:

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

402



Clonación estructurada

HTML5 define un algoritmo de clonación interno "estructurado" que puede crear clones profundos de objetos. Todavía está limitado a ciertos tipos incorporados, pero además de los pocos tipos admitidos por JSON, también admite fechas, RegExps, Maps, Sets, Blobs, FileLists, ImageDatas, Arreglos dispersos, Matrices mecanografiadas, y probablemente más en el futuro. También conserva las referencias dentro de los datos clonados, lo que le permite soportar estructuras cíclicas y recursivas que causarían errores para JSON.

Soporte directo en navegadores: ¿próximamente?

Los navegadores actualmente no proporcionan una interfaz directa para el algoritmo de clonación estructurado, sino un structuredClone() función se está discutiendo activamente en whatwg / html # 793 en GitHub ¡y puede llegar pronto! Como se propone actualmente, su uso para la mayoría de los propósitos será tan simple como:

const clone = structuredClone(original);

Hasta que esto se envíe, las implementaciones clonadas de los navegadores solo se exponen indirectamente.

Solución asincrónica: utilizable.

La forma de gastos menores para crear un clon estructurado con API existentes es publicar los datos a través de un puerto de un MessageChannels. El otro puerto emitirá un message evento con un clon estructurado de la adjunta .data. Desafortunadamente, escuchar estos eventos es necesariamente asincrónico, y las alternativas síncronas son menos prácticas.

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

Ejemplo de uso:

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

Soluciones sincrónicas: ¡horrible!

No hay buenas opciones para crear clones estructurados sincrónicamente. Aquí hay un par de hacks poco prácticos en su lugar.

history.pushState() y history.replaceState() ambos crean un clon estructurado de su primer argumento, y le asignan ese valor a history.state. Puede usar esto para crear un clon estructurado de cualquier objeto como este:

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

Ejemplo de uso:

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

Aunque sincrónico, esto puede ser extremadamente lento. Se incurre en todos los gastos generales asociados con la manipulación del historial del navegador. Llamar a este método repetidamente puede hacer que Chrome deje de responder temporalmente.

los Notification constructor crea un clon estructurado de sus datos asociados. También intenta mostrar una notificación del navegador al usuario, pero esto fallará en silencio a menos que haya solicitado permiso de notificación. En caso de que tenga el permiso para otros fines, cerraremos de inmediato la notificación que hemos creado.

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

Ejemplo de uso:

'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



Si no hubiera ninguno incorporado, podrías intentar:

    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



La forma eficiente de clonar (no clonar profundamente) un objeto en una línea de código

Un Object.assign método es parte del estándar ECMAScript 2015 (ES6) y hace exactamente lo que necesita.

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

El método Object.assign () se usa para copiar los valores de todas las propiedades enumerables de uno o más objetos de origen a un objeto de destino.

Lee mas...

los polyfill para admitir navegadores antiguos:

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



Código:

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

Prueba:

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



Esto es lo que estoy usando:

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

81



Copia profunda por rendimiento: Clasificado de mejor a peor

  • Reasignación "=" (matrices de cadenas, matrices numéricas - solamente)
  • Slice (matrices de cadenas, matrices de números, solo)
  • Concatenación (matrices de cadenas, matrices de números, solo)
  • Función personalizada: copia for-loop o recursiva
  • jQuery's $ .extend
  • JSON.parse (matrices de cadenas, matrices numéricas, matrices de objetos, solo)
  • Underscore.js's _.clone (matrices de cadenas, matrices de números, solo)
  • Lo-Dash's _.cloneDeep

Copia profunda una matriz de cadenas o números (un nivel - sin punteros de referencia):

Cuando una matriz contiene números y cadenas: funciones como .slice (), .concat (), .splice (), el operador de asignación "=" y la función de clonación de Underscore.js; hará una copia profunda de los elementos de la matriz.

Donde la reasignación tiene el rendimiento más rápido:

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

Y .slice () tiene mejor rendimiento que .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

Copia profunda una matriz de objetos (dos o más niveles - punteros de referencia):

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

Escriba una función personalizada (tiene un rendimiento más rápido que $ .extend () o 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);

Use funciones de utilidad de terceros:

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

Donde $ .extend de jQuery tiene un mejor rendimiento:


64



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

56