Deep Extend and Deep Copy #162

Closed
jscheel opened this Issue Mar 25, 2011 · 26 comments

Projects

None yet
@jscheel
jscheel commented Mar 25, 2011

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?

@jashkenas
Owner

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.

@jashkenas jashkenas closed this Apr 15, 2011
@jscheel
jscheel commented Apr 16, 2011

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.

@jashkenas
Owner

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.

@jscheel
jscheel commented Apr 16, 2011

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

@jashkenas
Owner

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.

@jscheel
jscheel commented Apr 16, 2011

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

@lexer
lexer commented Apr 18, 2011

Jquery.extend has deep option.

@visualmotive

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

@fshost
fshost commented Oct 12, 2011

+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.

@yuchi
yuchi commented Oct 12, 2011

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

@kmalakoff

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

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

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;
  };
@michaelficarra
Collaborator

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.

@kmalakoff

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: kmalakoff/underscore-awesomer@0cf6008

@adamhooper

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

@diversario

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

@adamhooper

@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.

@diversario

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.

@bernharduw bernharduw referenced this issue in powmedia/backbone-deep-model Nov 19, 2012
Closed

Bugfix toJSON #28

@pygy
pygy commented Dec 7, 2012

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

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

@ghost
ghost commented Dec 12, 2012

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

@fabriziomoscon

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

@jimisaacs

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

@jimisaacs

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.

@akre54 akre54 referenced this issue Apr 16, 2014
Closed

Deep extend #1585

@omidkrad

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
@fov42550564
_.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)});
  };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment