Wilt
Wilt

Reputation: 44356

Adding a constructor prototype to a javascript object

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.

Upvotes: 19

Views: 914

Answers (4)

T.J. Crowder
T.J. Crowder

Reputation: 1074295

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());

Upvotes: 2

Redu
Redu

Reputation: 26161

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;

Upvotes: 0

Wilt
Wilt

Reputation: 44356

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

Upvotes: 2

Sebastien Daniel
Sebastien Daniel

Reputation: 4778

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);

Upvotes: 11

Related Questions