38

I need to implement small ODM like feature. I get plain javascript object from database, and I need to convert it into my model class instance. Let's assume model looks like:

    class Model{
       constructor(){
           this.a = '777';
           ---- whole bunch of other things ---
       }
       print(){
           console.log(this.a);
       }
   }

So I need convert var a = {b:999, c:666} to instance of model and being able to call a.print() after, and when a.print() executed 777 should be placed in console. How to do that?

silent_coder
  • 6,222
  • 14
  • 47
  • 91
  • 1
    How could `{b:999, c:666}` become a `Model` instance? Your `Model`s only have an `a` property, not `b` or `c` ones. Maybe that's why people don't understand your question. – Bergi Nov 18 '15 at 12:04
  • @Bergi it could be dozens of fields in objects and all of them should not be listed in constructor i think. – silent_coder Nov 18 '15 at 12:07
  • @silent_coder: Of course all your fields should be listed in the constructor? An instance wouldn't have those fields if they weren't created. – Bergi Nov 18 '15 at 12:08
  • @Bergi It's javascript mate. You could type `this.b = xxx` in any method and it will be perfectly valid. – silent_coder Nov 18 '15 at 12:11
  • This looks like a duplicate of [Casting plain objects to function instances (“classes”) in javascript](http://stackoverflow.com/q/11810028/1048572) to me (nothing is different in ES6). Please tell me whether that helps. – Bergi Nov 18 '15 at 12:13
  • I'm surprised this question has gotten this little attention in about 3 years. – DarkNeuron Jun 22 '18 at 10:12

8 Answers8

41

There have a simple method. Just assign the object to instance(this)

class Model
{
  constructor(obj){
    Object.assign(this, obj)
  }
  print(){
    console.log(this.a);
  }
}

let obj = {a: 'a', b: 'b', c: 'c'}
    
let m = new Model(obj)
console.log(m)
m.print()  // 'a'
junlin
  • 1,835
  • 2
  • 25
  • 38
8

If I understand the question correctly, you can export a factory function and make use of Object.assign to extend your base Model:

// Export the factory function for creating Model instances
export default const createModel = function createModel(a) {
  const model = new Model();
  return Object.assign(model, a);
};
// Define your base class
class Model {
  constructor() {
    this.a = 777;
  }
  print() {
    console.log(this.a, this.b, this.c)
  }
}

And call it like:

const myModel = createModel({ b: 999, c: 666 });
myModel.print();

Babel REPL Example

Or, of course, you could forego the factory and pass a in as a parameter (or rest parameters) to the constructor but it depends on your preferred coding style.

CodingIntrigue
  • 75,930
  • 30
  • 170
  • 176
  • 3
    I would recommend `class Model { static create(a) { … } … }`, and maybe a more descriptive name like `createFrom` or `fromJSON` – Bergi Nov 18 '15 at 12:33
  • 1
    What will be in the case if `a.a` intially contains different value. Let's say '-500' ? I think that some data will be losted. Or which was in plain object, or which was set in constructor. And how does this case will work if model with time will be extended with properties and read only getters with the same name as fields in plain object? – Ph0en1x Nov 18 '15 at 12:37
  • @Ph0en1x None of that is specified in the question. Either way, it would be trivial to transform the *source* object in `Object.assign` to exclude keys. – CodingIntrigue Nov 18 '15 at 12:44
  • 1
    @Ph0en1x: Just try it out? The last value will overwrite the previous. And extending instances later (after construction) is an antipattern. Read-only getters will of course cause issues, you have to adapt your `createModel` method to work with them explicitly. – Bergi Nov 18 '15 at 12:45
3

I would suggest rewriting your class to store all its properties in a single JS object this.props and accept this object in its constructor:

class Model {
  constructor (props = this.initProps()) {
    this.props = props
    // other stuff
  }
  initProps () {
    return {a: '777'}
  }
  print () {
    console.log(this.props.a)
  }
}

Then you'll be able to store this.props in your database as a plain JS object and then use it to easily recreate corresponding class instance:

new Model(propsFromDatabase)

Though, if you don't want to move all properties to this.props, you could use Object.assign to keep your object plain:

class Model {
  constructor (props = this.initProps()) {
    Object.assign(this, props)
    // other stuff
  }
  initProps () {
    return {a: '777'}
  }
  print () {
    console.log(this.a)
  }
}

But I would recommend using the former approach, because it'll keep you safe from name collisions.

Halfacht
  • 924
  • 1
  • 12
  • 22
Leonid Beschastny
  • 50,364
  • 10
  • 118
  • 122
  • This doesn't default each property separately, but the whole `props` object only. – Bergi Nov 18 '15 at 12:58
  • @Bergi yes, it's what you would expect from ODM - either read everything from DB, or create from a scratch. – Leonid Beschastny Nov 18 '15 at 13:00
  • Also, new `Model({b:999, c:666}).print()` // a === undefined – CodingIntrigue Nov 18 '15 at 13:00
  • @RGraham yes, because it's in `this.props.a`. But good point, anyway. Added alternative approach. – Leonid Beschastny Nov 18 '15 at 13:05
  • 2
    This approach have serious disadvantage. If you need to use your property later, like `(new Model(a)).b` you will need to implement custom getter for any property. That's a big amount of work. – Ph0en1x Nov 18 '15 at 13:05
  • @LeonidBeschastny No, I mean if you don't specify `a` in the object passed to `props` it never gets assigned - https://babeljs.io/repl/#?experimental=false&evaluate=true&loose=false&spec=false&code=class%20Model%20%7B%0A%20%20constructor%20(props%20%3D%20this.initProps())%20%7B%0A%20%20%20%20this.props%20%3D%20props%0A%20%20%20%20%2F%2F%20other%20stuff%0A%20%20%7D%0A%20%20initProps%20()%20%7B%0A%20%20%20%20return%20%7Ba%3A%20'777'%7D%0A%20%20%7D%0A%20%20print%20()%20%7B%0A%20%20%20%20console.log(this.props.a)%0A%20%20%7D%0A%7D%0Anew%20Model(%7Bb%3A999%2C%20c%3A666%7D).print()%3B – CodingIntrigue Nov 18 '15 at 13:06
  • @RGraham I see now. Well, in most cases I would expect such behavior. Why it should be present in loaded object if it wasn't present in the saved one? – Leonid Beschastny Nov 18 '15 at 13:10
  • @LeonidBeschastny True. Defaults maybe. Who knows. Clearly a poor question if there are so many different answers & unknowns. – CodingIntrigue Nov 18 '15 at 13:11
  • @Ph0en1x I agree with you that it'll require some additional work, but there is no need to implement separate getter and setter for every property, because you could either implement one abstract getter function (i.e. `this.get('a')`), or implement a simple getters factory. In any complex project I would rather stick to this approach and use gettesr\setters, than allow name collisions to occur. – Leonid Beschastny Nov 18 '15 at 13:19
  • @LeonidBeschastny Using unified getter not good because it's not refactorable and don't friend well with minifying. Sometimes migrating from plain objects to models is a part of refactoring. In that case it will be needed to rewrite a huge amount of code (was doing similar a little time ago). So using this pattern for my point of view is like reinventing your own ODM, model goes more complex, restrcitive, etc. Too much for javascript. – Ph0en1x Nov 18 '15 at 13:25
3

If you need to typecast more consistently, you can also create your own typecast function like generic function

function typecast(Class, obj) {
  let t = new Class()
  return Object.assign(t,obj)
}

// arbitrary class
class Person {
 constructor(name,age) {
   this.name = name
   this.age = age
 }
 print() {
   console.log(this.name,this.age)
 }
}

call it to typecast any object to any class instance like

let person = typecast(Person,{name:'Something',age:20})
person.print() // Something 20
Manish
  • 31
  • 1
1

How about this?:

var a = Object.create(Model.prototype, {
    b: {
        enumerable: true, // makes it visible for Object.keys()
        writable: true, // makes the property writable
        value: 999
    }, c: {
        value: 666
    }
});

You'd be basically creating a new instance of Model from it's prototype and assigning your new properties to it. You should be able to call print as well.

G_hi3
  • 588
  • 5
  • 22
  • I need to write automated code to convert pojso into model instances. I couldn't fill properties manually. – silent_coder Nov 18 '15 at 11:58
  • Yes, the properties can also be modified with `enumerable`, `writable`, `configurable` and `get` and `set`like this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty – G_hi3 Nov 20 '15 at 08:53
1

You could have a static Model.from or Model.parse method, that returns a new Model with those properties:

class Model {
  static defaults = { a: 777, b: 888, c: 999, d: 111, e: 222 };
  constructor() {
    const { defaults } = Model;
    for (const key in defaults) this[key] = defaults[key];
  }
  print() {
    console.log(this.a);
  }
  static from(data) {
    const { defaults } = Model;
    return Object.assign(
      new Model(),
      defaults,
      Object.fromEntries(
        Object.entries(data).filter(([key]) => key in defaults)
      )
    );
  }
}

const data = {
  a: "a", b: "b", c: "c", ajkls: "this wont be included"
};
const myModel = Model.from(data);
console.log("myModel =", myModel);
console.log("myModel instanceof Model:", myModel instanceof Model);
console.log("myModel.print():")
myModel.print();
Jacob
  • 1,577
  • 8
  • 24
0

Just like G_hi3's answer, but it "automates" the creation of the properties object

function Model() {
  this.a = '777';
}

Model.prototype.print = function(){
    console.log(this.a);
}

   // Customize this if you don't want the default settings on the properties object.
function makePropertiesObj(obj) {
    return Object.keys(obj).reduce(function(propertiesObj, currentKey){
        propertiesObj[currentKey] = {value: obj[currentKey]};
        return propertiesObj;
    }, {}); // The object passed in is the propertiesObj in the callback
}

var data = {a: '888'};

var modelInstance = Object.create(Model.prototype, makePropertiesObj(data));
// If you have some non trivial initialization, you would need to call the constructor. 
Model.call(modelInstance);
modelInstance.print(); // 888
Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217
  • This still lacks a call to the constructor, though. – Bergi Nov 18 '15 at 12:32
  • 1
    @Bergi I intentionally didn't call the constructor because it would override the data passed in `this.a = '777';`. – Ruan Mendes Nov 18 '15 at 12:35
  • You could (should) call it *before* you pass in the data, I meant. If you don't call it, you might lack initialisation of your instance. – Bergi Nov 18 '15 at 12:40
0

First declare a class in which you want to convert JSON:

class LoginResponse {
  constructor(obj) {
    Object.assign(this, obj);
  }
  access_token;
  token_type;
  expires_in;
}

Now convert the general javascript object into your desired class object:

const obj = {
  access_token: 'This is access token1',
  token_type: 'Bearer1',
  expires_in: 123,
};
  let desiredObject = new LoginResponse(obj);
  console.log(desiredObject);

Output will be:

 LOG  {"access_token": "This is access token1", "expires_in": 123, "token_type": "Bearer1"}