Underscore: Ampliación profunda y copia profunda

Creado en 25 mar. 2011  ·  28Comentarios  ·  Fuente: jashkenas/underscore

Solicitud de función:
¿Sería posible tener un parámetro booleano en _.extend() y _.copy() para hacerlos profundos, o tener métodos profundos separados?

enhancement wontfix

Comentario más útil

_.deepClone = function(obj) {
      return (!obj || (typeof obj !== 'object'))?obj:
          (_.isString(obj))?String.prototype.slice.call(obj):
          (_.isDate(obj))?new Date(obj.valueOf()):
          (_.isFunction(obj.clone))?obj.clone():
          (_.isArray(obj)) ? _.map(obj, function(t){return _.deepClone(t)}):
          _.mapObject(obj, function(val, key) {return _.deepClone(val)});
  };

Todos 28 comentarios

Me temo que no hay una semántica realmente buena para una operación de copia profunda en JavaScript. Las implementaciones existentes tienen varios errores, como mutar matrices anidadas y no poder copiar objetos de fecha anidados. Si desea proponer esta función, deberá acompañarla con una implementación a prueba de balas.

Cierto, las copias profundas son definitivamente desordenadas en javascript. Sin embargo, tiene que haber algún punto en el que digas que los beneficios superan los perjuicios, ¿verdad? Hacer que la gente escriba constantemente métodos de copia profunda de mala calidad que no funcionan como se esperaba es peor que usar un método de copia profunda que se ha codificado cuidadosamente, pero que tiene limitaciones bien documentadas. Por ejemplo, la solución de este tipo es genial: http://stackoverflow.com/questions/728360/copying-an-object-in-javascript/728694#728694

Cubre la mayoría de las bases y documenta lo que no manejará correctamente. Supone que el objeto contiene solo estos tipos: Objeto, Matriz, Fecha, Cadena, Número y Booleano, y supone que cualquier objeto o matriz solo contendrá lo mismo.

Sí, e incluso la solución de ese tipo realmente no es lo suficientemente buena. Si no podemos implementarlo correctamente, no deberíamos implementarlo.

Pero, ¿es realmente mejor para los usuarios implementar su propia implementación, probablemente aún más rota?

No, es mejor que los usuarios no realicen copias profundas en JavaScript. Por lo general, puede encontrar una manera de lograr el mismo fin sin tener que tener una función robusta de copia profunda... conociendo la estructura del objeto que desea copiar de antemano.

Ah, entonces, ¿está sugiriendo que el usuario hidrate una nueva instancia del objeto con los valores necesarios? Puedo ver eso.

Jquery.extend tiene una opción profunda.

+1 para la opción profunda, independientemente de lo difícil que sea implementarla.

+2 por esto: no es difícil de implementar, es una cuestión de principios (es decir, no implementar una solución menos que perfecta). Sin embargo, como se mencionó anteriormente, se usa en la biblioteca jQuery y es bastante útil en todos los casos, excepto en algunos extremos. Sería bueno tenerlo disponible en una biblioteca ligera como esta.

@kmalakoff ha escrito una implementación , ¡deberías darle algunos comentarios! :)

Incluso podría fusionar mi _.cloneToDepth original con _clone y simplemente agregar un parámetro de profundidad...

  // Create a duplicate of a container of objects to any zero-indexed depth.
  _.cloneToDepth = _.clone = function(obj, depth) {
    if (typeof obj !== 'object') return obj;
    var clone = _.isArray(obj) ? obj.slice() : _.extend({}, obj);
    if (!_.isUndefined(depth) && (depth > 0)) {
      for (var key in clone) {
        clone[key] = _.clone(clone[key], depth-1);
      }
    }
    return clone;
  };

Además, escribí _.own y _.disown que introducen una convención para la propiedad (ya sea pares de retener/liberar o clonar/destruir). Solo recurre un nivel hacia abajo, pero supongo que podría agregarse una opción para la recursividad total (¡me gustaría ver un caso de uso!).

  _.own = function(obj, options) {
    if (!obj || (typeof(obj)!='object')) return obj;
    options || (options = {});
    if (_.isArray(obj)) {
      if (options.share_collection) { _.each(obj, function(value) { _.own(value, {prefer_clone: options.prefer_clone}); }); return obj; }
      else { var a_clone =  []; _.each(obj, function(value) { a_clone.push(_.own(value, {prefer_clone: options.prefer_clone})); }); return a_clone; }
    }
    else if (options.properties) {
      if (options.share_collection) { _.each(obj, function(value, key) { _.own(value, {prefer_clone: options.prefer_clone}); }); return obj; }
      else { var o_clone = {}; _.each(obj, function(value, key) { o_clone[key] = _.own(value, {prefer_clone: options.prefer_clone}); }); return o_clone; }
    }
    else if (obj.retain) {
      if (options.prefer_clone && obj.clone) return obj.clone();
      else obj.retain();
    }
    else if (obj.clone) return obj.clone();
    return obj;
  };

  _.disown = function(obj, options) {
    if (!obj || (typeof(obj)!='object')) return obj;
    options || (options = {});
    if (_.isArray(obj)) {
      if (options.clear_values) { _.each(obj, function(value, index) { _.disown(value); obj[index]=null; }); return obj; }
      else {
        _.each(obj, function(value) { _.disown(value); });
        obj.length=0; return obj;
      }
    }
    else if (options.properties) {
      if (options.clear_values) { _.each(obj, function(value, key) { _.disown(value); obj[key]=null; }); return obj; }
      else {
        _.each(obj, function(value) { _.disown(value); });
        for(key in obj) { delete obj[key]; }
        return obj;
      }
    }
    else if (obj.release) obj.release();
    else if (obj.destroy) obj.destroy();
    return obj;
  };

Y si desea un clon extensible de propósito general:

  // Create a duplicate of all objects to any zero-indexed depth.
  _.deepClone = function(obj, depth) {
    if (typeof obj !== 'object') return obj;
    if (_.isString(obj)) return obj.splice();
    if (_.isDate(obj)) return new Date(obj.getTime());
    if (_.isFunction(obj.clone)) return obj.clone();
    var clone = _.isArray(obj) ? obj.slice() : _.extend({}, obj);
    if (!_.isUndefined(depth) && (depth > 0)) {
      for (var key in clone) {
        clone[key] = _.deepClone(clone[key], depth-1);
      }
    }
    return clone;
  };

No estoy convencido de que esta sea una buena idea hasta que se dé un caso de uso en el que una copia profunda sea en realidad la mejor solución. Creo que será difícil encontrar uno.

No estaba completamente satisfecho con mi respuesta de ayer (no debería escribir código después de la medianoche)... Se me ocurrieron dos versiones ( _.cloneToDepth básicamente solo clona el contenedor, retiene las referencias a los objetos originales y _.deepClone que copia las instancias):

  // Create a duplicate of a container of objects to any zero-indexed depth.
  _.cloneToDepth = _.containerClone = _.clone = function(obj, depth) {
    if (!obj || (typeof obj !== 'object')) return obj;  // by value
    var clone;
    if (_.isArray(obj)) clone = Array.prototype.slice.call(obj);
    else if (obj.constructor!=={}.constructor) return obj; // by reference
    else clone = _.extend({}, obj);
    if (!_.isUndefined(depth) && (depth > 0)) {
      for (var key in clone) {
        clone[key] = _.clone(clone[key], depth-1);
      }
    }
    return clone;
  };

  // Create a duplicate of all objects to any zero-indexed depth.
  _.deepClone = function(obj, depth) {
    if (!obj || (typeof obj !== 'object')) return obj;  // by value
    else if (_.isString(obj)) return String.prototype.slice.call(obj);
    else if (_.isDate(obj)) return new Date(obj.valueOf());
    else if (_.isFunction(obj.clone)) return obj.clone();
    var clone;
    if (_.isArray(obj)) clone = Array.prototype.slice.call(obj);
    else if (obj.constructor!=={}.constructor) return obj; // by reference
    else clone = _.extend({}, obj);
    if (!_.isUndefined(depth) && (depth > 0)) {
      for (var key in clone) {
        clone[key] = _.deepClone(clone[key], depth-1);
      }
    }
    return clone;
  };

Como señala @michaelficarra , los casos de uso pueden no estar claros. Personalmente, uso:

1) _.own / _.disown cuando quiero compartir objetos que tienen ciclos de vida complejos y/o modelos de propiedad (como el recuento de referencias para manejar la limpieza correctamente)
2) Utilicé _.cloneToDepth / _.containerClone (¡raramente!) cuando tenía opciones anidadas y complejas para una función.
3) Nunca he necesitado un _.deepClone , pero supongamos que podría ser útil como un método de propósito general _.clone si está escribiendo una función genérica que admita tipos de manera flexible (por ejemplo, no importa lo que me pases, pero voy a modificarlo y no quiero tener efectos secundarios en el original, aunque las cadenas son un caso especial inmutable).

Envié el código y las pruebas aquí: https://github.com/kmalakoff/underscore-awesomer/commit/0cf6008f16ad6e6daf60caf456021693ef33fec5

Nota: aquellos que buscan un clon profundo incompleto de una línea para objetos simples con semántica predecible pueden simplemente JSON.parse(JSON.stringify(object)) .

@adamhooper ¿Este método tiende a perder propiedades o algo así? ¿Por qué está incompleto?

@diversario No copiará el prototipo del objeto y no copiará funciones. Eso se aplica de forma recursiva, por lo que tampoco copiará correctamente los prototipos o funciones de los objetos anidados.

En particular: no copiará correctamente ninguna Fecha en su árbol de objetos. Y si desea arreglarlo para que funcione con Fechas, bueno, simplemente está abordando un pequeño síntoma de un problema mucho mayor.

Correcto. Lo uso principalmente para romper la referencia a cosas como objetos de "plantilla", por lo que no me he encontrado con nada de eso. Pero veo la necesidad de una copia profunda real.

He convertido la mezcla deepExtend de Kurt Milam en un paquete npm.

https://github.com/pygy/undescoreDeepExtend/

@michaelficarra , explique por qué la copia profunda no es una buena solución para clonar una estructura de árbol genérica.

Para todos los que intenten usar la copia profunda de una sola línea de @adamhooper , tengan en cuenta que no funciona para las fechas.
JSON.parse(JSON.stringify(objeto))
de hecho, convierte cualquier objeto Fecha en cadena

-1 para copia profunda. El uso de una cadena prototípica para los objetos de configuración siempre se adaptará mejor a su API que las opciones anidadas.

En realidad, hay un lugar para la copia profunda, pero debe coincidir con la verificación de tipos, por lo que, en mi opinión, es un caso de uso muy específico y no un propósito general. Más adecuado para bibliotecas de esquemas JSON o cargadores de configuración. No es un cinturón de herramientas de javascript.

En la mayoría de los casos _.extend({}, obj1, { prop1: 1, prop2: 2 }) es lo que realmente necesito hacer, que:

  • me da un nuevo objeto
  • no modifica mi objeto fuente y
  • no es una copia profunda
_.deepClone = function(obj) {
      return (!obj || (typeof obj !== 'object'))?obj:
          (_.isString(obj))?String.prototype.slice.call(obj):
          (_.isDate(obj))?new Date(obj.valueOf()):
          (_.isFunction(obj.clone))?obj.clone():
          (_.isArray(obj)) ? _.map(obj, function(t){return _.deepClone(t)}):
          _.mapObject(obj, function(val, key) {return _.deepClone(val)});
  };

Nos encontramos con un caso de uso mucho en nuestro entorno de trabajo que se beneficiaría de la clonación profunda.

A veces, debe realizar la migración de la base de datos, cuando las características de una aplicación web complicada se actualizan y expanden a un nuevo paradigma. Una copia profunda de nuestros datos es muy útil cuando se tienen que realizar operaciones como generar hashes a partir de los datos cuando se prefiere no manipular la referencia original. No creo que un solo documento en nuestra enorme base de datos sea 'plano'. Las propiedades anidadas existen en todas partes.

Además, a veces desea enviar una copia de un objeto a un servicio diferente dentro de su aplicación web, para operarlo antes de decidir qué hacer con él. Tener las propiedades anidadas por referencia anula el propósito de tener un objeto clonado. TODO el punto, en mi opinión, de un objeto clonado es dejar intacta la referencia original. Tan pronto como introduce referencias para los niveles más profundos, todo el propósito es discutible.

Estoy seguro de que hay una manera decente de hacerlo en la que se puede navegar por un objeto hasta sus profundidades y luego reconstruirlo desde la capa más profunda hasta la raíz.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

zackschuster picture zackschuster  ·  5Comentarios

acl0056 picture acl0056  ·  5Comentarios

ksullivan picture ksullivan  ·  9Comentarios

marcalj picture marcalj  ·  5Comentarios

Francefire picture Francefire  ·  5Comentarios