2

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?

Alex
  • 980
  • 8
  • 20
  • `var o = {}; o.constructor === Object; //true only for base objects` this works for all built-in types. It _may_ fail for user defined Constructor types _if_ the definition doesn't properly update the "constructor" field of it's `.prototype` object. – Mike Edwards Oct 30 '13 at 16:02
  • There's always `obj.constructor.name === 'Object'`, although this is not necessarily reliable. – numbers1311407 Oct 30 '13 at 16:03
  • The .name field is non-standard across browsers. Stick to a direct comparison since you have a global reference to the Object constructor already. – Mike Edwards Oct 30 '13 at 16:04
  • Allow me to direct you to [this post](http://heyjavascript.com/4-creative-ways-to-clone-objects/). – Elliot Bonneville Oct 30 '13 at 16:06
  • @ElliotBonneville - I don't think any of the methods in that post address OP's problem. – Ted Hopp Oct 30 '13 at 16:20
  • @TedHopp I just looked through them and I'd mostly agree. None of them address the issue of special types like `RegExp`, but the JSON exploit is at least an interesting alternative to the custom function I wrote. Just for my own curiosity, I may try to benchmark it against mine to see which is more efficient. – Alex Oct 30 '13 at 16:23
  • Before embarking on this journey of extending beyond javascript primitives, please read http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object ...unless you draw the line somewhere, you're opening up Pandora's box. – zamnuts Oct 30 '13 at 17:29
  • In regards to cloning: http://stackoverflow.com/questions/122102/most-efficient-way-to-clone-an-object/ and http://stackoverflow.com/questions/4459928/how-to-deep-clone-in-javascript – zamnuts Oct 30 '13 at 17:32

2 Answers2

0

One way of detecting plain JS objects (created with {} or new Object) is to use the jQuery method jQuery.isPlainObject. However, the documentation says that "Host objects have a number of inconsistencies which are difficult to robustly feature detect cross-platform. As a result of this, $.isPlainObject() may evaluate inconsistently across browsers in certain instances." Whether this works reliably with node.js should be tested.

Edit: in response to your comment: you can use jQuery with node.js, see this question: Can I use jQuery with Node.js?

Otherwise it is also possible to just copy the jQuery implementation of the method to your project, as the MIT license (https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) seems to permit this. jQuery implementation:

isPlainObject: function( obj ) {
        // Not plain objects:
        // - Any object or value whose internal [[Class]] property is not "[object Object]"
        // - DOM nodes
        // - window
        if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
            return false;
        }

        // Support: Firefox <20
        // The try/catch suppresses exceptions thrown when attempting to access
        // the "constructor" property of certain host objects, ie. |window.location|
        // https://bugzilla.mozilla.org/show_bug.cgi?id=814622
        try {
            if ( obj.constructor &&
                    !core_hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) {
                return false;
            }
        } catch ( e ) {
            return false;
        }

        // If the function hasn't returned already, we're confident that
        // |obj| is a plain object, created by {} or constructed with new Object
        return true;
    }
Community
  • 1
  • 1
simon
  • 12,666
  • 26
  • 78
  • 113
  • That would be an available tool to try if I were running this in a browser context, but it's a node.js server, so I'm not using the jQuery library. – Alex Oct 30 '13 at 16:24
  • I suppose using jQuery would be a possibility. However, I still don't think it's a particularly good solution for two reasons: 1) It requires me to load at least a chunk of a library that I would be using one function from, and 2) it only addresses the issue of detecting non-generic objects, not copying them. – Alex Oct 30 '13 at 18:56
0

You can use this to detect if a object is a regular expression

Object.prototype.toString.call( regexpObject ) == "[object RegExp]"

This is the way mentioned in the specification for getting the class of object.

From ECMAScript 5, Section 8.6.2 Object Internal Properties and Methods:

The value of the [[Class]] internal property is defined by this specification for every kind of built-in object. The value of the [[Class]] internal property of a host object may be any String value except one of "Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", and "String". The value of a [[Class]] internal property is used internally to distinguish different kinds of objects. Note that this specification does not provide any means for a program to access that value except through Object.prototype.toString (see 15.2.4.2).

A RegExp is a class of object defined in the spec at Section 15.10 RegExp(RegularExpression)Objects:

A RegExp object contains a regular expression and the associated flags.

Then, you can copy a RegExp object using new RegExp()

var oldObject = /[a-z]+/;
var newObject = new RegExp(oldObject);
Vitim.us
  • 20,746
  • 15
  • 92
  • 109
  • That's fine for `RegExp` objects, but what about `Date` or `ArrayBuffer`, custom object type, etc.? That's what OP wants to handle. – Ted Hopp Oct 30 '13 at 19:06
  • I know that there is a toJSON prototype to custom objects, similar to toString but I don't know how it would be better than JSON.stringify and parse – Vitim.us Oct 31 '13 at 00:36