Dec 7, 2017

Ember Object Model extend vs create

#programming #frontend #ember.js

I learned something new the other day in the course of development.

I had a piece of code that took an object and used Object.assign to merge it with another object and but I noticed that none of the properties were being copied over.

someObject.someProperty;
// => "I'm here!"
const merged = Object.assign({}, someObject, { foo: "bar" });
merged.foo;
//=> 'bar'
merged.someProperty;
//=> undefined

This was very confusing, so I set a debugger right before the Object.assign and discovered that someObject was an Ember.Object. My first thought was that maybe Ember objects don't behave the same way as regular POJOs with Object.assign, so I tested out the theory:

const someObject = Ember.Object.create({
    someProperty: "I'm here!",
});

const merged = Object.assign({}, someObject, { foo: "bar" });
merged.someProperty;
//=> "I'm here!"

Wait, a second. That seemed to work fine? What is going on.

So I dug a little deeper and I noticed that someObject was coming from a different library and it was actually defined like this:

const someObject = Ember.Object.extend({
    someProperty: "I'm here!",
}).create();

I was confused for a second, but a coworker explained to me that the object passed to Ember.Object.extend() adds properties to the prototype, not the instance and Object.assign() does not iterate over the entire prototypal chain when copying properties.

So I had to test that also:

function CustomObject() {}
CustomObject.prototype.someProperty = "I'm here!";
const someObject = new CustomObject();
const merged = Object.assign({}, a, { foo: "bar" });
merged.someProperty;
//=> undefined

It wasn't surprising to me that a property on the prototype isn't copied over by Object.assign, so this made sense to me, but it was surprising that Ember.Object.extend({foo: 'bar'}).create() and Ember.Object.extend().create({foo: 'bar}) were not equivalent.

Over the years of working in Ember.js, I've never really had to care about how the object model works, because it just works. For all intents and purposes, when I define a controller or a route or any custom object by extending the core classes, I never had to think about where all my functions and properties lived. In fact, I've mostly been able to think of the framework language as a completely separate DSL from vanilla Javascript and treat it as a black box. Pulling back the curtain on this abstraction has been a long time coming!

One notable effect of this discovery is that copying an Ember.Object (or any object that extends it), will not copy any default attributes of that object. For example, duplicating an object using Object.assign is a bad idea, because any default properties will be on the prototype and won't be carried over to the duplicate.

Post = DS.Model.extend({
    author: "me",
});

const post = Post.create({
    title: "New post",
    body: "Lorem ipsum",
});

const duplicate = Object.assign({}, post);

duplicate.author;
//=> undefined

// !!!!

If you like this post, please share it on Twitter and/or subscribe to my RSS feed. Or don't, that's also ok.