36

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

CodingIntrigue
  • 75,930
  • 30
  • 170
  • 176
user5325596
  • 2,310
  • 4
  • 25
  • 42
  • 2
    If you're using ES2015, you should be using `class` & `extend` – Amit Nov 13 '15 at 12:39
  • 19
    @Amit that's obviously not the question he asked. Whether using `class` and `extend` is a good idea is part of another discussion. – nils Nov 13 '15 at 12:46
  • 2
    @Amit -Why should I use `class` and `extend`? If I do that I end up having to use `new` don't I? What advantage does using `class` and `extend` have over `Object.assign(Object.create(...`? – user5325596 Nov 13 '15 at 12:46
  • 3
    I added the ES6 tag back into this since the main question is *can I use Object.create and **Object.assign**.* – CodingIntrigue Nov 13 '15 at 14:50
  • 1
    Following that pattern you would rather have `var duck = Object.assign(Object.create` and `var daffy = Object.create(duck)`. – a better oliver Nov 13 '15 at 17:38
  • 1
    @user5325596, classes are better, because you will get easy access to superclass, via `super` keyword. Also, the code `class A extends B` says more explicitly that you have an inheritance here, rather than `Object.assign(Object.create())` combo – just-boris Nov 14 '15 at 12:30
  • 1
    @Amit you effectively ignore the reasonable point that the mentioned Medium article goes to great lengths to explain. "should" is a strong word to use, JS is inherently pragmatic and the arguments for using factory functions over classes are perfectly reasonable whether one chooses to agree with them or otherwise. – Jay Edwards Jul 12 '19 at 04:31
  • @JayEdwards this is a 4 years old question & comment, where my point was not meant to be an answer (hence - a comment). But more importantly, there's a reason for my input and that is that *semantics* are critical to code quality and when the intention is to create a constructor, a `class` is the preferred construct to use. 4 years later I believe this is even more prominent. Personally, I would not approve a pull request not using a `class` in such a scenario today. – Amit Jul 12 '19 at 09:42
  • that's a pity. as you were. – Jay Edwards Jul 12 '19 at 23:45

3 Answers3

33

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();
nils
  • 25,734
  • 5
  • 70
  • 79
  • Thanks for the long answer. I suppose an advantage of`Object.assign(Object.create` over `setPrototypeOf` is that I can do more easiliy do "multiple inheritance" (for want of a better term) e.g `Object.assign(Object.create(animal), duckObj, someOtherObj, anotherObj)`? Are there any disadvantages to `Object.assign(Object.create`? – user5325596 Nov 13 '15 at 14:10
  • `Object.assign(Object.create(animal), duckObj, someOtherObj, anotherObj)` is more of a mixin mixed with prototypes :) The resulting object would contain the own properties of duckObj, someOtherObj and anotherObj and would only have animale in the [[Prototype]] slot. – nils Nov 13 '15 at 14:16
  • If you want to have multiple objects in the prototype chain, you would have to mix them together in `Object.create`: `Object.assign(Object.create({animal: animal, bird: bird}), duck);` or `Object.setPrototypeOf(duck, {animal: animal, bird: bird});` – nils Nov 13 '15 at 14:19
  • I'm not aware of any downsides of using `Object.assign/Object.create`, except for a small one that probably wont matter in most cases: Objects created using `Object.create` (as opposed to `new`) don't profit from `hiddenClass` support in browsers: https://medium.com/javascript-scene/common-misconceptions-about-inheritance-in-javascript-d5d9bab29b0a#68b2 – nils Nov 13 '15 at 14:22
  • This comment `If you want to have multiple objects in the prototype chain,` made me wonder how I would instantiate `daffy` if I wanted `animal.isPrototypeOf(daffy)` and `bird.isPrototypeOf(daffy)` to both return true? – user5325596 Nov 13 '15 at 15:05
  • You could only do that if `animal` is the `[[Prototype]]` of `bird`, and then in turn `bird` is the `[[Prototype]]` for `duck` (Through multiple levels: animal -> bird -> duck). You can't have multiple `[[Prototype]]` entries for one Object. – nils Nov 13 '15 at 16:39
  • yeah, I was just playing around a bit in the console. I can do `var animal = {/*props and methods*/}`; var bird = Object.assign(Object.create(animal), {hasWings : true}); var daffy = Object.assign(Object.create(bird), duckObj);` – user5325596 Nov 13 '15 at 17:03
  • Yes, exactly. If you have more questions in the same vein, it might be better to start a new question (so that future visitors can find the questions easily as well). Also, if you're happy with the answer, please mark it as the correct one. Thank you. – nils Nov 13 '15 at 17:53
13

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.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • this is the best answer. Thank you! – Sulliwane May 27 '16 at 07:15
  • "I've read this post by Eric Elliott" is where you stop and disregard everything. No person knows less about good design than this loudmouth simpleton. Everything can be done with 'new' and 'call' and 'object.create' -- the last of which only for inheritance. The above example is so overwritten as to be singular proof of the Kruger Dunning effect. Thankfully ES2015 has rationally added extend and class keywords. – Hal50000 Jun 18 '17 at 17:14
  • 4
    @Hal50000 Instead of calling anyone a "loudmouth simpleton", you might want to argue why `class`, `extends` and `new` are a "better design". I'd even argue that they are exactly the same design as the above example, only with more magic syntactic sugar. So what part is "overwritten"? – Bergi Jun 18 '17 at 20:59
0

Rather than "inheritance" you should think about what type of "instantiation pattern" you intend to use. They have different purposes for implementation.

The top example is prototypal and the bottom, functional-shared. Check out this link: JS Instantiation patterns

Also, this is non-es6 related.

  • This does not cover his example. And yes, `Object.assign` is ES6. – nils Nov 13 '15 at 12:53
  • Not sure why you say "Also, this is non-es6 related." `Object.assign`is part of the ES6 standard. – user5325596 Nov 13 '15 at 12:54
  • ES6 as new standards for what you're essentially using as `classes`. The code posted is specifically related to ES5. –  Nov 13 '15 at 12:58
  • @nils, if you check out the link in my initial response you'll see what I'm talking about. –  Nov 13 '15 at 13:00
  • No, it is not specifically related to ES5 (see the two comments above, `Object.assign` does not exist in ES5). And the `Object.create/Object.assign` combo is a perfectly valid pattern in ES6. And as stated above, your link does not pertain to the `Object.create/Object.assign` case, it only talks about `Object.create` in conjuncture with `x.prototype`. This is specifically what the OP is NOT asking about. – nils Nov 13 '15 at 13:01
  • Agreed! `Ob.assign` is ES6. However, in terms of instantiation patterns, which he is actually making a comparison of there are specific use cases for those. This is more ES6 specific in terms of "classes" (which are new in ES6 as far as language goes): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes –  Nov 13 '15 at 13:07
  • @Shinobi881sorry, looks like your link is broken now – Fedor Apr 07 '19 at 15:03
  • Minor point, yes `Object.assign` is es6 but easily polyfilled using a library. – Jay Edwards Jul 12 '19 at 05:02