📜 ⬆️ ⬇️

Understanding OOP in JavaScript [Part 1]

- Prototype inheritance is fine
JavaScript is an object-oriented (OO) language, rooted in the Self language, despite the fact that it looks like Java. This circumstance makes the language really powerful due to some nice features.

One of these features is the implementation of prototype inheritance. This simple concept is flexible and powerful. It allows you to make the inheritance and behavior of first-class entities, as well as functions are first-class objects in functional languages ​​(including JavaScript).

Fortunately, ECMAScript 5 has a lot of things that allowed you to put the language on the right path (some of them are covered in this article). There will also be talked about the disadvantages of JavaScript design and a small comparison with the classical prototype OO model (including its advantages and disadvantages) will be made.

The article assumes that you are already familiar with the basics of JavaScript, have an idea about functions (including the concepts of closure and first-class functions), primitive values, operators, etc.
')

1. Objects


An object in JavaScript is simply a collection of key-value pairs (and sometimes a bit of internal magic).

However, there is no class concept in JavaScript. For example, an object with {name: Linda, age: 21} properties is not an instance of any class or class Object . Both Object and Linda are instances of themselves. They are determined directly by their own behavior. There is no meta-data layer (ie classes) that would tell these objects how to behave.

You may ask, “Yes, how so?”, Especially if you come from the world of classical object-oriented languages ​​(such as Java or C #). “But if each object has its own behavior (instead of inheriting it from a general class), then if I have 100 objects, then they correspond to 100 different methods?” Isn't it dangerous? And how do I know that, for example, an object is really an Array ? ”

To answer all these questions, you need to forget about the classic OO approach and start everything from scratch. Believe me, it's worth it.

The prototype OO model brings several new dynamic and expressive ways to solve old problems. It also presents powerful models for extending and reusing code (and this is what interests people who are talking about object-oriented programming). However, this model gives less guarantees. For example, it cannot be assumed that an object x will always have the same set of properties.

1.1. What are objects?

It was previously mentioned that objects are simply pairs of unique keys with corresponding values ​​— such pairs are called properties. For example, you want to describe several aspects of your old friend (let's call him Misha, he's Mikhail ), such as age, name and gender:

An object in JavaScript is created using the Object.create function. This function from the parent and optional property set creates a new entity. For now, we will not worry about the parameters.

An empty object is an object without a parent, without properties. Let's look at the syntax for creating such an object in JavaScript:
var mikhail = Object.create(null) 


1.2. Creating properties

So, then we already have an object, but it does not have any properties yet - we have to correct this situation to describe our object Mikhail .

JavaScript properties are dynamic. This means that we can create or delete them at any time. Properties are unique in the sense that the property key inside an object matches exactly one value.

Create new properties through the Object.defineProperty function, which uses the object as arguments, the name of the property to create and a descriptor that describes the semantics of the property.
 Object.defineProperty(mikhail, 'name', { value: 'Mikhail' , writable: true , configurable: true , enumerable: true }) Object.defineProperty(mikhail, 'age', { value: 19 , writable: true , configurable: true , enumerable: true }) Object.defineProperty(mikhail, 'gender', { value: 'Male' , writable: true , configurable: true , enumerable: true }) 

The Object.defineProperty function creates a new property if a property with a given key did not previously exist (otherwise, the semantics and values ​​of the existing property will be updated).

By the way, you can also use Object.defineProperties when you need to add more than one property to an object:
 Object.defineProperties(mikhail, { name: { value: 'Mikhail' , writable: true , configurable: true , enumerable: true } , age: { value: 19 , writable: true , configurable: true , enumerable: true } , gender: { value: 'Male' , writable: true , configurable: true , enumerable: true }}) 

Obviously, both calls are similar, they are completely configurable, but not intended for the end user of the code. It is better to create a level of abstraction over them.

1.3. Descriptors

Small objects that contain semantics are called descriptors (we used them when calling Object.defineProperty ). Descriptors are one of two types - data descriptors and access descriptors.

Both types of descriptors contain flags that define how the property will be considered by the language. If the flag is not set, then its default value is false (unfortunately this is not always a good default value, which leads to an increase in the amount of descriptor descriptions).

Consider some flags:


Access descriptors define access to a specific value through getters and setters of functions. If not set, then defaults to undefined .

modify the value of the property.

1.4. Strive for conciseness

Fortunately, property descriptors are not the only way to work with properties in JavaScript — you can create them more succinctly.

JavaScript also understands property references using a so-called bracket entry. The basic rule is written as follows:
 <bracket-access> ::= <identifier> "[" <expression> "]" 

Here identifier is a variable that stores an object containing a property whose value we want to set, and expression is any valid JavaScript expression that defines the name of the property. There are no restrictions on what name a property can have; everything is allowed.

Thus, we can rewrite the previous example:
 mikhail['name'] = 'Mikhail' mikhail['age'] = 19 mikhail['gender'] = 'Male' 

Note: all property names are ultimately converted to a string, i.e. the object [1] , object [[1]] , object ['1'], and object [variable] records (where the variable value is 1) are equivalent.

There is another way to refer to a property called a point entry. It looks simpler and more concise than the bracket alternative. However, with this method, the property name must comply with the rules of a valid JavaScript identifier and cannot be represented by an expression (that is, variables cannot be used).

The general rule for point recording is:
 <dot-access> ::= <identifier> "." <identifier-name> 

Thus, the previous example has become even more beautiful:
 mikhail.name = 'Mikhail' mikhail.age = 19 mikhail.gender = 'Male' 

Both syntaxes perform the equivalent process of creating properties, setting semantic flags to true .

1.5. Access to properties

It is very easy to get the value stored in a given property - the syntax is very similar to creating a property with the only difference being that there is no assignment in it.
For example, if we want to know the age of Misha, then we will write:
 mikhail['age'] // => 19 

But if we try to get the value of a property that does not exist in our object, we will get undefined :
 mikhail['address'] // => undefined 


1.6. Removing properties

To delete a property from an object, a delete operator is provided in JavaSCript. For example, if you want to remove the gender property from our mikhail object:
 delete mikhail['gender'] // => true mikhail['gender'] // => undefined 

The delete operator will return true if the property has been deleted, and false otherwise. We will not delve into how this operator works. But if you're still interested, then you can read the most beautiful article on how delete works .

1.6. Getters and setters

Getters and setters are commonly used in classical object-oriented languages ​​to provide encapsulation. They are not really needed in JavaScript, but, we have a dynamic language, and I am against this functionality .

But, from any point of view, they allow you to provide proxies for read and write requests for properties. For example, we had separate slots for a name and a surname, but we want to have a convenient way to read and install them.

To begin with, we will create the name and surname of our friend, describing the corresponding properties:
 Object.defineProperty(mikhail, 'first_name', { value: 'Mikhail' , writable: true }) Object.defineProperty(mikhail, 'last_name', { value: 'Weiß' , writable: true }) 

Then we describe the general way of getting and setting two properties at once at once - let's call their union name :
 // () → String // Returns the full name of object. function get_full_name() { return this.first_name + ' ' + this.last_name } // (new_name:String) → undefined // Sets the name components of the object, from a full name. function set_full_name(new_name) { var names names = new_name.trim().split(/\s+/) this.first_name = names[⁣'0'] || '' this.last_name = names['1'] || '' } Object.defineProperty(mikhail, 'name', { get: get_full_name , set: set_full_name , configurable: true , enumerable: true }) 

Now, every time we try to find out the value of our friend’s name property, the get_full_name function will actually be called :
 mikhail.name // => 'Mikhail Weiß' mikhail.first_name // => 'Mikhail' mikhail.last_name // => 'Weiß' mikhail.last_name = 'White' mikhail.name // => 'Mikhail White' 

We can also set the name of the object by accessing the corresponding property, but in fact the call to set_full_name will do all the dirty work:
 mikhail.name = 'Michael White' mikhail.name // => 'Michael White' mikhail.first_name // => 'Michael' mikhail.last_name // => 'White' 

There are scenarios in which it is really convenient to do this, but it is worth remembering that such a mechanism works very slowly .
In addition, it should be noted that getters and setters are usually used in other languages ​​for encapsulation, and in ECMAScript 5 you still cannot do this — all properties of the object are public.

1.8. Enumeration of properties

Due to the fact that the properties are dynamic, JavaScript provides functionality for checking the set of object properties. There are two ways to list all the properties of an object, depending on what kind of properties you are interested in.

The first way is to call the Object.getOwnPropertyNames function, which will return an Array containing the names of all the properties set for this object — we will call these properties our own . For example, let's see what we know about Misha:
 Object.getOwnPropertyNames(mikhail) // => [ 'name', 'age', 'gender', 'first_name', 'last_name' ] 

The second way is to use Object.keys , which returns a list of its own properties, which are marked with the enumerable flag:
 Object.keys(mikhail) // => [ 'name', 'age', 'gender' ] 


1.9. Literals

A simple way to create an object is to use the literal syntax of JavaScript. A literal object defines a new object, the parent of which is an Object.prototype (we'll talk about parents a little later).

In any case, the syntax of literal objects allows you to define simple objects and initialize their properties. Let's rewrite the example of creating the Mikhail object:
 var mikhail = { first_name: 'Mikhail' , last_name: 'Weiß' , age: 19 , gender: 'Male' // () → String // Returns the full name of object. , get name() { return this.first_name + ' ' + this.last_name } // (new_name:String) → undefined // Sets the name components of the object, // from a full name. , set name(new_name) { var names names = new_name.trim().split(/\s+/) this.first_name = names['0'] || '' this.last_name = names['1'] || '' } } 


Invalid property names can be enclosed in quotes. Note that the entry for getter / setter is defined literally by anonymous functions. If you want to associate a previously declared function with getter / setter, then you must use the Object.defineProperty method.

Let's look at the general rules of literal syntax:
 <object-literal> ::= "{" <property-list> "}" ; <property-list> ::= <property> ["," <property>]* ; <property> ::= <data-property> | <getter-property> | <setter-property> ; <data-property> ::= <property-name> ":" <expression> ; <getter-property> ::= "get" <identifier> : <function-parameters> : <function-block> ; <setter-property> ::= "set" <identifier> : <function-parameters> : <function-block> ; <property-name> ::= <identifier> | <quoted-identifier> ; 

Literal objects can appear inside expressions in javascript. Due to some ambiguity, newbies are sometimes confused:
 // This is a block statement, with a label: { foo: 'bar' } // => 'bar' // This is a syntax error (labels can't be quoted): { "foo": 'bar' } // => SyntaxError: Invalid label // This is an object literal (note the parenthesis to force // parsing the contents as an expression): ({ "foo": 'bar' }) // => { foo: 'bar' } // Where the parser is already expecting expressions, // object literals don't need to be forced. Eg: var x = { foo: 'bar' } fn({foo: 'bar'}) return { foo: 'bar' } 1, { foo: 


2. Methods


Until now, the Mikhail object had only data storage slots (well, except for getter / setter for the name property). The description of actions that can be done with an object is done in JavaScript very simply. Simple - because in JavaScript there is no difference between manipulating things like Function , Number , Object . Everything is done in the same way (do not forget that functions in JavaScript are first-class entities).

We describe the action on this object by simply setting the function as the value of our property. For example, we want Misha to greet other people:
 // (person:String) → String // Greets a random person mikhail.greet = function(person) { return this.name + ': Why, hello there, ' + person + '.' } 

After setting the property value, we can use a similar method to set specific data associated with the object. Thus, access to properties will return a reference to the function stored in it, which we can call:
 mikhail.greet('you') // => 'Michael White: Why, hello there, you.' mikhail.greet('Kristin') // => 'Michael White: Why, hello there, Kristin.' 


2.1. Dynamic this

One thing to consider when describing the greet function is that this function should refer to the getter / setter of the name property, and for this it uses the magic variable this .

It stores a reference to the object to which the executing function belongs. This does not necessarily mean that this is always equal to the object in which the function is stored . No, JavaScript is not so selfish.

Functions are generic . Those. in javascript, the variable this defines a dynamic reference that is resolved when the function is executed.

This dynamic resolution process provides an incredibly powerful mechanism for dynamizing JavaScript's object-oriented nature and compensates for the lack of strict correspondence to specified structures (that is, classes). This means that you can apply a function to any object that meets the launch requirements, regardless of how the object is structured (as in CLOS ).

2.2. Permission this

There are four different ways to allow this in a function, depending on how the function is called: directly, as a method, explicitly applied as a constructor. We will look at the first three, and will return to the designers later.

For the following examples, you will take:
 // Returns the sum of the object's value with the given Number function add(other, yet_another) { return this.value + other + (yet_another || 0) } var one = { value: 1, add: add } var two = { value: 2, add: add } 


2.2.1 Call as a method

If the function is called as an object method, then this inside the function refers to the object itself. Those. when we explicitly specify which object performs the action, the object will be the value of this in our function.

This will happen when we call mikhail.greet () . This entry tells the javascript that we want to apply the greet action to the mikhail object.
 one.add(two.value) // this === one // => 3 two.add(3) // this === two // => 5 one['add'](two.value) // brackets are cool too // => 3 


2.2.2 Direct Call

When the function is called directly, this is resolved to the global engine object ( window in browser, global in Node.js)
 add(two.value) // this === global // => NaN // The global object still has no `value' property, let's fix that. value = 2 add(two.value) // this === global // => 4 


2.2.3. Explicit application

Finally, the function can be explicitly applied to any object, regardless of whether the object has a corresponding property or not. This functionality is achieved using call or apply methods.

The difference between the two methods lies in the parameters passed to the function and the execution time — apply works about 55 times slower than an immediate call, but call is usually not particularly worse. Everything depends on the current engine, so use Perf test to be sure - do not optimize the code ahead of time.

In any case, call expects an object as the first parameter of the function, followed by the usual arguments of the original function:
 add.call(two, 2, 2) // this === two // => 6 add.call(window, 4) // this === global // => 6 add.call(one, one.value) // this === one // => 2 

On the other hand, apply allows the second parameter to describe an array of parameters of the original function:
 add.apply(two, [2, 2]) // equivalent to two.add(2, 2) // => 6 add.apply(window, [ 4 ]) // equivalent to add(4) // => 6 add.apply(one, [one.value]) // equivalent to one.add(one.value) // => 2 

On a note. Note that the resolution of this in null or undefined depends on the semantics of the engine used. The result is usually the same as applying a function to a global object. But if the engine runs in strict mode , then this will be allowed as expected - exactly to the thing to which it is applied:
 window.value = 2 add.call(undefined, 1) // this === window // => 3 void function() { "use strict" add.call(undefined, 1) // this === undefined // => NaN // Since primitives can't hold properties. }() 


2.3. Method binding

Let us distract from the dynamic essence of functions in JavaScript, go along the path of creating functions, associating them with certain objects, so that this inside a function always points to this object, whether it is called as an object method or directly.

The function provides the functionality called bind : the object is taken and an additional parameter (very similar to the call call ) and a new function is returned, which will apply the parameters to the original function when calling:
 var one_add = add.bind(one) one_add(2) // this === one // => 3 two.one_adder = one_add two.one_adder(2) // this === one // => 3 one_add.call(two) // this === one // => 3 


3. Inheritance


So far, we have seen how objects can define their behavior and how we can use their actions on other objects, but, we still have not seen the normal way to reuse code and its extensibility.

This is where inheritance comes in handy. It will allow separating tasks in which objects define specialized behavior from creating common behavior for other objects.

The prototyping model goes further. Although it supports technologies such as “selective extensibility” and “behavior sharing”, we will not particularly study them. The sad thing: the specific prototype OO models implemented in JavaScript are somewhat limited. We can bypass these restrictions, but the overhead will be great.

3.1. Prototypes

Inheritance in JavaScript is done through cloning the behavior of an object and expanding it with specialized behavior. An object whose behavior is cloned is called a prototype .

A prototype is an ordinary object that shares its behavior with other objects — in this case, it acts as a parent.

The concept of cloning behavior does not mean that you will have two different copies of the same function or data. In fact, JavaScript implements inheritance through delegation, i.e. All properties are stored in the parent, and access to them is extended through the child.

As mentioned earlier, the parent (or [[Prototype]] ) of an object is determined by calling Object.create with the first argument referring to the parent object.

Let's go back to the example of Misha. We single out his name and the ability to greet people in a separate object that will share his behavior with Misha. Here is how our model will look like:

We implement it in javascript:
 var person = Object.create(null) // Here we are reusing the previous getter/setter functions Object.defineProperty(person, 'name', { get: get_full_name , set: set_full_name , configurable: true , enumerable: true }) // And adding the `greet' function person.greet = function (person) { return this.name + ': Why, hello there, ' + person + '.' } // Then we can share those behaviours with Mikhail // By creating a new object that has it's [[Prototype]] property // pointing to `person'. var mikhail = Object.create(person) mikhail.first_name = 'Mikhail' mikhail.last_name = 'Weiß' mikhail.age = 19 mikhail.gender = 'Male' // And we can test whether things are actually working. // First, `name' should be looked on `person' mikhail.name // => 'Mikhail Weiß' // Setting `name' should trigger the setter mikhail.name = 'Michael White' // Such that `first_name' and `last_name' now reflect the // previously name setting. mikhail.first_name // => 'Michael' mikhail.last_name // => 'White' // `greet' is also inherited from `person'. mikhail.greet('you') // => 'Michael White: Why, hello there, you.' // And just to be sure, we can check which properties actually // belong to `mikhail' Object.keys(mikhail) // => [ 'first_name', 'last_name', 'age', 'gender' ] 


3.2 But how does [⁣ [Prototype] ⁣] work?

As you saw in the last example, we did not explicitly define any of the properties defined in Person explicitly in Mikhail , but we were still able to access them. This is due to the fact that JavaScript implements the delegation of access to properties, i.e. property is searched through all parents of the object.

This chain of parents is defined by a hidden slot in each object, which is called [⁣ [Prototype]] . You cannot change it directly, there is only one way to set a value for it - when creating a new object.

When a property is requested from an object, the engine first tries to get the property from the target object. If the property is not found, then the immediate parent of the object is considered, then the parent of the parent, etc.

This means that we can change the behavior of the prototype in the middle of the program, then the behavior of all objects that were inherited from it will automatically change. For example, suppose we want to change the default greeting:
 // (person:String) → String // Greets the given person person.greet = function(person) { return this.name + ': Harro, ' + person + '.' } mikhail.greet('you') // => 'Michael White: Harro, you.' 


3.3. Property overload

So, prototyping (i.e., inheritance) is used so that you can share data with other objects. Moreover, this method works quickly and is economical with respect to memory, since we always have only one instance of the data used.

, ? , — .

, Person , Person . , :


, mikhail , kristin greet . greet , greet , Person :
 // Here we set up the greeting for a generic person // (person:String) → String // Greets the given person, formally person.greet = function(person) { return this.name + ': Hello, ' + (person || 'you') } // And a greeting for our protagonist, Mikhail // (person:String) → String // Greets the given person, like a bro mikhail.greet = function(person) { return this.name + ': \'sup, ' + (person || 'dude') } // And define our new protagonist, Kristin var kristin = Object.create(person) kristin.first_name = 'Kristin' kristin.last_name = 'Weiß' kristin.age = 19 kristin.gender = 'Female' // Alongside with her specific greeting manners // (person:String) → String // Greets the given person, sweetly kristin.greet = function(person) { return this.name + ': \'ello, ' + (person || 'sweetie') } // Finally, we test if everything works according to the expected mikhail.greet(kristin.first_name) // => 'Michael White: \'sup, Kristin' mikhail.greet() // => 'Michael White: \'sup, dude' kristin.greet(mikhail.first_name) // => 'Kristin Weiß: \'ello, Michael' // And just so we check how cool this [[Prototype]] thing is, // let's get Kristin back to the generic behaviour delete kristin.greet // => true kristin.greet(mikhail.first_name) // => 'Kristin Weiß: Hello, Michael' 


To be continued...

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


All Articles