📜 ⬆️ ⬇️

Symbols, generators, async / await, and asynchronous iterators in JavaScript: their essence, interconnection, and use cases

The essence and purpose of the many features of JavaScript are quite obvious. But some, like generators, may, at first glance, seem strange. The same impression can also be caused by, say, symbols that are similar to the values ​​of primitive types and objects. However, a programming language is a complete system, some of which rely on others. Therefore, it is usually impossible to fully understand one thing without understanding all that is connected with it, on which it depends, and on what it affects.

image

The material, the translation of which we are publishing today, is aimed at explaining such JavaScript mechanisms and constructions as symbols, well-known symbols, iterators, iterable objects, generators, the async / await mechanism, and asynchronous iterators. In particular, it’s about why they appeared in the language and how to use them. It should be noted that the topics that will be raised here are designed for those who already have some idea of ​​JavaScript.

Symbols and famous symbols


In ES2015, a new sixth data type, symbol . What for? There are three main reasons for the appearance of this data type.
')

â–ŤReason # 1. Expansion of basic features, taking into account backward compatibility


JavaScript developers and the ECMAScript committee (TC39) needed the ability to add new object properties without disrupting existing mechanisms, such as for-in loops and methods like Object.keys .

Suppose we have such an object:

 var myObject = {firstName:'raja', lastName:'rao'} 

If you execute the Object.keys(myObject) command, it returns the array [firstName, lastName] .

Now let's add another property to the myObject object, for example, newProperty . In doing so, we need the Object.keys(myObject) command to return the same values ​​as before (in other words, something should make this function ignore the new property newProperty ), that is, [firstName, lastName] , and not [ firstName, lastName, newProperty] . How to do it?

In fact, before the appearance of the symbol data type, this could not be done. Now, if you add newProperty as a symbol, the Object.keys(myObject) command Object.keys(myObject) ignore this property (since it simply does not know about it) and returns what is needed - [firstName, lastName] .

â–ŤReason # 2. Name collision prevention


Those who are engaged in the development of JavaScript, would like to see the new properties of objects would be unique. This would allow them to continue to add new properties to global objects (the same could be done by developers using JS for solving practical problems), without worrying about name collisions.

For example, suppose that while working on a certain project, you decided to add your own toUpperCase method in Array.prototype .

Now imagine that you have connected to a project a certain library (or released ES2019), where you have your own version of the Array.prototype.toUpperCase method. This may well cause your code to stop working correctly.

Consider an example.

 Array.prototype.toUpperCase = function(){   var i;   for (i = 0; i<this.length; i++){       this[i] = this[i].toUpperCase();   }   return this; } var myArray = ['raja', 'rao']; myArray.toUpperCase(); //['RAJA', 'RAO'] 

How to resolve such a collision, provided that the developer may not even be aware of its existence? This is where symbols come to our aid. Inside them, unique identifiers are created that allow you to add new properties and methods to objects without worrying about possible name collisions.

â–ŤReason number 3. The organization of a call by standard mechanisms of the language of methods developed by the programmer independently


Suppose you want some standard function, say, String.prototype.search , to call your own function that implements your logic to search for something in the string. That is, for example, it is necessary that the construction 'somestring'.search(myObject); would call the search function of the myObject object and pass it, as an argument, 'somestring' . How to do it?

In such situations, we can use the capabilities of ES2015, in which there are many global symbols, called “well-known symbols”. And if in your object there is a property represented by one of these characters, you can organize a call to your function with standard functions. Below we will look at this mechanism in more detail, but before we do this, we will talk about how to work with symbols.

â–ŤCreate characters


You can create a new symbol by calling the global function Symbol . This function will return a value of type symbol .

 // mySymbol   symbol var mySymbol = Symbol(); 

Please note that the characters can be taken for objects, as they have methods, but they are not objects. These are primitive values. They can be considered as certain “special” objects that are somewhat similar to ordinary objects, but behave differently from them.

For example, symbols have methods that make them related to objects, but, unlike objects, symbols are immutable and unique.

â–ŤCreate characters and keyword new


Since symbols are not objects, and when using the keyword new , a new object is expected to return, you cannot use the keyword new to create entities of type symbol .

 var mySymbol = new Symbol(); // ,     

"Description" characters


What is called a “description” of a character is represented as a string and is used for logging.

 // mySymbol    , //   -  "some text" const mySymbol = Symbol('some text'); 

â–ŤOn the uniqueness of characters


Symbols are unique - even if the same description is used when creating them. This statement can be illustrated by the following example:

 const mySymbol1 = Symbol('some text'); const mySymbol2 = Symbol('some text'); mySymbol1 == mySymbol2 // false 

Create characters using the Symbol.for method


Instead of creating symbol type variables using the Symbol() function, you can create them using the Symbol.for(<key>) method Symbol.for(<key>) . This method takes a key (string <key> ) and creates new characters. In this case, if this key is given a key already assigned to an existing symbol, it will simply return that existing symbol. Therefore, we can say that the behavior of Symbol.for resembles a singleton design pattern.

 var mySymbol1 = Symbol.for('some key'); //   var mySymbol2 = Symbol.for('some key'); //     mySymbol1 == mySymbol2 //true 

The Symbol.for method exists so that you can create characters in one place and work with them in another.

Please note that the features of the .for() method .for() transfer of a key that has already been used, it does not create a new character with such a key, but returns an existing one. Therefore, use it with caution.

Keys and character descriptions


It is worth noting that if you do not use the Symbol.for construction, the symbols, even when using the same keys, will be unique. However, if you use Symbol.for , then specifying non-unique keys and symbols returned by Symbols.for will be non-unique. Consider an example.

 var mySymbol1 = Symbol('some text'); //     "some text" var mySymbol2 = Symbol('some text'); //     "some text" var mySymbol3 = Symbol.for('some text'); //     "some text" var mySymbol4 = Symbol.for('some text'); //   ,    mySymbol3 //    true, //        .for //       mySymbol3 == mySymbol4 //true //    mySymbol1 == mySymbol2 //false mySymbol1 == mySymbol3 //false mySymbol1 == mySymbol4 //false 

â–Ť Using characters as object property identifiers


Although symbols are like objects, they are primitive values. Perhaps this unique feature of them is most confusing. In particular, symbols can be used as identifiers of object properties — just as strings are used for this.

In fact, object property identifiers are one of the main uses for symbols.

 const mySymbol = Symbol("Some car description"); const myObject = {name: 'bmw'}; myObject[mySymbol] = 'This is a car'; //      //    console.log(myObject[mySymbol]); //'This is a car' 

Please note that the properties of the objects, which are symbols, are known as “Symbol-keyed properties”, or “properties with the keys of the Symbol”.

â–Ť Point and square brackets


When working with properties of objects that are characters, you cannot use a period, since this operator is only suitable for working with properties specified by strings. Instead, in such situations, use square brackets.

 let myCar = {name: 'BMW'}; let type = Symbol('store car type'); myCar[type] = 'A_1uxury_Sedan'; let honk = Symbol('store honk function'); myCar[honk] = () => 'honk'; // myCar.type; //  myCar[type]; // 'store car type' myCar.honk(); //  myCar[honk](); // 'honk' 

â–ŤWhy use symbols?


Now, after we have learned how symbols work, we will repeat and rethink three main reasons for using them.

â–Ť Reason number 1. Symbols used as identifiers of object properties are invisible to loops and other methods.


In the following example, the for-in loop loops over the properties of the obj object, but it does not know about the prop3 and prop4 (or ignores these properties), since their identifiers are represented by characters.

 var obj = {}; obj['prop1'] = 1; obj['prop2'] = 2; //     -, //   (    //   ) var prop3 = Symbol('prop3'); var prop4 = Symbol('prop4'); obj[prop3] = 3; obj[prop4] = 4; for(var key in obj){   console.log(key, '=', obj[key]); } //   ,     //   prop3  prop4 //prop1 = 1 //prop2 = 2 //   prop3  prop4  , //   console.log(obj[prop3]); //3 console.log(obj[prop4]); //4 

Below is another example in which the Object.keys and Object.getOwnPropertyNames ignore property names represented by characters.

 const obj = {   name: 'raja' }; //     - obj[Symbol('store string')] = 'some string'; obj[Symbol('store func')] = () => console.log('function'); //  -   //  console.log(Object.keys(obj)); //[name] console.log(Object.getOwnPropertyNames(obj)); //[name] 

â–Ť Reason # 2. The characters are unique


Suppose we need to expand the global Array object by adding our own Array.prototype.includes method to its prototype. This method will conflict with the standard includes method that is present in ES2018 JavaScript. How to equip a prototype with this method and avoid collisions?

First you need to create a variable with the name includes and assign a symbol to it. Then you need to use this variable to add a new property to the global Array object using the bracket notation. After that, it remains only to assign the necessary function to the new property.

To call a new function, you must use square brackets. Moreover, pay attention to the fact that in brackets you need to use the name of the corresponding variable, that is, something like arr[includes]() , and not a regular string.

 var includes = Symbol('will store custom includes method'); //    Array.prototype Array.prototype[includes] = () => console.log('inside includes func'); // var arr = [1,2,3]; //       // includes console.log(arr.includes(1)); //true console.log(arr['includes'](1)); //true //       includes arr[includes](); // 'inside includes func',   includes -   

â–Ť Reason number 3. Known Symbols (Global Symbols)


By default, JavaScript automatically creates many character variables and writes them to the global Symbol object (this is the same object that we used to create new characters).

In ECMAScript 2015, these symbols are then used to work with basic methods, such as String.prototype.search and String.prototype.replace standard objects like String or Array .

Here are some examples of such symbols: Symbol.match , Symbol.replace , Symbol.search , Symbol.iterator and Symbol.split .

Since these symbols are global and publicly available, it is possible to make the methods of standard objects call our own functions instead of internal ones.

â–Ť Example number 1. Using Symbol.search


The publicly accessible String.prototype.search method of the String.prototype.search object searches the string using a regular expression or a sample string, and if it managed to find what it needs, it returns the index of the search term in the analyzed string.

 'rajarao'.search(/rao/); 'rajarao'.search('rao'); 

In ES2015, this method first checks whether the Symbol.search method is Symbol.search in a RegExp object. If this is the case, then it is the method of the RegExp object that is being searched for. Thus, the basic objects, like RegExp , implement methods corresponding to Symbol.search , which solve search problems.

Symbol Symbol.search internal mechanisms (standard behavior)


The process of searching for a substring in a string consists of the following steps.

  1. 'rajarao'.search('rao'); command 'rajarao'.search('rao');
  2. The string "rajarao" converted to a String (new String("rajarao")) object String (new String("rajarao"))
  3. The "rao" character sequence is converted to a RegExp (new Regexp("rao")) object RegExp (new Regexp("rao"))
  4. The search method of a String object based on the string "rajarao"
  5. Inside the search method of the "rajarao" object "rajarao" method of the "rao" object is called (that is, the search operation is delegated to the "rao" object). The string "rajarao" passed to this method. If you schematically present this call, it can look something like this: "rao"[Symbol.search]("rajarao")
  6. The "rao"[Symbol.search]("rajarao") returns, as a search result, the number 4 representing the index of the search substring in the string of the search function of the object "rajarao" , and this function, in turn, returns 4 to our code .

Below is a fragment written in pseudocode, demonstrating the structure of the internal mechanisms of standard JavaScript objects described above.

 //  String class String {   constructor(value){       this.value = value;   }   search(obj){       //  Symbol.search  obj          // value       obj[Symbol.search](this.value);   } } //  RegExp class RegExp {   constructor(value){       this.value = value;   }   //     [Symbol.search](string){       return string.indexOf(this.value);   } } 

