19

I have several javascript objects like this:

var object = {
    name: "object name",
    description: "object description",
    properties: [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
};

Now I wanted to change these objects to smarter objects (adding some methods etc).
At first I made a constructor function like this:

SmartObject = function( object ){

    this.name = object.name;

    this.description = object.description;

    this.properties = object.properties;

};

SmartObject.prototype.getName = function(){
    return this.name;
};

SmartObject.prototype.getDescription = function(){
    return this.description;
};

SmartObject.prototype.getProperies = function(){
    return this.properties;
};

And then I used this constructor to change my object to a SmartObject instance like this:

var smartObject = new SmartObject( object );

This seems the proper Object Oriented javascript code to do this, but this feels so overly complicated since all I actually want to do is add some methods and now I copy all properties from my object to my SmartObject in the constructor function.

In this example there are only 3 properties and some simple methods, but in my project there are several dozens of (nested) properties and much more complex methods.

So then I tried this:

object.__proto__ = SmartObject.prototype;

And this seems to result in exactly the same result and seems much easier (check this fiddle for this example).

Is this a proper and acceptable way to add the prototype to my object? Or is this breaking object oriented patterns and considered bad practice and should I continue doing like I did (using the constructor).

Or is there maybe another more acceptable way to add the methods to my existing object without pulling it through the constructor function?


Note. I tried to find such example on StackOverflow, but when I search I always end up in examples extending the prototype of existing javascript classes. If there is such question feel free to mark this as a duplicate and I will close my question again.

Wilt
  • 41,477
  • 12
  • 152
  • 203
  • 5
    don't change object prototypes after creating said object. It's going to kill the compiler's ability to optimize your code. See [MDNs nice warning](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/proto) – Sebastien Daniel Jun 17 '16 at 12:14
  • Thanks for the edit of your comment... The link is helpful. – Wilt Jun 17 '16 at 12:16
  • 3
    Have you considered using [the `Object.assign()` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)? Put your desired method in another object `methods = { getName : function() {}, etc:etc}` and then `Object.assign(object, methods)`. – nnnnnn Jun 17 '16 at 12:20
  • Here's the fiddle demo with two distinct objects: https://jsfiddle.net/BloodyKnuckles/uq0psLyx/1/ – bloodyKnuckles Jun 17 '16 at 12:24
  • @nnnnnn `assignObject = Object.assign( new SmartObject(), object );` doesn't work since the constructor tries to access the properties of an argument that I don't pass. I could reqork the constructor, but I wonder if that would be faster then using the constructor directly? – Wilt Jun 17 '16 at 12:26
  • Read my comment again. It doesn't use the constructor at all, or the prototype, I suggested a `methods` object to hold all your methods, and then `Object.assign()` to copy the methods into *any* object. – nnnnnn Jun 17 '16 at 12:28
  • @nnnnnn I read the comment before you updated, I will look again. Thanks! – Wilt Jun 17 '16 at 12:29
  • MDN covers [Object.create](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) Suggests `object1.prototype = Object.create(SmartObject.prototype)` ...interesting. – bloodyKnuckles Jun 17 '16 at 12:30
  • @nnnnnn I think that will consume more memory then using a constructor and a prototype. – Wilt Jun 17 '16 at 12:30
  • It will create one instance of each function, but multiple references to them. So yes, more memory, but not tonnes of memory - how many objects do your expect to have? The trade-off is extreme simplicity, which is what I thought you wanted... Or you could do what you had in your comment and set the constructor to have no arguments and no body. – nnnnnn Jun 17 '16 at 12:32
  • @nnnnnn Several 100 objects. I need a simpler alternative with the same result, meaning a prototyped object as a result. – Wilt Jun 17 '16 at 12:59
  • @nnnnnn Thanks for the `Object.assign()` suggestion together with `Object.create()` it combined to a nice solution! – Wilt Jun 17 '16 at 14:44
  • @bloodyKnuckles: No it doesn't. That's when `object1` is a constructor. – Bergi Jun 18 '16 at 19:58
  • possible duplicate of [Casting plain objects to function instances (“classes”) in javascript](http://stackoverflow.com/q/11810028/1048572)? – Bergi Jun 18 '16 at 20:12

4 Answers4

11

As I previously mentioned, changing an object's prototype is going to have a severe impact on your code's performance. (tbh, I've never taken the time to measure the impact). This MDN page explains.

However, if you're issue is about boilerplate, you could easily create a generic factory for your objects, such as:

function genericFactory(proto, source) {
    return Object.keys(source).reduce(function(target, key) {
        target[key] = source[key];
        return target;
    }, Object.create(proto));
}

And now you can use it by passing your SmartObject.prototype and object as arguments like this:

var smartObject = genericFactory(SmartObject.prototype, object);
Wilt
  • 41,477
  • 12
  • 152
  • 203
Sebastien Daniel
  • 4,649
  • 3
  • 19
  • 34
  • Still something strange, check [here in this fiddle](https://jsfiddle.net/wilt/uq0psLyx/2/), it outputs target 3 times in the factory when I add a console.log, should only happen once right? – Wilt Jun 17 '16 at 12:47
  • Reduce is an accumulator. It loops over an array's values (keys, in this case) - `Object.keys()` creates an array of an object's own property names. Your object is instantiated with 3 properties (keys), so reduce will loop 3 times to build your instance, but genericFactory will only return once. – Sebastien Daniel Jun 17 '16 at 12:51
  • Thanks for the explanation, but because of this it will be 3 times as slow right? This can maybe be optimized this so it only gets called the last time... – Wilt Jun 17 '16 at 12:54
  • 1
    I would recommend to avoid optimization until you - know - you need it (*i.e. avoid premature optimization*). Top of mind, a way to "optimize" the above would be to use a loop instead of the function callback overhead of `reduce()`. In any case, the perf. gains will be minimal unless you have thousands of objects to instantiate at once. – Sebastien Daniel Jun 17 '16 at 12:56
  • 1
    This is a good solution, but note that it only does a shallow copy of `source`, which may or may not matter. – nnnnnn Jun 17 '16 at 13:13
  • 1
    I don't know why you think this is the case but mutating __proto__ doesn't make things slow and haven't done so for a while. In fact when you assign something as __proto__ to an object engines like v8 optimize it immediately. – Benjamin Gruenbaum Jun 17 '16 at 15:59
  • @BenjaminGruenbaum as I've mentioned, I've never experienced the issue myself. I'm merely relating to the statements made on MDN, which is generally considered a good javascript reference – Sebastien Daniel Jun 17 '16 at 16:34
  • @BenjaminGruenbaum: Are you sure of that? [It certainly used to](http://stackoverflow.com/a/23809148/1048572). Assigning to `__proto__` right after the object creation might not be a problem, but assigning to an object that already was used repeatedly elsewhere seems to be. – Bergi Jun 18 '16 at 20:02
  • @Bergi if you're interested I go through the code that v8 runs when setting a prototype here: http://stackoverflow.com/questions/24987896/how-does-bluebirds-util-tofastproperties-function-make-an-objects-properties/24989927#24989927 - but generally check `OptimizeAsPrototype` – Benjamin Gruenbaum Jun 18 '16 at 23:13
  • @BenjaminGruenbaum: It doesn't appear to be a problem with the object that is used *asPrototype*, but rather with the object whose [[prototype]] is set. – Bergi Jun 19 '16 at 10:44
  • @Bergi oh, it will confuse the compiler initially for sure since it'd have to dump a bunch of related data - but the JIT will eventually catch on when it needs to. – Benjamin Gruenbaum Jun 19 '16 at 11:13
2

Combining the Object.create() from @SebastienDaniel his answer and @bloodyKnuckles his comment and the Object.assign() method suggested by @nnnnnn in his comment I managed with the following simple code to do exactly what I wanted:

var smartObject = Object.assign( Object.create( SmartObject.prototype ), object );

Check the updated fiddle here

Wilt
  • 41,477
  • 12
  • 152
  • 203
2

So then I tried this:

object.__proto__ = SmartObject.prototype;

...

Is this a proper and acceptable way to add the prototype to my object? Or is this breaking object oriented patterns and considered bad practice and should I continue doing like I did (using the constructor).

I recommend against it, because:

  1. Changing the prototype of an object after-the-fact ruins its performance in current JavaScript engines.

  2. It's unusual, and thus makes your code a bit alien to anyone else you might have maintain it.

  3. It's browser-specific; the __proto__ property is only defined in an Appendix to the JavaScript spec, and only for browsers (the spec does require browsers to implement it, though). The non-browser-specific way is Object.setPrototypeOf(object, SmartObject.prototype); but see #1 and #2.

You see concerned that it's redundant or repetitive, either at the coding level or at the memory level (I'm not sure). It isn't if you embrace your SmartObject from the beginning rather than creating object first and then adding the smarts later:

var SmartObject = function(name, description, properties) {
    this.name = name;
    this.description = description;
    this.properties = properties;
};

SmartObject.prototype.getName = function(){
    return this.name;
};

SmartObject.prototype.getDescription = function(){
    return this.description;
};

SmartObject.prototype.getProperies = function(){
    return this.properties;
};

var object = new SmartObject(
    "object name",
    "object description",
    [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
);

var anotherObject = new SmartObject(
    /*...*/
);

var yetAnotherObject = new SmartObject(
    /*...*/
);

or better yet, with ES2015 (which you can use today with a transpiler like Babel):

class SmartObject {
    constructor() {
        this.name = name;
        this.description = description;
        this.properties = properties;
    }

    getName() {
        return this.name;
    }

    getDescription() {
        return this.description;
    }

    getProperies(){
        return this.properties;
    }
}

let object = new SmartObject(
    "object name",
    "object description",
    [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
);


let anotherObject = new SmartObject(
    /*...*/
);

let yetAnotherObject = new SmartObject(
    /*...*/
);

You've said that you can't embrace SmartObject from the beginning because they're coming from a JSON source. In that case, you can incorporate your SmartObject into the JSON parsing using a reviver function:

var objects = JSON.parse(json, function(k, v) {
    if (typeof v === "object" && v.name && v.description && v.properties) {
        v = new SmartObject(v.name, v.description, v.properties);
    }
    return v;
});

While that does mean that the objects are first created and then recreated, creating objects is a very cheap operation; here's an example showing the time difference when parsing 20k objects with and without a reviver:

var json = '[';
for (var n = 0; n < 20000; ++n) {
  if (n > 0) {
    json += ',';
  }
  json += '{' +
    '   "name": "obj' + n + '",' +
    '   "description": "Object ' + n + '",' +
    '   "properties": [' +
    '       {' +
    '           "name": "first",' +
    '           "value": "' + Math.random() + '"' +
    '       },' +
    '       {' +
    '           "name": "second",' +
    '           "value": "' + Math.random() + '"' +
    '       }' +
    '    ]' +
    '}';
}
json += ']';

var SmartObject = function(name, description, properties) {
  this.name = name;
  this.description = description;
  this.properties = properties;
};

SmartObject.prototype.getName = function() {
  return this.name;
};

SmartObject.prototype.getDescription = function() {
  return this.description;
};

SmartObject.prototype.getProperies = function() {
  return this.properties;
};

console.time("parse without reviver");
console.log("count:", JSON.parse(json).length);
console.timeEnd("parse without reviver");

console.time("parse with reviver");
var objects = JSON.parse(json, function(k, v) {
  if (typeof v === "object" && v.name && v.description && v.properties) {
    v = new SmartObject(v.name, v.description, v.properties);
  }
  return v;
});
console.log("count:", objects.length);
console.timeEnd("parse with reviver");
console.log("Name of first:", objects[0].getName());

On my machine, it roughly doubles the time, but we're talking ~60ms to ~120ms, so in absolute terms it's nothing to worry about — and that's for 20k objects.


Alternately, you could mix in your methods rather than having a prototype:

// The methods to mix in
var smartObjectMethods = {
    getName() {
      return this.name;
    },
    getDescription() {
      return this.description;
    },
    getProperies() {
      return this.properties;
    }
};
// Remember their names to make it faster adding them later
var smartObjectMethodNames = Object.keys(smartObjectMethods);

// Once we have the options, we update them all:
objects.forEach(function(v) {
    smartObjectMethodNames.forEach(function(name) {
       v[name] = smartObjectMethods[name];
    });
});

ES2015 has Object.assign which you could use instead of smartObjectMethodNames and the inner forEach:

// Once we have the options, we update them all:
objects.forEach(function(v) {
    Object.assign(v, smartObjectMethods);
});

Either way, its slightly less memory-efficient because each of the objects ends up having its own getName, getDescription, and getProperties properties (the functions aren't duplicated, they're shared, but the properties to refer to them are duplicated). That's extremely unlikely to be a problem, though.

Here's an example with 20k objects again:

var json = '[';
for (var n = 0; n < 20000; ++n) {
  if (n > 0) {
    json += ',';
  }
  json += '{' +
    '   "name": "obj' + n + '",' +
    '   "description": "Object ' + n + '",' +
    '   "properties": [' +
    '       {' +
    '           "name": "first",' +
    '           "value": "' + Math.random() + '"' +
    '       },' +
    '       {' +
    '           "name": "second",' +
    '           "value": "' + Math.random() + '"' +
    '       }' +
    '    ]' +
    '}';
}
json += ']';

var smartObjectMethods = {
    getName() {
      return this.name;
    },
    getDescription() {
      return this.description;
    },
    getProperies() {
      return this.properties;
    }
};
var smartObjectMethodNames = Object.keys(smartObjectMethods);

console.time("without adding methods");
console.log("count:", JSON.parse(json).length);
console.timeEnd("without adding methods");

console.time("with adding methods");
var objects = JSON.parse(json);
objects.forEach(function(v) {
  smartObjectMethodNames.forEach(function(name) {
     v[name] = smartObjectMethods[name];
  });
});
console.log("count:", objects.length);
console.timeEnd("with adding methods");

if (Object.assign) { // browser has it
  console.time("with assign");
  var objects = JSON.parse(json);
  objects.forEach(function(v) {
    Object.assign(v, smartObjectMethods);
  });
  console.log("count:", objects.length);
  console.timeEnd("with assign");
}

console.log("Name of first:", objects[0].getName());
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks for your answer. I cannot *"embrace my `SmartObject` from the beginning"*, the question is just an example in real life the objects are loaded from a JSON source... – Wilt Jun 17 '16 at 17:53
  • @Wilt: Ah! In that case, you can use a JSON "reviver" function during parsing. Sadly I'm running out the door, but I'll update with an example tomorrow. – T.J. Crowder Jun 17 '16 at 18:04
0

Given your object is same as SmartObject in properties you might come up with things like this;

var obj = {
    name: "object name",
    description: "object description",
    properties: [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
}, 
SmartObject = function( object ){

    this.name = object.name;

    this.description = object.description;

    this.properties = object.properties;

};

SmartObject.prototype.getName = function(){
    return this.name;
};

SmartObject.prototype.getDescription = function(){
    return this.description;
};

SmartObject.prototype.getProperties = function(){
    return this.properties;
};
obj.constructor = SmartObject;
obj.__proto__ = obj.constructor.prototype;
console.log(obj.getName());

Ok the __proto__ property is now included in the ECMAScript standard and safe to use. However there is also the Object.setPrototypeOf() object method that can be utilized in the same fashion. So you might as well do like Object.setPrototypeOf(obj, obj.constructor.prototype) in the place of obj.__proto__ = obj.constructor.prototype;

Redu
  • 25,060
  • 6
  • 56
  • 76
  • My question was whether this solution is considered bad practice. Your answer is simply repeating the `obj.__proto__ = ...` solution from my question. – Wilt Jun 17 '16 at 14:48
  • From ES6 on `__proto__` is now within the standard. So should be safe. Yet you still have to assign the constructor properly as well for a proper relationship between your object and SmartObject constructor. (like `obj.constructor = SmartObject;`). Another way is to use `obj.setPrototypeOf(obj.constructor.prototype)` method but i believe now it's exactly doing what `obj.__proto__ = obj.constructor.prototype` does. – Redu Jun 17 '16 at 15:01
  • Thanks, it would be great if you would add what you wrote in your comment to the answer. – Wilt Jun 17 '16 at 15:05
  • Sorry my comment with `Object.setPrototypeOf()` is not correct since it is an Object method not an Object.prototype method so in your case it should be invoked like `Object.setPrototypeOf(obj, obj.constructor.prototype)` – Redu Jun 17 '16 at 15:15