📜 ⬆️ ⬇️

JavaScript - inheritance patterns

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; // "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 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; // "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, 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; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true 


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 a
Object.create (proto) creates a new object from the specified proto prototype.
Object.setPrototypeOf (obj, proto) changes the internal [[Prototype]] property of obj to proto

Source: https://habr.com/ru/post/263967/


All Articles