Translator's note: The JavaScript inheritance topic is one of the most difficult for beginners. With the addition of the new syntax with the class keyword, the understanding of inheritance has clearly not become simpler, although nothing radically new has appeared. This article does not address the nuances of prototype inheritance in JavaScript, so if the reader has any questions, I recommend reading the following articles: Fundamentals and errors about JavaScript and Understanding OOP in JavaScript [Part 1]
For all comments related to the translation, contact the PM.
JavaScript is a very powerful language. So powerful that it coexists in many different ways of designing and creating objects. Each method has its pros and cons, and I would like to help newcomers understand this. This is a continuation of my previous post,
Stop “classify” JavaScript . I received many questions and comments asking for examples, and for this very purpose I decided to write this article.
JavaScript uses prototype inheritance
This means that in JavaScript objects are inherited from other objects. Simple objects in JavaScript, created using {} curly braces, have only one prototype:
Object.prototype .
Object.prototype , in turn, is also an object, and all properties and methods of
Object.prototype are accessible to all objects.
Arrays created with [] square brackets have several prototypes, including
Object.prototype and
Array.prototype . This means that all the properties and methods
Object.prototype and
Array.prototype are available for all arrays. Properties and methods of the same name, such as
.valueOf and
.ToString , are called from the nearest prototype, in this case from
Array.prototype .
')
Prototype definitions and object creation
Method 1: Template Constructor
JavaScript has a special type of function called constructors, which act in the same way as constructors in other languages. Constructor functions are called only using the
new keyword and associate the object being created with the context of the constructor function through the
this keyword . A typical constructor might 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"); };
Using this constructor looks the same as creating an object in other languages:
var sparkie = new Dog("Sparkie", "Border Collie"); sparkie.name;
bark and
print methods of the prototype, which are applied to all objects created using the
Dog constructor. The
name and
breed properties are initialized in the constructor. This is a common practice, when all methods are defined in the prototype, and properties are initialized by the designer.
Method 2: Class Definition in ES2015 (ES6)
The
class keyword was reserved in JavaScript from the very beginning and now it's finally time to use it. Class definitions in JavaScript are similar to 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"); } }
Many people find this syntax convenient, because it combines the constructor and the declaration of static and prototype methods in one block. The use is exactly the same as in the previous method.
var sparkie = new Dog("Sparkie", "Border Collie");
Method 3: Explicit prototype declaration, Object.create, factory method
This method shows that in fact the new syntax with the
class keyword uses prototype inheritance. This method also allows you to create a new object without using the
new operator.
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 proto = Object.assign(Animal.create("dog"), Dog.prototype); var dog = Object.create(proto); 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); } } };
This syntax is convenient because the prototype is declared explicitly. It is clear what is defined in the prototype, and what is defined in the object itself. The
Object.create method
is convenient because it allows you to create an object from the specified prototype. Verification with
.isPrototypeOf still works in both cases. The use is varied, but not excessive:
var sparkie = Dog.create("Sparkie", "Border Collie"); sparkie.name;
Method 4: Object.create, top-level factory, deferred prototype
This method is a small change in method 3, where the class itself is a factory, in contrast to the case when the class is an object with a factory method. It looks like an example constructor (method 1), but uses the factory method and
Object.create .
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 proto = Object.assign(Animal("dog"), Dog.prototype); var dog = Object.create(proto); 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); } };
This method is interesting in that it is similar to the first method, but does not require the keyword
new and works with the
instanceOf operator. The use is the same as in the first method, but without using the
new keyword:
var sparkie = Dog("Sparkie", "Border Collie"); sparkie.name;
Comparison
Method 1 vs. Method 4
There are quite a few reasons to use Method 1 instead of Method 4. Method 1 requires either using the
new keyword or adding the following check in the constructor:
if(!(this instanceof Foo)){ return new Foo(a, b, c); }
In this case, it is easier to use
Object.create with a factory method. You also cannot use the
Function # call or
Function # apply functions with constructor functions, because they override the context of the
this keyword . Verification above may solve this problem, but if you need to work with an unknown number of arguments in advance, you must use the factory method.
Method 2 vs. Method 3
The same reasoning about constructors and the
new operator that were mentioned above applies in this case too. Instance checking is necessary if the new
class syntax is used without using the
new operator or using
Function # call or
Function # apply .
My opinion
The programmer should strive for clarity of his code. Method 3 syntax very clearly shows what is actually happening. It also makes it easy to use multiple inheritance and stack inheritance. Since the
new operator violates the principle of openness / closeness due to incompatibility with
apply or
call , it should be avoided. The
class keyword hides the prototype character of JavaScript inheritance behind the mask of the class system.
“Simple is better than tricky”, and using classes, because it is considered more “sophisticated” is simply an unnecessary, technical wash.
Using
Object.create is more expressive and clear than using the
new and
this binding. In addition, the prototype is stored in an object that can be outside the context of the factory itself, and thus can be more easily modified and expanded by
adding methods . Just like classes in ES6.
The class keyword may be the most disastrous feature in JavaScript. I have tremendous respect for the brilliant and very hard-working people who were involved in the process of writing the standard, but even brilliant people sometimes do the wrong things. - Eric Elliott
Adding something unnecessary and possibly harmful, contrary to the very nature of the language, is thoughtless and erroneous.
If you decide to use a
class , I sincerely hope that I will never have to work with your code. In my opinion, developers should avoid using constructors,
class and
new , and use methods that are more natural to the paradigm and architecture of the language.
Glossary
Object.assign (a, b) copies all the enumerable properties of object
b to object
a , and then returns object
aObject.create (proto) creates a new object from the specified
proto prototype.
Object.setPrototypeOf (obj, proto) changes the internal
[[Prototype]] property of
obj to
proto