📜 ⬆️ ⬇️

JavaScript Prototypes for C / C ++ / C # / Java Programmers

JavaScript is different from many other "object-oriented" programming languages ​​in that there are objects in it, but not classes. Instead of classes in JavaScript, there are prototype chains and some other tricks that take time to comprehend. Before professional programmers in other languages, when switching to JavaScript, there is the problem of quickly entering its object model.

This text is written to give a novice or episodic JavaScript developer an idea of ​​how to create objects in JavaScript, from simple “structural”, as in C, to more “object-oriented”, as in C ++ / C # / Java.

The article can be recommended both to beginners in programming, and backend programmers who write in JavaScript only sporadically.

Objects and classes in C and C ++


Objects are entities that possess

For ease of introduction into the subject matter, objects can be represented as instances of the corresponding “classes” that exist in memory only after the program has started.
')
Objects can be generated during the life of the program, change, disappear.

Compare the conditional code in C and C ++:


In these examples, we in C and C ++ describe the Person object. They did not “create an object”, but “described its fields and methods,” so that later it was possible to create such objects and use them.

Also look at the appropriate ways to create a single Person object in C and C ++:


These two programs do the same thing: create an object and allow you to use its associated functions setLastName , computeAge (behavior) to change or poll the state of an object (state). We can access the created object at any time through the pointer p (identity). If we create another Person *m = new Person object Person *m = new Person , then we can use the methods of the new object, accessing it through the pointer m. Pointers p and m will point to different objects, each with its own state, albeit with the same set of methods (behavior).

As we can see, even the related languages ​​C and C ++ offer slightly different ways of describing the Person object. In one case, we describe an object through a struct Person Data data structure and friendly functions somewhere nearby. In another case, we syntactically put both data and functions into the same class Person .

Why can people prefer C ++ and the “object-oriented approach”, since we can do about the same thing in the C language, “without classes”, and in C ++? There are some good answers that are relevant in the context of learning JavaScript, in which you can use both the C approach and the C ++ approach:

  1. Namespaces. In the C version, we defined the computeAge function. This function is in the global namespace: it is “visible” to the entire program. In another place now to create such a function will not work. But what if we made a new kind of objects, say, Pony, and want to make a similar method that calculates the age of the pony? We will need not only to create a new ponyComputeAge () method, but also to rename the old method in order to achieve uniformity: personComputeAge (). In general, we “clutter up” the name space, making the creation of new types of objects more and more difficult over time. If we put the computeAge () function in a class, as in C ++, we may have many similar functions in different classes. They will not interfere with each other.

  2. Information hiding. In the C version, whoever has a pointer p to the Person structure, he can change any field in the object. For example, you can say p-> yearOfBirth ++. So to do it — arbitrarily changing arbitrary fields of arbitrary objects — is considered bad practice. Indeed, it is often necessary not just to change the field, but to consistently change several fields of the object. And who can do this better and more correctly than a specialized procedure (method)? Therefore, it should be possible to prohibit changing fields directly, and letting them be changed only with the help of appropriate methods. On C, this is difficult to do, so they are rarely used. But in C ++, it’s easy to do. It is enough to declare any attributes of the private object, and then it will be possible to access them only from within the class methods:
     class Person { //       computeAge  setLastName: private: char *firstName; char *lastName; int yearOfBirth; //   ()  : public: void computeAge(int currentYear); void setLastName(char *newLastName); } 

  3. Creating an interface. In version C we have to remember for each object how to get an age for it. For one object we will call ponyComputeAge() , for the other personComputeAge() . In the C ++ version, we can simply remember that calculating the age of any object is done through computeAge() . That is, we introduce a single interface for calculating the age, and use it as an application to many objects. It's comfortable.


JavaScript Objects and Prototypes


JavaScript programmers also take advantage of object programming, but there are no “classes” as a syntactic way of describing objects.

Naive way


It would be possible in JavaScript to use the C approach when we describe an object through a data structure and a set of functions working on data:

 function createPerson(first, last, born) { var person = { firstName: first, lastName: last, yearOfBirth: born }; return person; } function computeAge(p, currentYear) { return currentYear - p.yearOfBirth; } function setLastName(p, newLastName) { p.lastName = newLastName; } // Create a new person and get their age: var p = createPerson("Anne", "Hathaway", 1982); console.log(p); console.log(computeAge(p, 2013)); 

Try copying all this code into the node program (having previously installed the Node.JS project) and see what it displays.

Crammed namespace


But this method has the same disadvantages of the variant on C, which was mentioned above. Let's try one more time, but only this time we “stick” the setLastName() and computeAge() methods “inside” the object. By this we will “unload” the global namespace, we will not litter it:

 function createPerson(first, last, born) { var computeAgeMethod = function(p, currentYear) { return currentYear - p.yearOfBirth; } var setLastNameMethod = function(p, newLastName) { p.lastName = newLastName; } var person = { firstName: first, lastName: last, yearOfBirth: born, computeAge: computeAgeMethod, setLastName: setLastNameMethod }; return person; } // Create a new person and get their age: var p = createPerson("Anne", "Hathaway", 1982); // Note the p.computeAge(p) syntax, instead of just computeAge(p). console.log(p.computeAge(p, 2013)); console.log(p["computeAge"](p, 2013)); 

Notice that we simply moved functions from outside to createPerson inwards. The body of the function has not changed. That is, each function still expects an argument p , with which it will work. The method of calling these methods has not changed much: yes, instead of calling the computeAge global function, you need to call the method of the p.computeAge object, but the function still expects p first argument.

This is quite redundant. Let's use the following trick: as in C ++, Java and other languages, JavaScript has a special variable this . If the function is called by itself ( f() ), then this variable points to a global object (in the browser it will be a window ). But if the function is called through a point, as a method of an object, ( pf() ), then it will be passed a pointer to this object p as this. Since we will still be forced to call methods through a call to the corresponding fields of the object ( p.computeAge ), this will already exist in the methods and set to the correct value of p . Rewrite the code using this knowledge. Also try copying it to the node .

 function createPerson(first, last, born) { var computeAgeMethod = function(currentYear) { return currentYear - this.yearOfBirth; } var setLastNameMethod = function(newLastName) { this.lastName = newLastName; } var person = { firstName: first, lastName: last, yearOfBirth: born, computeAge: computeAgeMethod, setLastName: setLastNameMethod }; return person; } // Create a new person and get their age: var p = createPerson("Anne", "Hathaway", 1982); console.log(p.computeAge(2013)); 

image

Prototypes


The resulting createPerson function has the following disadvantage: it does not work very fast and spends a lot of memory every time an object is created. Each time you call createPerson JavaScript constructs two new functions, and assigns them as values ​​to the “computeAge” and “setLastName” fields.

How to make it so as not to create these functions every time? How to make the object referenced by a person not have the setLastName and setLastName fields, but the person.computeAge() and person.setLastName() methods still work?

To solve just this problem in JavaScript there is a mechanism called “prototypes”, or rather “prototype chains”. The concept is simple: if an object does not have its own method or field, then the JavaScript engine tries to find this field in the prototype. And if the prototype does not have a field, then try to find the field in the prototype of the prototype. And so on. Try twisting the following code in Node.JS by copying it to the node :

 var obj1 = { "a": "aVar" }; var obj2 = { "b": "bVar" }; obj1 obj2 obj2.a obj2.b obj2.__proto__ = obj1; obj1 obj2 obj2.a obj2.b 

image

We see that if we indicate that the object obj2 is the prototype of the object obj1 , then obj2 “properties” of the object obj1 “appear”, such as the field “a” with the value “aVar”. At the same time, printing obj2 will not show the presence of the “a” attribute in the object.

Therefore, you can do the methods once, put them into the prototype, and createPerson transform so as to use this prototype:

 function createPerson(first, last, born) { var person = { firstName: first, lastName: last, yearOfBirth: born }; person.__proto__ = personPrototype; return person; } var personPrototype = { "computeAge": function(currentYear) { return currentYear - this.yearOfBirth; }, //     "setLastName": function(newLastName) { this.lastName = newLastName; } } // Create a new person and get their age: var p = createPerson("Anne", "Hathaway", 1982); console.log(p); console.log(p.computeAge(2013)); 

Try this code in node . Notice which simple object, without its own methods, is shown via console.log(p) . And that this simple object still has a computeAge method.

This method of specifying a prototype object has two drawbacks. The first is that the special attribute __proto__ very new, and may not be supported by browsers. The second drawback is that even if we no longer clutter up the namespace with the functions computeAge and setLastName we still bogged it down with the name personPrototype .

Fortunately, another JavaScript trick comes to the rescue, which is standard and compatible with all browsers.

If the function is called not just by the name f() , but through new f() (compare with C ++ or Java!), Then two things happen:

  1. A new empty object {} is created, and this in the body of the function starts to show on it.

    More details. By default, when calling a function f() accessible from inside function this points simply to the global context; that is, wherever the window shows in the browser, or global from Node.JS.
     var f = function() { console.log(this); }; f() //     ,    : console.log(window) 

    We know that if we call a function as a field of some object pf() , then this function will already show this object p on this object. But if the function is called via new f() , then a fresh empty object {} will be created, and this within the function will already be pointed to it. Try node:
     var f = function() { }; console.log({ "a": "this is an object", "f": f }.f()); console.log(new f()); 

  2. In addition, each function has a special attribute .prototype . The object on which the .prototype attribute is .prototype will automatically become the prototype of the newly created object from item 1.

    Try node :

     var fooProto = { "foo": "prototype!" }; var f = function() { }; (new f()).foo //  undefined f.prototype = fooProto; (new f()).foo //  "prototype!" 

With this knowledge, it is easy to understand how the createPerson code written above using the __proto__ supernova attribute is equivalent to this more traditional code:

 function createPerson(first, last, born) { this.firstName = first; this.lastName = last; this.yearOfBirth = born; } createPerson.prototype = { "computeAge": function(currentYear) { return currentYear - this.yearOfBirth; }, //     "setLastName": function(newLastName) { this.lastName = newLastName; } } // Create a new person and get their age: var p = new createPerson("Anne", "Hathaway", 1982); console.log(p); console.log(p.computeAge(2013)); 

Pay attention to the following aspects:

In principle, you can not change the entire object pointed to by createPerson.prototype , but simply set the required fields separately. This idiom can also be found in industrial JavaScript code:

 createPerson.prototype.computeAge = function(currentYear) { return currentYear - this.yearOfBirth; } createPerson.prototype.setLastName = function(newLastName) { this.lastName = newLastName; } 


We connect a piece of jQuery library


Please note that the body of the createPerson function instead of simple and clear

 function createPerson(first, last, born) { var person = { firstName: first, lastName: last, yearOfBirth: born }; return person; } 

turned into a pretty awful sequence of manipulations with this :

 function createPerson(first, last, born) { this.firstName = first; this.lastName = last; this.yearOfBirth = born; } 

This manual initialization of object attributes ( firstName , lastName ) into argument values ​​( first , last ) is suitable for variants with a very small number of arguments. But for large and spreading configurations, the manual listing of attributes becomes inconvenient and unnecessarily verbose.

We can simplify the initialization of an object with multiple fields using the jQuery.extend function, which simply copies attributes from one object to another:

 function createPerson(first, last, born) { var person = { firstName: first, lastName: last, yearOfBirth: born }); $.extend(this, person); } 

In addition, we can not transfer a bunch of fields with the function arguments, but pass an object with the fields we need to the input of the function:

 function createPerson(person) { $.extend(this, person); } var p = new createPerson({ firstName: "Anne", lastName: "Hathaway", yearOfBirth: 1982 }); console.log(p); 

(Unfortunately, due to the need to use jQuery , this code is easiest to try in the browser, and not in the terminal with the node .)

This code already looks simple and compact. But why do we create a “new createPerson”? It's time to rename the method to a more appropriate name:

 function Person(person) { $.extend(this, person); } Person.prototype.computeAge = function(currentYear) { return currentYear - this.yearOfBirth; } Person.prototype.setLastName = function(newLastName) { this.lastName = newLastName; } var anne = new Person({ firstName: "Anne", lastName: "Wojcicki", yearOfBirth: 1973 }); var sergey = new Person({ firstName: "Sergey", lastName: "Brin", yearOfBirth: 1973 }); console.log(anne); console.log(sergey); 


Here’s how it looks in the Safari or Chrome console:

image

This form of writing is already very similar to how a class is written and works in C ++, so in JavaScript, the Person function is sometimes called a class. For example, you can say: “Person has a method computeAge”.

Link


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


All Articles