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
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 ( // 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:
I've submitted the code and tests here: kmalakoff/underscore-awesomer@0cf6008 |
Note: those stuck looking for a one-line, incomplete deep-clone for simple objects with predictable semantics can just |
@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. |
@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 |
-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
|
_.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. |
Lodash has it - https://lodash.com/docs/4.17.5#cloneDeep |
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?The text was updated successfully, but these errors were encountered: