Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Object.assign and Object.create for inheritance

I typically implement inheritance along the following lines.

function Animal () { this.x = 0; this.y = 0;}

Animal.prototype.locate = function() { 
  console.log(this.x, this.y);
  return this;
};
Animal.prototype.move = function(x, y) {
  this.x = this.x + x;
  this.y = this.y + y; 
  return this;
}


function Duck () {
    Animal.call(this);
}

Duck.prototype = new Animal();
Duck.prototype.constructor = Duck;
Duck.prototype.speak = function () {
    console.log("quack");
    return this;
}

var daffy = new Duck();

daffy.move(6, 7).locate().speak();

I've read this post by Eric Elliott and if I understand correctly I can use Object.create and Object.assign instead? Is it really that simple?

var animal = {
   x : 0,
   y : 0,
   locate : function () { 
     console.log(this.x, this.y);
     return this;
   },
   move : function (x, y) { 
     this.x = this.x + x; 
     this.y = this.y + y;
     return this;
   }
}

var duck = function () {
   return Object.assign(Object.create(animal), {
     speak : function () { 
       console.log("quack");
       return this;
     }
   });
}

var daffy = duck();

daffy.move(6, 7).locate().speak();

As an aside, by convention constructor functions are capitalized, should object literals that act as constructors also be capitalized?

I realise there are many questions here discussing new versus Object.create, but they typically seem to relate to Duck.prototype = new Animal(); versus Duck.prototype = Object.create(Animal.prototype);

like image 258
user5325596 Avatar asked Nov 13 '15 12:11

user5325596


2 Answers

Yes, it is that simple. In your example with Object.create/Object.assign, you are using a factory function to create new instances of duck (similar to the way jQuery creates new instances if you select an element with var body = $('body')). An advantage of this code style is, that it doesn't force you to call a constructor of animal when you want to create a new duck instance (as opposed to ES2015 Classes).

Differences in initialization

Maybe one interesting tidbit that works slightly differently than if you were to use a constructor (or any other initialization function):

When you create a duck instace, all the properties of animal are in the [[Prototype]] slot of the duck instance.

var daffy = duck();
console.log(daffy); // Object { speak: function() }

So daffy does not have any own x and y properties yet. However, when you call the following, they will be added:

daffy.move(6, 7);
console.log(daffy); // Object { speak: function(), x: 6, y: 7 }

Why? In the function-body of animal.move, we have the following statement:

this.x = this.x + x; 

So when you call this with daffy.move, this refers to daffy. So it will try to assign this.x + x to this.x. Since this.x is not yet defined, the [[Prototype]] chain of daffy is traversed down to animal, where animal.x is defined.

Thus in the first call, the this.x on the right side of the assignment refers to animal.x, because daffy.x is not defined. The second time daffy.move(1,2) is called, this.x on the right side will be daffy.x.

Alternative Syntax

Alternatively, you could also use Object.setPrototypeOf instead of Object.create/Object.assign (OLOO Style):

var duck = function () {
   var duckObject = {
       speak : function () { 
           console.log("quack");
           return this;
       }
   };
   return Object.setPrototypeOf(duckObject, animal);
}

Naming Conventions

I'm not aware of any established conventions. Kyle Simpson uses uppercase letters in OLOO, Eric Elliot seems to use lowercase. Personally I would stick with lower-case, because the object literals that act as constructors are already fully fledged objects themselves (not just blueprint, like classes would be).

Singleton

If you only wanted a single instance (e.g. for a singleton), you could just call it directly:

var duck = Object.assign(Object.create(animal), {
    speak : function () { 
        console.log("quack");
        return this;
    }
});

duck.move(6, 7).locate().speak();
like image 82
nils Avatar answered Oct 23 '22 08:10

nils


I've read this post by Eric Elliott and if I understand correctly I can use Object.create and Object.assign instead? Is it really that simple?

Yes, create and assign is much more simple because they're primitives, and less magic is going on - everything you do is explicit.

However, Eric's mouse example is a bit confusing, as he leaves out one step, and mixes the inheritance of mouses from animals with instantiating mouses.

Rather let's try transcribing your duckling example again - let's start with doing it literally:

const animal = {
  constructor() {
    this.x = 0;
    this.y = 0;
    return this;
  },
  locate() { 
    console.log(this.x, this.y);
    return this;
  },
  move(x, y) {
    this.x += x;
    this.y += y; 
    return this;
  }
};
const duck = Object.assign(Object.create(animal), {
  constructor() {
    return animal.constructor.call(this);
  },
  speak() {
    console.log("quack");
    return this;
  }
});
/* alternatively: 
const duck = Object.setPrototypeOf({
  constructor() {
    return super.constructor(); // super doesn't work with `Object.assign`
  },
  speak() { … }
}, animal); */

let daffy = Object.create(duck).constructor();
daffy.move(6, 7).locate().speak();

You should see that what happens here is really no different from using constructors (or class syntax for that matter), we've just stored our prototypes directly in the variables and we're doing instantiation with an explicit call to create and constructor.

Now you can figure that our duck.constructor does nothing but calling its super method, so we can actually omit it completely and let inheritance do its work:

const duck = Object.assign(Object.create(animal), {
  speak() {
    console.log("quack");
    return this;
  }
});

The other thing that is often changed is the initialisation of instance properties. There is actually no reason to initialise them if we don't really need them, it's sufficient to put some default values on the prototype:

const animal = {
  x: 0,
  y: 0,
  locate() { 
    console.log(this.x, this.y);
  }
};
const duck = … Object.create(animal) …;

let daffy = Object.create(duck); // no constructor call any more!
daffy.x = 5; // instance initialisation by explicit assignment
daffy.locate();

The problem with this is that it only works for primitive values, and it gets repetitive. This is where factory functions get in:

function makeDuck(x, y) {
    return Object.assign(Object.create(duck), {x, y});
}
let daffy = makeDuck(5, 0);

To allow for easy inheritance, the initialisation is often not done in the factory but in a dedicated method so it can be called on "subclass" instances as well. You may call this method init, or you may call it constructor like I did above, it's basically the same.

As an aside, by convention constructor functions are capitalized, should object literals that act as constructors also be capitalized?

If you're not using any constructors, you may assign a new meaning to capitalized variable names, yes. It might however be confusing for everyone who's not accustomed to this. And btw, they're not "object literals that act as constructors", they're just prototype objects.

like image 33
Bergi Avatar answered Oct 23 '22 08:10

Bergi