;
JavaScript is a very powerful language. So powerful, in fact, that there are multiple different ways of designing prototypes and instantiating objects. There are tradeoffs when using each different method, and I aim to assist newcomers to the language by clearing up the mess. This is a follow-up to my previous post, Stop Classifying JavaScript. I received many comments and responses asking for code examples, so here they are.
This means that, in JavaScript, objects inherit from other objects. Basic objects in JavaScript, created with the { } curly braces, have only one prototype: Object.prototype. Object.prototype is, in itself, an object, and all members of Object.prototype are accessible from all objects.
Basic arrays, created with the [ ] square brackets, have multiple prototypes, including Object.prototype and Array.prototype. This means that all members of Object.prototype and all members of Array.prototype are accessible as members of arrays. Any members that overlap, like .valueOf and .toString, are overridden by the closest prototype, Array.prototype in this case.
Method 1: Constructor pattern
JavaScript has a special type of function called constructor functions, which act similarly to constructors in other languages. The are called mandatorily with the new keyword and bind the this keyword to the object being created by the constructor function. A typical constructor may look like this:
function Animal(type){
this.type = type;
}
Animal.isAnimal = function(obj, type){
if(!Animal.prototype.isPrototypeOf(obj)){
return false;
}
return type ? obj.type === type : true;
};
function Dog(name, breed){
Animal.call(this, "dog");
this.name = name;
this.breed = breed;
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
Dog.prototype.bark = function(){
console.log("ruff, ruff");
};
Dog.prototype.print = function(){
console.log("The dog " + this.name + " is a " + this.breed);
};
Dog.isDog = function(obj){
return Animal.isAnimal(obj, "dog");
};
The usage of this constructor looks like instantiation in other languages:
var sparkie = new Dog("Sparkie", "Border Collie");
sparkie.name; // "Sparkie"
sparkie.breed; // "Border Collie"
sparkie.bark(); // console: "ruff, ruff"
sparkie.print(); // console: "The dog Sparkie is a Border Collie"
Dog.isDog(sparkie); // true
bark and print are prototype methods which apply to all dogs. The name and breed properties are own properties that are set by the constructor. Usually, all methods are set in the prototype and all properties are set by the constructor.
Method 2: ES2015 (ES6) Class definitions
class has been a reserved keyword in JavaScript since the beginning, and now there is finally a use for it. Class definitions in JavaScript look a lot like they do in other languages.
class Animal {
constructor(type){
this.type = type;
}
static isAnimal(obj, type){
if(!Animal.prototype.isPrototypeOf(obj)){
return false;
}
return type ? obj.type === type : true;
}
}
class Dog extends Animal {
constructor(name, breed){
super("dog");
this.name = name;
this.breed = breed;
}
bark(){
console.log("ruff, ruff");
}
print(){
console.log("The dog " + this.name + " is a " + this.breed);
}
static isDog(obj){
return Animal.isAnimal(obj, "dog");
}
}
A lot of people like this syntax because it combines the constructor, static, and the prototype method declarations into one nice block. The usage is exactly the same as the Constructor method.
var sparkie = new Dog("Sparkie", "Border Collie");
Method 3: Explicit prototype declaration, Object.create, method factory
This method really displays the prototypal inheritance behind the workings of the class syntax, and allows for the omittance of the new keyword.
var Animal = {
create(type){
var animal = Object.create(Animal.prototype);
animal.type = type;
return animal;
},
isAnimal(obj, type){
if(!Animal.prototype.isPrototypeOf(obj)){
return false;
}
return type ? obj.type === type : true;
},
prototype: {}
};
var Dog = {
create(name, breed){
var dog = Object.create(Dog.prototype);
Object.assign(dog, Animal.create("dog"));
dog.name = name;
dog.breed = breed;
return dog;
},
isDog(obj){
return Animal.isAnimal(obj, "dog");
},
prototype: {
bark(){
console.log("ruff, ruff");
},
print(){
console.log("The dog " + this.name + " is a " + this.breed);
}
}
};
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
This syntax is nice because the prototypes are very explicitly defined. It is very clear exactly which are members of the prototype and which are members of the object. Object.create is nice because it allows the creation of an object with a specific prototype. The .isPrototypeOf check still works in both cases. The usage is different, but not incredible different:
var sparkie = Dog.create("Sparkie", "Border Collie");
sparkie.name; // "Sparkie"
sparkie.breed; // "Border Collie"
sparkie.bark(); // console: "ruff, ruff"
sparkie.print(); // console: "The dog Sparkie is a Border Collie"
Dog.isDog(sparkie); // true
Method 4: Object.create, top-level factory, prototype post-declaration
This method is a slight variation of Method 3, where the factory is the class, versus the class being an object with a factory method. It looks like the constructor example (Method 1), but uses factories and Object.create instead.
function Animal(type){
var animal = Object.create(Animal.prototype);
animal.type = type;
return animal;
}
Animal.isAnimal = function(obj, type){
if(!Animal.prototype.isPrototypeOf(obj)){
return false;
}
return type ? obj.type === type : true;
};
Animal.prototype = {};
function Dog(name, breed){
var dog = Object.create(Dog.prototype);
Object.assign(dog, Animal("dog"));
dog.name = name;
dog.breed = breed;
return dog;
}
Dog.isDog = function(obj){
return Animal.isAnimal(obj, "dog");
};
Dog.prototype = {
bark(){
console.log("ruff, ruff");
},
print(){
console.log("The dog " + this.name + " is a " + this.breed);
}
};
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
This method is nice because it’s usage looks like Method 1, but does not require the new keyword and works with instanceof. The usage is the same as the first method, but without the new:
var sparkie = Dog("Sparkie", "Border Collie");
sparkie.name; // "Sparkie"
sparkie.breed; // "Border Collie"
sparkie.bark(); // console: "ruff, ruff"
sparkie.print(); // console: "The dog Sparkie is a Border Collie"
Dog.isDog(sparkie); // true
Method 1 vs Method 4
There is very little reason to use Method 1 over Method 4. Method 1 requires either the use of new in your code or a check like this in the constructor:
if(!(this instanceof Foo)){
return new Foo(a, b, c);
}
At which point you may as well just use Object.create in a factory. You also can’t use Function#call or Function#apply on constructor functions, because they mess up this. The check above could remedy that issue as well, but if you want to use an unknown amount of arguments, you have to use a factory.
Method 2 vs Method 3
The same arguments about constructors and new that applied above apply to this as well. The instanceof check is necessary for using the class syntax without new or with Function#apply and Function#call.
A programmer should strive for code clarity. Method 3's explicit syntax very clearly shows exactly what is going on. It also allows for easy multiple inheritance and concatenative inheritance. Since using the new keyword violates the open-closed principle due to incompatibility with apply or call, it should be avoided. The class keyword hides the prototypal nature of JavaScript’s inheritance behind the guise of a classical system.
“Simple is better than clever,” and using classical syntax because it is considered to be more “sophisticated” is just unnecessary, technical overhead.
Object.create is more expressive and clearer than a bound this variable and new. Also, the prototype is stored in an object possibly outside the scope of the factory itself, so it can be modified and improved more easily and with method definition syntax, just like ES6 classes.
The class keyword will probably be the most harmful feature in JavaScript. I have enormous respect for the brilliant and hard-working people who have been involved in the standardization effort, but even brilliant people occasionally do the wrong thing. — Eric Elliott
Adding something which is unnecessary, possibly damaging, and counter to the very nature of the language is a bad move.
If you choose to use class, I hope I never have to try to maintain your code. In my opinion, developers should avoid the use of constructors, class, and new, and use methods of inheritance which more closely follow the language architecture.
Object.assign(a, b) copies all enumerable properties of object b onto object a and then returns object a
Object.create(proto) creates a new bare object with the prototype proto
Object.setPrototypeOf(obj, proto) sets the internal [[Prototype]] property of obj to proto