JavaScript prototypes for C / C ++ / C # / Java programmers

  • Tutorial
JavaScript differs from many other "object-oriented" programming languages ​​in that there are objects in it, but no classes. Instead of classes in JavaScript, there are prototype chains and some other tricks that take time to comprehend. When switching to JavaScript, professional programmers in other languages ​​encounter the problem of quickly entering its object model.

This text is written in order to give a beginner 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 to backend programmers who write JavaScript only occasionally.

Objects and classes in C and C ++


Objects are entities that possess
  • identity (identity, the ability to distinguish one object from another),
  • state (state, attributes, fields),
  • and behavior (behavior, methods, functions that can change state).

For ease of introduction to an object, objects can be represented as instances of the corresponding “classes” that exist in memory only after the program starts.

Objects can be generated during the life of the program, change, disappear.

Compare conditional code in C and C ++:

  • C

    struct Person {
        char *firstName;
        char *lastName;
        int yearOfBirth;
    }
    // Compute person's age in a given year.
    void computeAge(struct Person *person, int currentYear);
    // Set a new last name, possibly deallocating the old one.
    void setLastName(struct Person *person, char *newLastName);
    

  • C ++

    class Person {
       char *firstName;
       char *lastName;
       int yearOfBirth;
       void computeAge(int currentYear);
       void setLastName(char *newLastName);
    }
    

In these examples, we have described the Person object in C and C ++. Not “created an object”, but “described its fields and methods”, so that later you could create such objects and use them.

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

  • C

    struct Person *p = malloc(sizeof(*p));
    setLastName(p, "Poupkine");
    printf("Person's age is %d\n", computeAge(p, 2013));
    

  • C ++

    Person *p = new Person;
    p->setLastName("Poupkine");
    printf("Person's age is %d\n", p->computeAge(2013));
    

These two programs do the same thing: they create an object and allow you to use the functions associated with it setLastName, computeAge(behavior) to change or poll the state of the object (state). We can access the created object at any time through the p (identity) pointer. If we create another object Person *m = new Person, we can use the methods of the new object by accessing it through the m pointer. 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 to describe the Person object. In one case, we describe an object through a data structurestruct Personand friendly features somewhere nearby. In the other case, we syntactically put both the data and the functions in the same one class Person.

Why can people prefer C ++ and the "object-oriented approach", since we can do about the same thing in C, "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 variant, we defined the computeAge function. This function is located in the global namespace: it is “visible” to the entire program. In another place, creating such a function now fails. But what if we made a new kind of objects, say, Pony, and we want to make a similar method that calculates the age of a pony? We will not only need to create a new ponyComputeAge () method, but also rename the old method to achieve uniformity: personComputeAge (). In general, we “clutter up” the namespace, 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 can have many similar functions in different classes. They will not interfere with each other.

  2. Information hiding. In the C variant, who has a pointer p to the Person structure, he can change any field in the object. For example, you can say p-> yearOfBirth ++. So doing - arbitrarily changing arbitrary fields of arbitrary objects - is considered bad practice. After all, often you need 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 is worthwhile to be able to prohibit changing fields directly, and letting them be changed only using appropriate methods. In C, this is difficult, so they are rarely used. But in C ++, this is elementary. It is enough to declare some attributes of the private object, and then they can be accessed only from inside the class methods:
    class Person {
       // Эти данные могут менять только функции computeAge и setLastName:
       private:
           char *firstName;
           char *lastName;
           int yearOfBirth;
       // Эти функции (методы) доступны всем:
       public:
           void computeAge(int currentYear);
           void setLastName(char *newLastName);
    }
    

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


JavaScript Objects and Prototypes


JavaScript programmers also take advantage of object programming, but it lacks “classes” as a syntactic way to describe objects.

Naive way


We could use the C approach in JavaScript when we describe an object through a data structure and a set of functions that work 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 program node(pre-installing the Node.JS project ) and see what it outputs.

Sweeping namespaces


But this method has the same drawbacks of the C variant as mentioned above. Let's try again, but only this time we will “shove” the methods setLastName()and the computeAge()“inside” of the object. This way we “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));

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

This is pretty redundant. We use the following trick: as in C ++, Java and other languages, JavaScript has a special variable this. If the function is called on its own ( f()), then this variable points to the global object (in the browser it will be window). But if a function is called through a point, as a method of some object, (p.f()), then a pointer to this very object p is passed to it as this. Since we will still be forced to call methods through accessing the corresponding fields of the object ( p.computeAge), then the methods thiswill already exist and set to the correct value p. Rewrite the code using this knowledge. Also try copying it to 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 function createPersonhas the following drawback: it does not work very fast, and spends a lot of memory every time you create an object. Each time createPersonJavaScript is called, it constructs two new functions, and assigns them as values ​​to the fields "computeAge" and "setLastName".

How to make sure not to create these functions every time anew? How to make the object referenced by the person, fields computeAge, and setLastNameit was not, but the methods person.computeAge()and person.setLastName()continued to work?

To solve just this problem, JavaScript has 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 they try to find the field with the prototype prototype. Etc. Try to twist the following code in Node.JS by copying it to 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 you indicate that the prototype of the object obj2is an object obj1, then the obj2properties of the object “appear” obj1, such as the field “a” with the value “aVar”. However, printing obj2does not show the presence of the attribute “a” in the object.

Therefore, you can do the methods once, put them in the prototype, and createPersonconvert them to take advantage of 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 through console.log(p). And that the method still works for this simple object computeAge.

This method of specifying a prototype of an object has two drawbacks. The first is that the special attribute is __proto__very new, and may not be supported by browsers. The second drawback is that even after stopping cluttering up the namespace with functions computeAge, setLastNamewe still dirtied it with a name personPrototype.

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

If you call a function not just by 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 begins to point to it.

    More details. By default, when calling a function f(), an inside accessible function thissimply points to the global context; that is, to where it shows in the browser window, or globalat Node.JS.
    var f = function() { console.log(this); };
    f() // Выведет в браузере то же, что и строка ниже:
    console.log(window)
    

    We know that if you call a function as a field of any object p.f(), then this of this function will point p to this object already. But if you call the function through new f(), a fresh empty object will be created {}, and thisinside the function it will already point to it. Try in 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 that the attribute points to .prototypewill automatically become the prototype of the newly created object from point 1.

    Try it in node:

    var fooProto = { "foo": "prototype!" };
    var f = function() { };
    (new f()).foo   // Выведет undefined
    f.prototype = fooProto;
    (new f()).foo   // Выведет "prototype!"
    

With this knowledge, it’s easy to understand how the createPerson code written above, using the supernova attribute __proto__, 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:
  • we call new createPerson instead of createPerson;
  • we set the prototype object once from outside the function, so as not to construct functions every time we call createPerson;

In principle, you can not change the whole object that it points to createPerson.prototype, but simply separately set the necessary fields for it. 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;
}


Connecting a jQuery library slice


Please note that the function body is createPersoninstead 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 the attributes of the object ( firstName, lastName) into the values ​​of the arguments ( first, last) is suitable for variants with a very small number of arguments. But for large and branchy configurations, manual enumeration of attributes becomes inconvenient and too verbose.

We can simplify the initialization of an object with many 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 pass a bunch of fields to the arguments of the function, 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, because of the need to use jQuery, this code is easiest to try already in the browser, and not in terminal C. node)

This code already looks simple and compact. But why are we creating 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 what it looks like 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 ++, which is why in JavaScript a function is Personsometimes called a class. For example, you could say: "the Person class has a computeAge method."

Link



Also popular now: