Skip to content

Instantly share code, notes, and snippets.

@jmakeig
Last active March 25, 2017 18:40
Show Gist options
  • Save jmakeig/52337c191b8f4e176e56d796129cad25 to your computer and use it in GitHub Desktop.
Save jmakeig/52337c191b8f4e176e56d796129cad25 to your computer and use it in GitHub Desktop.

TL;DR: For custom types, it’s always better to extend an type’s prototype than to overwrite it.

function Custom() {  // constructor
  this.constructor.apply(this, arguments); // call the super-type’s constructor
}
Object.assign(
  Custom.prototype, 
  Object.create(Super.prototype, /* type descriptors for Custom.prototype */), 
  { /* properties and methods for Custom.prototype */ },
);

If you were to just assign Custom.prototype = Object.create(Super.prototype) (or Custom.prototype = new Super), instances of Custom will get their constructor property from Super.prototype because the default Me.protype.constructor was overwritten in the assignment. Thus, (new Custom).constructor === Super, not Custom, as you’d expect. If you must use this style of inheritance, make sure to add Custom.prototype.constructor = Custom to explicitly set the sub-type’s constructor.

There’s no perfect way of determining the type name of an object in JavaScript. instanceof determines if an object has a particular type on its prototype chain, but can’t tell you which type constructed an object instance. The new operator and Object.create() both set the constructor property pointing to the function (or class) that constructed an object. If a custom type doesn’t set its constructor property (on its prototype), the constructors is inherited from higher on the prototype chain, all the way up to Object.prototype.

'use strict';
function instanceType(obj) {
const typeOf = typeof obj;
switch (typeOf) {
case 'undefined':
case 'number': // ironically also NaN
case 'string':
case 'boolean':
case 'function':
case 'symbol':
return typeOf;
}
if (null === obj) {
return 'null'; // FIXME: Is this correct?
}
// Symbol.toStringTag
const stringified = Object.prototype.toString
.call(obj)
.match(/^\[object (.+)\]$/)[1]; // [object Object] // Object
if ('Object' !== stringified) {
return stringified;
}
if (obj.constructor && obj.constructor.name) {
return obj.constructor.name;
}
// Note: `Object.create(null)` will not have a `constructor` property
return stringified;
}
function top(stack) {
return stack.split('\n')[1].match(/^\s+at ([^\s/]+)/)[1].split('.')[0]; // all stack frames // just the `Constructor.method` // just the `Constructor`
}
// A. Overwrite prototype
function AnimalA() {}
function DogA() {}
DogA.prototype = /* ⬅︎ Don’t do this */ Object.create(AnimalA.prototype); // Will inherit from AnimalA.prototype
DogA.prototype.speak = function speak() {
return 'A: bark';
};
DogA.prototype.fetch = function fetch() {
Error.captureStackTrace(this);
return top(this.stack);
};
const dogA = new DogA();
// A1: Same as A, but sets [Symbol.toStringTag] property
function AnimalA1() {}
function DogA1() {}
DogA1.prototype = /* ⬅︎ Don’t do this */ Object.create(AnimalA1.prototype);
DogA1.prototype.speak = function speak() {
return 'A1: bark';
};
DogA1.prototype.fetch = function fetch() {
Error.captureStackTrace(this);
return top(this.stack);
};
DogA1.prototype[Symbol.toStringTag] = 'DogA1'; // This overrides the behavior of Object.prototype.toString.call(dog)
const dogA1 = new DogA1();
// B. Overwrite prototype, set subclass constructor
function AnimalB() {}
function DogB() {}
DogB.prototype = Object.create(AnimalB.prototype);
DogB.prototype.constructor = DogB;
DogB.prototype.speak = function speak() {
return 'B: bark';
};
DogB.prototype.fetch = function fetch() {
Error.captureStackTrace(this);
return top(this.stack);
};
const dogB = new DogB();
// C. Extend prototype
function AnimalC() {}
function DogC() {}
Object.assign(DogC.prototype, Object.create(AnimalC.prototype), {
speak() {
return 'C: bark';
},
fetch() {
Error.captureStackTrace(this);
return top(this.stack);
}
});
const dogC = new DogC();
// C1. Same as C, but instantiates instance with `Object.create()` instead of `new`
function AnimalC1() {}
function DogC1() {}
Object.assign(DogC1.prototype, Object.create(AnimalC1.prototype), {
speak() {
return 'C: bark';
},
fetch() {
Error.captureStackTrace(this);
return top(this.stack);
}
});
const dogC1 = Object.create(DogC1.prototype);
// D. Class
class AnimalD {}
class DogD extends AnimalD {
speak() {
return 'D: bark';
}
fetch() {
Error.captureStackTrace(this);
return top(this.stack);
}
}
const dogD = new DogD();
const instances = [dogA, dogA1, dogB, dogC, dogC1, dogD];
({
instanceType: instances.map(instanceType),
stackLabel: instances.map(dog => dog.fetch()),
optSc: instances.map(dog => Object.prototype.toString.call(dog))
});
/*
{
"instanceType": [
"AnimalA", // This is because `DogA.prototype` doesn’t set the `constructor` property so it’s inheritied from `AnimalA`
"DogA1",
"DogB",
"DogC",
"DogC1",
"DogD"
],
"stackLabel": [
"AnimalA",
"AnimalA1",
"DogB",
"DogC",
"DogC1",
"DogD"
],
"optSc": [
"[object Object]",
"[object DogA1]",
"[object Object]",
"[object Object]",
"[object Object]",
"[object Object]"
]
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment