JavaScript: object exploration

Original author: Cristi Salcescu
  • Transfer
The material, the translation of which we are publishing today, is devoted to the study of objects - one of the key entities of JavaScript. It is designed mainly for novice developers who want to streamline their knowledge about objects. Objects in JavaScript are dynamic collections of properties that, in addition, contain a “hidden” property, which is a prototype of the object. Properties of objects are characterized by keys and values. Let's start the conversation about JS objects with keys.





Object Property Keys


The property key of the object is a unique string. To access the properties, you can use two methods: accessing them through a point and specifying the object key in square brackets. When accessing properties through a dot, the key must be a valid JavaScript identifier. Consider an example:

let obj = {
  message : "A message"
}
obj.message //"A message"
obj["message"] //"A message"

If you attempt to access a non-existent property of the error message object, it will not appear, but the value will be returned undefined:

obj.otherProperty //undefined

When used to access the properties of square brackets, you can use keys that are not valid JavaScript identifiers (for example, the key can be a string containing spaces). They can have any value that can be cast to the string:

let french = {};
french["merci beaucoup"] = "thank you very much";
french["merci beaucoup"]; //"thank you very much"

If non-string values ​​are used as keys, they are automatically converted to strings (using, if possible, the method toString()):

et obj = {};
//Number
obj[1] = "Number 1";
obj[1] === obj["1"]; //true//Objectlet number1 = {
  toString : function() { return"1"; }
}
obj[number1] === obj["1"]; //true

In this example, the object is used as the key number1. When attempting to access a property, it is converted to a string 1, and the result of this conversion is used as a key.

Object Property Values


Object properties can be primitive values, objects, or functions.

▍Object as an object property value


Objects can be placed in other objects. Consider an example :

let book = {
  title : "The Good Parts",
  author : {
    firstName : "Douglas",
    lastName : "Crockford"
  }
}
book.author.firstName; //"Douglas"

A similar approach can be used to create namespaces:

let app = {};
app.authorService = { getAuthors : function() {} };
app.bookService = { getBooks : function() {} };

▍Function as an object property value


When a function is used as a property value of an object, it usually becomes an object method. Inside the method, to refer to the current object, a keyword is used this.

This keyword, however, may have different meanings, depending on how the function was called. Here you can read about situations in which it thisloses context.

Dynamic nature of objects


Objects in JavaScript, by their nature, are dynamic entities. You can add properties to them at any time, the same goes for deleting properties:

let obj = {};
obj.message = "This is a message"; //добавление нового свойства
obj.otherMessage = "A new message"; // добавление нового свойстваdelete obj.otherMessage; //удаление свойства

Objects as associative arrays


Objects can be considered as associative arrays. The keys of an associative array are the names of the properties of an object. In order to gain access to the key, it is not necessary to view all the properties, that is, the operation of accessing the key of an associative array based on an object is performed in O (1) time.

Object Prototypes


Objects have a “hidden” link, __proto__pointing to a prototype object, from which the object inherits properties.

For example, an object created using an object literal has a link to Object.prototype:

var obj = {};
obj.__proto__ === Object.prototype; //true

Уст Empty objects


As we have just seen, the “empty” object, {}in fact, is not so empty, since it contains a link to Object.prototype. In order to create a truly empty object, you need to use the following construction:

Object.create(null)

This will create an object without a prototype. Such objects are usually used to create associative arrays.

ПротA prototype chain


Prototype objects can have their own prototypes. If you try to access the property of an object that is not in it, JavaScript will try to find this property in the prototype of this object, and if there is no desired property there, an attempt will be made to find it in the prototype prototype. This will continue until the desired property is found, or until the end of the prototype chain is reached.

Primitive Type Values ​​and Object Wrappers


JavaScript allows you to work with values ​​of primitive types as objects, in the sense that the language allows you to access their properties and methods.

(1.23).toFixed(1); //"1.2""text".toUpperCase(); //"TEXT"true.toString(); //"true"

In this case, of course, the values ​​of primitive types are not objects.

To provide access to the “properties” of values ​​of primitive types of JavaScript, if necessary, create wrapper objects, which, after they are no longer needed, are destroyed. The process of creating and destroying wrapper objects is optimized by the JS engine.

Object wrappers have numeric, string and boolean values. Objects of the respective types of functions by design are presented Number, Stringand Boolean.

Embedded Prototypes


Object-numbers inherit properties and methods from the prototype Number.prototype, which is the successor Object.prototype:

var no = 1;
no.__proto__ === Number.prototype; //trueno.__proto__.__proto__ === Object.prototype; //true

The prototype of the string objects is String.prototype. The prototype of logical value objects is Boolean.prototype. The prototype of arrays (which are also objects) is Array.prototype.

Functions in JavaScript are also objects that have a prototype Function.prototype. Functions have methods like bind(), apply()and call().

All objects, functions, and objects that represent values ​​of primitive types (except for values ​​of nulland undefined) inherit properties and methods from Object.prototype. This leads to the fact that, for example, they all have a method toString().

Extending embedded objects with polyfills


JavaScript allows you to easily extend built-in objects with new functions using so-called polyfills. Polyfill is a piece of code that implements features not supported by any browsers.

▍Using polyfills


For example, there is a polyfill for a method Object.assign(). It allows you to add a Objectnew function if it is not available in it.

The same applies to the polyfillArray.from() , which, if Arraythere is no method in the object from(), equips it with this method.

▍Polythills and prototypes


With the help of polyfills new methods can be added to the prototypes of objects. For example, polyfill for String.prototype.trim()allows you to equip all string objects with the method trim():

let text = "   A text  ";
text.trim(); //"A text"

Polifill for Array.prototype.find()equips all arrays method find(). Similarly, the polyfill works for Array.prototype.findIndex():

let arr = ["A", "B", "C", "D", "E"];
arr.indexOf("C"); //2

Single inheritance


The command Object.create()allows you to create new objects with a given prototype object. This command is used in JavaScript to implement a single inheritance mechanism. Consider an example :

let bookPrototype = {
  getFullTitle : function(){
    returnthis.title + " by " + this.author;
  }
}
let book = Object.create(bookPrototype);
book.title = "JavaScript: The Good Parts";
book.author = "Douglas Crockford";
book.getFullTitle();//JavaScript: The Good Parts by Douglas Crockford

Multiple inheritance


The command Object.assign()copies properties from one or more objects to the target object. It can be used to implement a multiple inheritance scheme. Here is an example :

let authorDataService = { getAuthors : function() {} };
let bookDataService = { getBooks : function() {} };
let userDataService = { getUsers : function() {} };
let dataService = Object.assign({},
 authorDataService,
 bookDataService,
 userDataService
);
dataService.getAuthors();
dataService.getBooks();
dataService.getUsers();

Immunity objects


The command Object.freeze()allows you to "freeze" the object. In such an object can not add new properties. Properties cannot be deleted, and their values ​​cannot be changed. By using this command, the object becomes immutable or immutable:

"use strict";
let book = Object.freeze({
  title : "Functional-Light JavaScript",
  author : "Kyle Simpson"
});
book.title = "Other title";//Ошибка: Cannot assign toreadonly property 'title'

The team Object.freeze()performs the so-called "shallow freezing" of objects. This means that objects nested in a “frozen” object can be modified. In order to implement a “deep freezing” of an object, one must recursively “freeze” all its properties.

Cloning objects


To create clones (copies) of objects, you can use the command Object.assign():

let book = Object.freeze({
  title : "JavaScript Allongé",
  author : "Reginald Braithwaite"
});
let clone = Object.assign({}, book);

This command performs a shallow copy of objects, that is, it copies only the properties of the top level. Nested objects appear to be common for original objects and their copies.

Object literal


Object literals give the developer a simple and intuitive way to create objects:

let timer = {
  fn : null,
  start : function(callback) { this.fn = callback; },
  stop : function() {},
}

However, this method of creating objects has disadvantages. In particular, with this approach, all properties of an object are publicly available, object methods can be overridden, they cannot be used to create new instances of identical objects:

timer.fn;//null 
timer.start = function() { console.log("New implementation"); }

Object.create () method


The two problems mentioned above can be solved by sharing the methods Object.create()and Object.freeze().

Apply this technique to our previous example. First, create a frozen prototype timerPrototypecontaining all the methods needed by different instances of the object. After that, create an object that is a successor timerPrototype:

let timerPrototype = Object.freeze({
  start : function() {},
  stop : function() {}
});
let timer = Object.create(timerPrototype);
timer.__proto__ === timerPrototype; //true

If the prototype is protected from changes, an object that is its successor will not be able to change the properties defined in the prototype. Now methods start()and stop()override can not be:

"use strict";
timer.start = function() { console.log("New implementation"); } //Ошибка: Cannot assign to read only property 'start' of object

The design Object.create(timerPrototype)can be used to create multiple objects with the same prototype.

Constructor function


In JavaScript, there are so-called constructor functions, which are “syntactic sugar” for performing the above described steps for creating new objects. Consider an example :

functionTimer(callback){
  this.fn = callback;
}
Timer.prototype = {
  start : function() {},
  stop : function() {}
}
functiongetTodos() {}
let timer = new Timer(getTodos);

As a constructor, you can use any function. The constructor is called using a keyword new. An object created using the constructor function named FunctionConstructorwill get a prototype FunctionConstructor.prototype:

lettimer = newTimer();
timer.__proto__ === Timer.prototype;

Here, to prevent the prototype from changing, again, you can freeze the prototype:

Timer.prototype = Object.freeze({
  start : function() {},
  stop : function() {}
});

▍ Keyword new


When a view command is executed new Timer(), the same actions are performed as the following function performs newTimer():

functionnewTimer(){
  let newObj = Object.create(Timer.prototype);
  let returnObj = Timer.call(newObj, arguments);
  if(returnObj) return returnObj;
    
  return newObj;
}

Here a new object is created, the prototype of which is Timer.prototype. Then a function is called Timerthat sets the fields for the new object.

Keyword class


In ECMAScript 2015, a new way to perform the actions described above has been introduced, representing another portion of “syntactic sugar”. We are talking about the keyword classand the corresponding structures associated with it. Consider an example :

class Timer{
  constructor(callback){
    this.fn = callback;
  }
  
  start() {}
  stop() {}  
}
Object.freeze(Timer.prototype);

An object created using a keyword classbased on a class with a name ClassNamewill have a prototype ClassName.prototype. When creating an object based on a class, use the keyword new:

lettimer= newTimer();
timer.__proto__ === Timer.prototype;

Using classes does not make prototypes unchanged. If necessary, they will have to be “frozen” just as we have already done:

Object.freeze(Timer.prototype);

Prototype Inheritance


In JavaScript, objects inherit properties and methods from other objects. Constructor functions and classes are “syntactic sugar” for creating prototype objects containing all the necessary methods. With their use, new objects are created which are the heirs of the prototype, whose properties that are specific to a particular instance are established using the constructor function or using class mechanisms.

It would be nice if constructor functions and classes could automatically make prototypes unchanged.

The strength of prototype inheritance is memory savings. The fact is that a prototype is created only once, after which it is used by all objects created on its basis.

▍The problem of the lack of built-in encapsulation mechanisms


The prototype inheritance pattern does not use the separation of object properties into private and public. All properties of objects are publicly available.

For example, the command Object.keys()returns an array containing all property keys of the object. It can be used to iterate through all the properties of an object:

function logProperty(name){
  console.log(name); //имя свойства
  console.log(obj[name]); //значение свойства
}
Object.keys(obj).forEach(logProperty);

There is one pattern that imitates private properties, relying on the fact that developers will not refer to those properties whose names begin with an underscore ( _):

classTimer{
  constructor(callback){
    this._fn = callback;
    this._timerId = 0;
  }
}

Factory Functions


Encapsulated objects in JavaScript can be created using factory functions. It looks like this:

function TodoStore(callback){
    let fn = callback;
    
    functionstart() {},
    function stop() {}
    
    returnObject.freeze({
       start,
       stop
    });
}

Here the variable fnis private. Only methods start()and are generally available stop(). These methods cannot be modified from the outside. The keyword is not used here this, so when using this method of creating objects, the problem of loss of context thisis irrelevant.

The command returnuses an object literal containing only functions. Moreover, these functions are declared in closure, they share a common state. To “freeze” the public API of an object, the command you already know is used Object.freeze().

Here we, in the examples, used the object Timer. In this material you can find its full implementation.

Results


In JavaScript, values ​​of primitive types, ordinary objects and functions are perceived as objects. Objects have a dynamic nature, they can be used as associative arrays. Objects are the heirs of other objects. Constructor functions and classes are “syntactic sugar”; they allow you to create objects based on prototypes. For the organization of single inheritance, you can use the method Object.create()for the organization of multiple inheritance - метод Object.assign(). You can use factory functions to create encapsulated objects.

Dear readers! If you came to JavaScript from other languages, please tell us what you like or dislike in JS objects, in comparison with the implementation of objects in languages ​​you already know.


Also popular now: