A project I've been working on recently gave me the need to create a function which can return a complete copy of a JSON object, recursively copying any internal objects. After a couple of failed attempts, I came up with this:
function copyObj(obj) {
var copy;
if (obj instanceof Array) {
copy = [];
for (var i in obj) {
copy.push(copyObj(obj[i]));
}
}
else if (obj instanceof Object) {
copy = {};
for (var prop in obj) {
copy[prop] = copyObj(obj[prop]);
}
}
else {
copy = obj;
}
return copy;
}
The function works perfectly for my purposes, which are to copy objects that will only ever contain primitive types, arrays, and nested generic JSON objects. For example, it will return a flawless copy of this: { prop1:0, prop2:'test', prop3:[1, 2, 3], prop4:{ subprop1:['a', 'b', 'c'], subprop2:false } }
.
There's one thing about this function that's nagging at me, though - its inability to handle any other types of objects (e.g. the RegExp
object). I'd like to improve on it by adding the capability to handle them, but at the same time I'd really rather not just have a huge wall of else if (obj instanceof [insert object type here])
. As such, my question is this: Is there a simple way in JavaScript of differentiating between a generic object (i.e. one declared as var obj = { }
) and one with a proper prototype/constructor? And if so, is there also a simple generalized way of copying such objects? My expectation for the second part of the question is no, and that I'd still need special handling to call constructors, but I'd still like to know with certainty either way.
P.S. In case anyone was curious about the context, the project requires me to manipulate a large list of items on a server, but in different ways for different connected clients. The easiest way I could think of to handle that was to create one master list and then have the server clone a fresh copy to manipulate without altering the master list for every new client that connects, hence the need for copyObj()
.
Edit: I probably should have mentioned this in the original question - this is running with node.js as a server, not in a browser, so browser cross-compatibility isn't an issue.
Edit 2: In the interest of not cluttering the comments too much, I'll mention it here: I tried a quick benchmarking of my copyObj()
function against the JSON.parse(JSON.stringify(obj))
exploit using the example object above. My version seems to run in about 75% of the time that the JSON method takes (1 million copies took ~3.2 seconds for mine and ~4.4 seconds for JSON). So that makes me feel better about having taken the time to write my own.
Edit 3: Working off of the list of object types in Vitum.us's answer, I threw together an updated version of my copyObj()
function. I haven't tested it extensively, and the performance is about 2x worse than the old version, but I think it should actually work for all built-in types (assuming that list was complete).
function copyObjNew(obj) {
var copy;
if (obj.constructor === Object) {
// Generic objects
copy = {};
for (var prop in obj) {
copy[prop] = copyObjNew(obj[prop]);
}
}
else if (obj.constructor === Array) {
// Arrays
copy = [];
for (var i in obj) {
copy.push(copyObjNew(obj[i]));
}
}
else if (obj.constructor === Number || obj.constructor === String || obj.constructor === Boolean) {
// Primitives
copy = obj;
}
else {
// Any other type of object
copy = new obj.constructor(obj);
}
return copy;
}
I'm using the .constructor
property now, as Mike suggested, and it seems to be doing the trick. I've tested it so far with RegExp
and Date
objects, and they both seem to copy correctly. Do any of you see anything blatantly (or subtly) incorrect about this?