Underscore: Deep Extend e Deep Copy

Criado em 25 mar. 2011  ·  28Comentários  ·  Fonte: jashkenas/underscore

Solicitação de recurso:
Seria possível ter um parâmetro booleano em _.extend() e _.copy() para torná-los profundos ou ter métodos profundos separados?

enhancement wontfix

Comentários muito úteis

_.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 comentários

Receio que não haja uma semântica realmente boa para uma operação de cópia profunda em JavaScript. As implementações existentes têm vários bugs, como mutação de matrizes aninhadas e falha ao copiar objetos Date aninhados. Se você quiser propor esse recurso, terá que acompanhá-lo com uma implementação à prova de balas.

É verdade que cópias profundas são definitivamente confusas em javascript. Tem que haver algum ponto em que você diz que os benefícios superam os malefícios, certo? Ter pessoas constantemente escrevendo métodos de cópia profunda que não funcionam como esperado é pior do que usar um método de cópia profunda que foi cuidadosamente codificado, mas com limitações bem documentadas. Por exemplo, a solução desse cara é ótima: http://stackoverflow.com/questions/728360/copying-an-object-in-javascript/728694#728694

Ele cobre a maioria das bases e documenta o que não vai lidar corretamente. Ele assume que o objeto contém apenas estes tipos: Object, Array, Date, String, Number e Boolean, e ele assume que quaisquer objetos ou arrays conterão apenas os mesmos.

Sim - e mesmo a solução desse cara realmente não é boa o suficiente. Se não podemos implementá-lo corretamente, não devemos implementá-lo.

Mas é realmente melhor para os usuários lançarem sua própria implementação, provavelmente ainda mais quebrada?

Não - é melhor para os usuários não copiarem profundamente em JavaScript. Normalmente, você pode encontrar uma maneira de realizar o mesmo objetivo sem ter que ter uma função robusta de cópia profunda ... conhecendo a estrutura do objeto que deseja copiar com antecedência.

Ah, então você está sugerindo que o usuário hidrate uma nova instância do objeto com os valores necessários? Eu posso ver isso.

Jquery.extend tem opção profunda.

+1 para a opção profunda, independentemente da dificuldade de implementação.

+2 para isso - não é difícil de implementar, é uma questão de princípio (ou seja, não implementar uma solução menos que perfeita). No entanto, como mencionado acima, ele é usado na biblioteca jQuery e bastante útil em todos, exceto em alguns casos extremos. Seria bom tê-lo disponível em uma biblioteca leve como esta.

@kmalakoff escreveu uma implementação , você deve dar a ele algum feedback! :)

Você pode até mesclar meu _.cloneToDepth original com _clone e apenas adicionar um parâmetro de profundidade ....

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

Além disso, escrevi _.own e _.disown que introduzem uma convenção para propriedade (ou pares de reter/liberar ou clonar/destruir). Ele recua apenas um nível abaixo, mas suponho que possa haver uma opção adicionada para recursão total (eu gostaria de ver um 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;
  };

E se você quiser um clone extensível de uso geral:

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

Não estou convencido de que isso seja uma boa ideia até que seja fornecido um caso de uso em que uma cópia profunda seja realmente a melhor solução. Acho que vai ser difícil encontrar um.

Eu não estava completamente satisfeito com minha resposta ontem (não deveria escrever código depois da meia-noite)... Eu criei duas versões ( _.cloneToDepth basicamente apenas clona o container, mantendo referências aos objetos originais e _.deepClone que copia as instâncias):

  // 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 @michaelficarra aponta, os casos de uso podem não ser claros. Pessoalmente, eu uso:

1) _.own / _.disown quando quero compartilhar objetos que possuem ciclos de vida complexos e/ou modelos de propriedade (como contagem de referência para lidar com a limpeza corretamente)
2) Eu usei _.cloneToDepth / _.containerClone (raramente!) quando eu tinha opções complexas e aninhadas para uma função.
3) Eu nunca precisei de um _.deepClone , mas suponha que ele possa ser útil como um método _.clone propósito geral se você estiver escrevendo uma função genérica que suporte tipos de forma flexível (por exemplo, eu não me importo com o que você me passar, mas vou modificá-lo e não quero ter efeitos colaterais no original - embora as strings sejam um caso especial imutável).

Enviei o código e os testes aqui: https://github.com/kmalakoff/underscore-awesomer/commit/0cf6008f16ad6e6daf60caf456021693ef33fec5

Nota: aqueles presos procurando por um clone profundo de uma linha e incompleto para objetos simples com semântica previsível podem apenas JSON.parse(JSON.stringify(object)) .

@adamhooper Esse método tende a perder propriedades ou algo assim? Por que está incompleto?

@diversario Não copiará o protótipo do objeto e não copiará funções. Isso se aplica recursivamente - portanto, também não copiará os protótipos ou funções dos objetos aninhados corretamente.

Em particular: ele não copiará corretamente nenhuma Data em sua árvore de objetos. E se você quiser corrigi-lo para funcionar com Datas, bem, você está simplesmente abordando um pequeno sintoma de um problema muito maior.

Oh, certo. Eu o uso principalmente para quebrar a referência a coisas como objetos "modelo", então não encontrei nada assim. Mas eu vejo a necessidade de uma cópia profunda real.

Transformei o mixin deepExtend de Kurt Milam em um pacote npm.

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

@michaelficarra , explique como a cópia profunda não é uma boa solução para clonar uma estrutura de árvore genérica?

Para todos vocês que estão tentando usar a cópia profunda de uma linha do @adamhooper, estejam cientes de que isso não funciona para datas
JSON.parse(JSON.stringify(objeto))
na verdade, converte qualquer objeto Date em string

-1 para cópia profunda. Usar uma cadeia de protótipos para objetos de configuração sempre se adequará melhor à sua API do que às opções aninhadas.

Na verdade, há um lugar para cópia profunda, mas deve coincidir com a verificação de tipo, então é um caso de uso muito específico na minha opinião, e não de propósito geral. Mais adequado para bibliotecas de esquema JSON ou carregadores de configuração. Não é um cinto de ferramentas javascript.

Na maioria dos casos _.extend({}, obj1, { prop1: 1, prop2: 2 }) é o que eu realmente preciso fazer:

  • me dá um novo objeto
  • não modifica meu objeto de origem e
  • não é cópia 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)});
  };

Encontramos muito um caso de uso em nosso ambiente de trabalho que se beneficiaria da clonagem profunda.

Às vezes, você precisa fazer a migração do banco de dados, quando os recursos de um aplicativo Web complicado são atualizados e expandidos para um novo paradigma. Uma cópia profunda de nossos dados é muito útil ao realizar operações como gerar hashes dos dados quando a referência original é preferida a não ser manipulada. Eu não acho que um único documento em nosso enorme banco de dados seja 'plano'. Propriedades aninhadas existem em todos os lugares.

Além disso, às vezes você deseja enviar uma cópia de um objeto para um serviço diferente em seu aplicativo da Web, para ser operado antes de decidir o que fazer com ele. Ter as propriedades aninhadas por referência anula o propósito de ter um objeto clonado. O ponto INTEIRO, imo, de um objeto clonado é deixar a referência original intacta. Assim que você introduz referências para os níveis mais profundos, todo o propósito é discutível.

Tenho certeza de que há uma maneira decente de fazer isso, onde um objeto pode ser navegado até suas profundezas e depois reconstruído da camada mais profunda até a raiz.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

arypbatista picture arypbatista  ·  3Comentários

jdalton picture jdalton  ·  4Comentários

markvr picture markvr  ·  3Comentários

githublyp picture githublyp  ·  3Comentários

umarfarooq125 picture umarfarooq125  ·  8Comentários