The most interesting thing here is that in such a situation it is no longer necessary to use the RegExp object. The standard function String.prototype.search can now correctly perceive any object that implements the Symbol.search method, which returns what the developer needs, and this will not break the code. Let us dwell on this in more detail.

â–Ť Example number 2. Using Symbol.search to organize a call to a self-developed function from standard functions


The following example shows how we can make the function String.prototype.search the search function of our own class Product . This is possible thanks to the global symbol Symbol.search .

 class Product {   constructor(type){       this.type = type;   }   //     [Symbol.search](string){       return string.indexOf(this.type) >=0 ? 'FOUND' : "NOT_FOUND";   } } var soapObj = new Product('soap'); 'barsoap'.search(soapObj); //FOUND 'shampoo'.search(soapObj); //NOT_FOUND 

Symbol Symbol.search internal mechanisms (custom behavior)


When “searching” our object in the string, the following actions are performed.

  1. 'barsoap'.search(soapObj); command 'barsoap'.search(soapObj);
    The string "barsoap" converted to a String object ( new String("barsoap") )
  2. Since soapObj already an object, it is not converted.
  3. Call the search method of a String object based on the string "barsoap"
  4. Inside this method, the Symbol.search method of the Symbol.search object is soapObj (the search operation is delegated to this object), and the string "barsoap" . In fact, we are talking about this command: soapObj[Symbol.search]("barsoap")
    soapObj[Symbol.search]("barsoap")

    The soapObj[Symbol.search]("barsoap") returns to the search function a result that, in accordance with the internal logic of the soapObj object, can be FOUND and NOT_FOUND . The search function returns this result to our code.

Now that we’ve dealt with the characters, let's do some iterators.

Iterators and iterated objects


To begin, let us ask ourselves why this is necessary. Here the fact is that in almost all applications it is necessary to work with lists of data. For example, these lists should be displayed on ordinary web pages or in mobile applications. Usually, developers write their own methods to store and retrieve data from such lists.

However, we already have standard language mechanisms, like a for-of loop and an extension operator ( … ), designed to extract data sets from standard objects like arrays, strings, objects of type Map . Why don't we use these standard methods to work with our own objects that store data sets?

The following example shows that the for-of loop and the extension operator cannot be used to extract data from the User class, which we have created ourselves. Here, to work with it, you have to use the get method, which we also created ourselves.

 //    //      for-of  //       //   Users,    //  class Users {   constructor(users){       this.users = users;   }   //     get() {       return this.users;   } } const allUsers = new Users([   { name: 'raja' },   { name: 'john' },   { name: 'matt' }, ]); // allUsers.get() ,     for (const user of allUsers){   console.log(user); } //    TypeError: allUsers is not iterable //    const users = [...allUsers]; //    TypeError: allUsers is not iterable 

It would be nice if we could use, for working with our own objects, standard language mechanisms. In order to achieve this, you need to have rules that developers can follow when creating such objects with which standard JS tools can work.

In fact, such rules exist. They describe the features of extracting data from objects. Objects built according to these rules are called iterables .

Here are the rules:

  1. The main object / class must store some data.
  2. It must have a property that is the well-known Symbol.iterator symbol, which implements the method described in clauses 3-6.
  3. The Symbol.iterator method should return another object - an iterator.
  4. The iterator must have a next method.
  5. The next method should have access to the data described in clause 1.
  6. When the next method is called, the data from point 1 should be returned, either in the format {value:<stored data>, done: false} , if the iterator can return something else, or in the form {done: true} , if The iterator has nothing more to return.

If all these requirements are met, then the main object is called iterable, and the object it returns is called an iterator.

Let's talk now about how to make the Users object iterable.

 //   // Users   ,     // Symbol.iterator,      next, //       class Users{   constructor(users){       this.users = users;   }   // Symbol.iterator -  ,     //    [Symbol.iterator](){       let i = 0;       let users = this.users;       //           return {           next(){               if (i<users.length) {                   return { done: false, value: users[i++] };               }               return { done: true };           },       };   } } //allUsers    const allUsers = new Users([   { name: 'raja' },   { name: 'john' },   { name: 'matt' }, ]); //allUsersIterator   const allUsersIterator = allUsers[Symbol.iterator](); // next      , //    console.log(allUsersIterator.next()); console.log(allUsersIterator.next()); console.log(allUsersIterator.next()); //     : //{ done: false, value: { name: 'raja' } } //{ done: false, value: { name: 'john' } } //{ done: false, value: { name: 'matt' } } //  for-of for(const u of allUsers){   console.log(u.name); } //   : raja, john, matt //   console.log([...allUsers]); //     

, ( allUsers ) for-of , <iterable>[Symbol.iterator]() , ( allUsersIterator ), .

.

Generators


:

  1. .
  2. , , .

.

â–Ť â„–1.


, , , - (generator), .

.

  1. - , *<myGenerator> . - function * myGenerator(){}
  2. myGenerator() generator , () , , , iterator .
  3. yield .
  4. yield , .
  5. yield , , next .

â–Ť â„–1. - Symbol.iterator


- ( *getIterator() ) Symbol.iterator next() , .

 //     ,  //  - (*getIterator()) //      class Users{   constructor(users) {       this.users = users;       this.len = users.length;   }   // ,      *getIterator(){       for (let i in this.users){           yield this.users[i];           //     ,           //yield             }   } } const allUsers = new Users([   { name: 'raja' },   { name: 'john' },   { name: 'matt' }, ]); //allUsersIterator   const allUsersIterator = allUsers.getIterator(); // next      , //    console.log(allUsersIterator.next()); console.log(allUsersIterator.next()); console.log(allUsersIterator.next()); console.log(allUsersIterator.next()); //     : //{ done: false, value: { name: 'raja' } } //{ done: false, value: { name: 'john' } } //{ done: false, value: { name: 'matt' } } //{ done: true, value: undefined } //  for-of for(const u of allUsers.getIterator()){   console.log(u.name); } //   : raja, john, matt //   console.log([...allUsers.getIterator()]); //     

â–Ť â„–2. -


-. , , , . - ( * ) yield .

 // Users -  ,    function* Users(users){   for (let i in users){       yield users[i++];       //     ,       //yield         } } const allUsers = Users([   { name: 'raja' },   { name: 'john' },   { name: 'matt' }, ]); // next      , //    console.log(allUsers.next()); console.log(allUsers.next()); console.log(allUsers.next()); console.log(allUsers.next()); //     : //{ done: false, value: { name: 'raja' } } //{ done: false, value: { name: 'john' } } //{ done: false, value: { name: 'matt' } } //{ done: true, value: undefined } const allUsers1 = Users([   { name: 'raja' },   { name: 'john' },   { name: 'matt' }, ]); //  for-of for(const u of allUsers1){   console.log(u.name); } //   : raja, john, matt const allUsers2 = Users([   { name: 'raja' },   { name: 'john' },   { name: 'matt' }, ]); //   console.log([...allUsers2]); //     

, «iterator» allUsers , , , Generator .

Generator throw return , next . , , .

â–Ť â„–2.


, , , .

, , , yield ( , ) , , , yield .

, , - yield , . , generator.next("some new value") , yield .


-

, , .

 function* generator(a, b){   //  a + b   // ,   k   ,     //  a + b   let k = yield a + b;   //      m   let m = yield a + b + k;   yield a + b + k + m; } var gen = generator(10, 20); //   a + b //   ,  done   false,   //      yield console.log(gen.next()); //{value: 30, done: false} //      ,     //a  b,    .next() ,    - //,      ,    // //  k 50      a + b + k console.log(gen.next(50)); //{value: 80, done: false} //     ,     a, b  k. //  .next()      ,  //     ,     //  m 100      a + b + k + m console.log(gen.next(100)); //{value: 180, done: false} //   .next(),   undefined,     //    yield console.log(gen.next()); //{value: undefined, done: true} 

â–Ť


.

 // //  - function *myGenerator() {} // function * myGenerator() {} // function* myGenerator() {} //  const myGenerator = function*() {} //   ,  //      let generator = *() => {} //      //SyntaxError: Unexpected token * //   ES2015 class MyClass {   *myGenerator() {} } //    const myObject = {   *myGenerator() {} } 

â–Ť yield return


yield , return . , , return , . , yield , .

 function* myGenerator() {   let name = 'raja';   yield name;   console.log('you can do more stuff after yield'); } //   const myIterator = myGenerator(); // .next()    console.log(myIterator.next()); //{value: "raja", done: false} // .next()   //      // 'you can do more stuff after yield'    //{value: undefined, done: true} console.log(myIterator.next()); 

â–Ť yield


yield , , return , , yield .

 function* myGenerator() {   let name = 'raja';   yield name;    let lastName = 'rao';   yield lastName; } //   const myIterator = myGenerator(); // .next()    console.log(myIterator.next()); //{value: "raja", done: false} // .next()   console.log(myIterator.next()); //{value: "rao", done: false} 

â–Ť next


.next() .

, , , ( ). , , redux-saga .

.next() , ( ). ( 23 ), .next(23) .

 function* profileGenerator() {   //  .next()   ,     //,    yield,   .   //     ,  ,     //  .next(),     answer   let answer = yield 'How old are you?';   // 'adult'  'child'       // answer   if (answer > 18){       yield 'adult';   } else {       yield 'child';   } } //   const myIterator = profileGenerator(); console.log(myIterator.next()); //{value: "How old are you?", done: false} console.log(myIterator.next(23)); //{value: "adult", done: false} 

â–Ť


, .

, , co , , , .next() . , .

, co .next(result) â„–5 10, .

 co(function *() {   let post = yield Post.findByID(10);   let comments = yield post.getComments();   console.log(post, comments); }).catch(function(err){   console.error(err); }); 



  1. co , ,
  2. Post.findByID(10)
  3. Post.findByID(10)
  4. , , .next(result)
  5. post
  6. post.getComments()
  7. post.getComments()
  8. , , .next(result)
  9. comments
  10. console.log(post, comments);

async/await.

async/await


, , , , , co . , — , ECMAScript , . async await .
async/await.

  1. async/await await yield .
  2. await .
  3. async function , function* .

, async/await — , « ».

async , , JavaScript- , -. , , await . , await , , .

getAmount — getUser getBankBalance . , async/await .

 //  ES2015... function getAmount(userId){   getUser(userId)       .then(getBankBalance)       .then(amount => {           console.log(amount);       }); } //  async/await ES2017 async function getAmount2(userId){   var user = await getUser(userId);   var amount = await getBankBalance(user);   console.log(amount); } getAmount('1'); //$1,000 getAmount2('1'); //$1,000 function getUser(userId){   return new Promise(resolve => {       setTimeout(() => {           resolve('john');       }, 1000);   }); } function getBankBalance(user){   return new Promise((resolve, reject) => {       setTimeout(() => {           if (user == 'john'){               resolve('$1,000');           } else {               reject('unknown user');           }       }, 1000);   }); } 


. ES2018 ( ) TC39 Symbol.asyncIterator , — for-await-of , .

.

â–Ť


  1. .next() {value: 'some val', done: false}
  2. : iterator.next() //{value: 'some val', done: false}

â–Ť


.next() , , , {value: 'some val', done: false} .

:

 iterator.next().then(({ value, done })=> {//{value: 'some val', done: false}} 

for-await-of .

 const promises = [   new Promise(resolve => resolve(1)),   new Promise(resolve => resolve(2)),   new Promise(resolve => resolve(3)), ]; //       , //           async function test(){   for await (const p of promises){       console.log(p);   } } test(); //1, 2, 3 

Results


JavaScript, , , . .


Dear readers! , , ?

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


All Articles