Underscore: Deep Extend and Deep Copy

Created on 25 Mar 2011  ·  28Comments  ·  Source: jashkenas/underscore

Feature Request:
Would it be possible to either have a boolean parameter on _.extend() and _.copy() to make them deep, or to have separate deep methods?

enhancement wontfix

Most helpful comment

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

All 28 comments

I'm afraid that there are no really good semantics for a deep copy operation in JavaScript. Existing implementations have various bugs, like mutating nested arrays, and failing to copy nested Date objects. If you'd like to propose this feature, you'll have to accompany it with a bulletproof implementation.

True, deep copies are definitely messy in javascript. There has to be some point where you say that the benefits outweigh the detriments though, right? Having people constantly write crummy deep-copy methods that don't work as expected is worse than using a deep copy method that has been carefully coded, but has well-documented limitations. For example, this guy's solution is great: http://stackoverflow.com/questions/728360/copying-an-object-in-javascript/728694#728694

He covers most of the bases, and documents what it won't handle correctly. He assumes the object contains only these types: Object, Array, Date, String, Number, and Boolean, and he assumes that any objects or arrays will only contain the same.

Yep -- and even that guy's solution really isn't good enough. If we can't implement it correctly, we shouldn't be implementing it.

But is it really better for users to roll their own, probably even more broken, implementation?

No -- it's better for users not to deep copy in JavaScript. You can usually find a way to accomplish the same end without having to have a robust deep copy function ... by knowing the structure of the object you want to copy in advance.

Ah, so you are suggesting the user hydrates a new instance of the object with the necessary values? I can see that.

Jquery.extend has deep option.

+1 for the deep option, regardless of how difficult it is to implement.

+2 for this - it is not difficult to implement, it is a matter of principle (i.e. not implementing a less-than-perfect solution). However, as mentioned above, it is used in the jQuery library and quite useful in all but a few edge cases. It would be nice to have it available in a light-weight library like this.

@kmalakoff has written an implementation, you should give him some feedback! :)

You could even merge my original _.cloneToDepth with _clone and just add a depth parameter....

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

Also, I wrote _.own and _.disown which introduce a convention for ownership (either pairs of retain/release or clone/destroy). It only recurses one level down, but I suppose there could be an option added for total recursion (I'd want to see a use case!).

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

And if you want a general purpose, extensible clone:

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

I'm unconvinced that this is a good idea until a use case is given where a deep copy is actually the best solution. I think it will be hard to find one.

I wasn't completely satisfied with my response yesterday (shouldn't write code after midnight)...I've come up with two versions (_.cloneToDepth basically just clones the container, retaining references to original objects and _.deepClone which copies the instances):

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

As @michaelficarra points out, the use cases may be unclear. Personally, I use:

1) _.own/_.disown when I want to share objects that have complex lifecycles and/or ownership models (like reference counting to handle clean up properly)
2) I used _.cloneToDepth/_.containerClone (rarely!) when I had complex, nested options for a function.
3) I've never needed a _.deepClone, but suppose it could be useful as a general purpose _.clone method if you are writing a generic function that flexibly supports types (eg. I don't care what you pass me, but I'm going to modify it and don't want to have side-effects on the original - although strings are a special immutable case).

I've submitted the code and tests here: https://github.com/kmalakoff/underscore-awesomer/commit/0cf6008f16ad6e6daf60caf456021693ef33fec5

Note: those stuck looking for a one-line, incomplete deep-clone for simple objects with predictable semantics can just JSON.parse(JSON.stringify(object)).

@adamhooper Does this method tend to lose properties or something? Why is it incomplete?

@diversario It won't copy the object prototype, and it won't copy functions. That applies recursively--so it won't copy nested objects' prototypes or functions properly either.

In particular: it won't properly copy any Date in your object tree. And if you want to fix it to work with Dates, well, you're simply addressing a small symptom of a much larger problem.

Oh, right. I mostly use it to break reference to things like "template" objects, so I haven't ran into anything like that. But I see the need for real deep copy.

I've turned Kurt Milam's deepExtend mixin into a npm package.

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

@michaelficarra , please explain how is deep-copying not a good solution for cloning a generic tree structure?

For all of you trying to use @adamhooper one-liner deep copy be aware that it doesn't work for dates
JSON.parse(JSON.stringify(object))
in fact it converts any object Date to string

-1 for deep copy. Using a prototypal chain for config objects will always suit your api better than nested options.

Actually there is a place for deep copy, but it should coincide with type checking, so it is a very specific use case in my opinion, and not general purpose. Better suited for JSON schema libraries, or configuration loaders. Not a javascript tool belt.

In most cases _.extend({}, obj1, { prop1: 1, prop2: 2 }) is what I really need to do which:

  • gives me a new object
  • does not modify my source object and
  • is not deep copy
_.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)});
  };

We run into a use case a lot in our work environment that would benefit from deep cloning.

Sometimes, you have to do database migration, when features of a complicated web app are updated and expanded to a new paradigm. A Deep copy of our data is very helpful when having to do operations like generating hashes from the data when the original reference is preferred to not be manipulated. I don't think a single document in our massive database is 'flat'. Nested properties exist everywhere.

Also, sometimes you want to send a copy of an object to a different service within your web application, to be operated on before deciding what to do with it. Having the nested properties by reference defeats the purpose of having a cloned object. The WHOLE point, imo, of a cloned object is to leave the original reference in tact. As soon as you introduce references for the deeper levels, the whole purpose is moot.

I'm sure there's a decent way to go about this where an object can be navigated to it's depths, then reconstructed from the deepest layer up to the root.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ksullivan picture ksullivan  ·  9Comments

Francefire picture Francefire  ·  5Comments

githublyp picture githublyp  ·  3Comments

afranioce picture afranioce  ·  8Comments

arieljake picture arieljake  ·  4Comments