Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deep Extend and Deep Copy #162

Closed
jscheel opened this issue Mar 25, 2011 · 28 comments
Closed

Deep Extend and Deep Copy #162

jscheel opened this issue Mar 25, 2011 · 28 comments

Comments

@jscheel
Copy link

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
Copy link
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.

@jscheel
Copy link
Author

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
Copy link
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
Copy link
Author

jscheel commented Apr 16, 2011

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

@jashkenas
Copy link
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
Copy link
Author

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
Copy link

lexer commented Apr 18, 2011

Jquery.extend has deep option.

@visualmotive
Copy link

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

@fshost
Copy link

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
Copy link
Contributor

yuchi commented Oct 12, 2011

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

@kmalakoff
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
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
Copy link
Contributor

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
Copy link

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
Copy link

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

@adamhooper
Copy link

@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
Copy link

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.

@pygy
Copy link

pygy commented Dec 7, 2012

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

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

@ghost
Copy link

ghost commented Dec 12, 2012

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

@fabriziomoscon
Copy link

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
Copy link

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

@jimisaacs
Copy link

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.

dgbeck referenced this issue in jashkenas/backbone Nov 10, 2013
@akre54 akre54 mentioned this issue Apr 16, 2014
@omidkrad
Copy link

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
Copy link

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

@luncht1me
Copy link

luncht1me commented Feb 6, 2017

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.

@seyfer
Copy link

seyfer commented Nov 19, 2018

Lodash has it - https://lodash.com/docs/4.17.5#cloneDeep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests