Introduction to ECMAScript 2017 (ES8)

Table of contents



Foreword

ES7 Overview

1. Object.entries

2. Object.values

3. String.prototype.padEnd

4. String.prototype.padStart

5. Object.getOwnPropertyDescriptor

6. Trailing commas

7. SharedArrayBuffer

8. Atomics

9. Async functions



Foreword



Hello, in the past I already considered innovations in ES6 and now it's time to take apart ES8 as it brought a lot of new things. I did not consider ES7 (2016) separately, since this release brought only 2 innovations. This is Array.prototype.includes () and the exponentiation operator. But still, before starting ES8, let's look at the innovations from ES7.



ES7 Overview



The includes () method determines whether the array contains a specific element, returning true or false depending on this.



Array.prototype.includes(searchElement[, fromIndex = 0]) : Boolean
      
      





searchElement - The item to search.



fromIndex - The position in the array from which to start the searchElement element search. For negative values, the search is performed starting with the index array.length + fromIndex ascending. The default value is 0.



Examples



 [1, 2, 3].includes(2); // true [1, 2, 3].includes(4); // false [1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true [1, 2, NaN].includes(NaN); // true
      
      





includes () can be applied to other types of objects (for example, array-like objects). Example: using the includes () method on an arguments object.



 (function() { console.log([].includes.call(arguments, 'a')); // true console.log([].includes.call(arguments, 'd')); // false })('a','b','c');
      
      





The exponentiation operator (**) returns a degree with base a and a natural exponent b. Raising a to the power of b.



 a ** b
      
      





Examples



 2 ** 3 // 8 3 ** 2 // 9 3 ** 2.5 // 15.588457268119896 10 ** -1 // 0.1 NaN ** 2 // NaN 2 ** 3 ** 2 // 512 2 ** (3 ** 2) // 512 (2 ** 3) ** 2 // 64 -(2 ** 2) // -4 (-2) ** 2 // 4
      
      





1. Object.entries



Object.entries () returns an array whose elements are arrays corresponding to the enumerated property of the [key, value] pair found directly in object. The order of the properties is the same as when passing through the loop through the properties of an object manually.



 Object.entries(obj) : Array
      
      





obj - An object whose enumerated properties will be returned as an array [key, value].



Object.entries () returns the properties in the same order as in the for ... in loop (the difference is that for-in also lists properties from the prototype chain). The order of the elements in the array that Object.entries () returns is independent of how the object is declared. If a specific order is needed, the array must be sorted before the method is called.



Examples



 var obj = { foo: "bar", baz: 42 }; console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ] //    var obj = { 0: 'a', 1: 'b', 2: 'c' }; console.log(Object.entries(obj)); // [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ] //    c random   var an_obj = { 100: 'a', 2: 'b', 7: 'c' }; console.log(Object.entries(an_obj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ] // getFoo  ,    var my_obj = Object.create({}, { getFoo: { value: function() { return this.foo; } } }); my_obj.foo = "bar"; console.log(Object.entries(my_obj)); // [ ['foo', 'bar'] ] // non-object     object console.log(Object.entries("foo")); // [ ['0', 'f'], ['1', 'o'], ['2', 'o'] ] let obj = { one: 1, two: 2 }; for (let [k,v] of Object.entries(obj)) console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`) // "one": 1 // "two": 2
      
      





Convert Object to Map



The new Map () constructor accepts the repetition of values. With Object.entries, you can easily convert Object to Map. This is more concise than using an array of 2 element arrays, but keys can only be strings.



 var obj = { foo: "bar", baz: 42 }; var map = new Map(Object.entries(obj)); console.log(map); // Map {"foo" => "bar", "baz" => 42}
      
      





Why is the return value of Object.entries () an array and not an iterator?

The corresponding use case in this case is Object.keys (), and not, for example, Map.prototype.entries ().



Why does Object.entries () return only enumerated native properties with string keys?



Again, this is done to match Object.keys (). This method also ignores properties whose keys are characters. After all, there may be a Reflect.ownEntries () method that returns all of its own properties.



See object.entries in the official specification , as well as in MDN Web Docs .



2. Object.values



Object.values โ€‹โ€‹() returns an array whose elements are the values โ€‹โ€‹of the enumerated properties found in the object. The order is the same as if you cycle through the object manually.



 Object.values(obj) : Array
      
      





obj - An object whose values โ€‹โ€‹of the enumerated properties will be returned.



The Object.values โ€‹โ€‹() method returns an array of values โ€‹โ€‹of the enumerated properties of the object in the same order as the for ... in loop. The difference between a loop and a method is that the loop lists properties from and from the prototype chain.



Examples



 var obj = { foo: "bar", baz: 42 }; console.log(Object.values(obj)); // ['bar', 42] //    var obj = { 0: 'a', 1: 'b', 2: 'c' }; console.log(Object.values(obj)); // ['a', 'b', 'c']
      
      





The difference between Object.entries and Object.values โ€‹โ€‹() is that the first returns an array of arrays containing the name and value of the property, while the second returns only an array with the value of the properties.



Example difference between Object.values โ€‹โ€‹() and Object.entries ()



 const object = { a: 'somestring', b: 42, c: false }; console.log(Object.values(object)); // ["somestring", 42, false] console.log(Object.entries(object)); // [ ["a", "somestring"], ["b", 42], ["c", false] ]
      
      





See Object.values โ€‹โ€‹() in the official specification , as well as in MDN Web Docs .



3. String.prototype.padEnd



The padEnd () method completes the current line with the given string (eventually repeating) so that the resulting string reaches the specified length. Addition is applied at the end (right) of the current line.



 String.prototype.padEnd(maxLength [ , fillString ]) : String
      
      





maxLength - The length of the resulting row after the current row has been padded. If this parameter is less than the length of the current line, the current line will be returned as it is.

fillString - A string to complement the current line with. If this line is too long, it will be truncated and its left most will be applied. "" (0x0020 SPACE) is the default value for this parameter.



Examples



 'abc'.padEnd(10); // "abc " 'abc'.padEnd(10, "foo"); // "abcfoofoof" 'abc'.padEnd(6,"123456"); // "abc123"
      
      





Use cases for populating strings include:





See String.prototype.padEnd in the official specification , as well as in MDN Web Docs .



4. String.prototype.padStart



The padStart () method fills the current line with another line (several times, if necessary) so that the resulting line reaches the specified length. Filling is carried out at the beginning (left) of the current line.



 String.prototype.padStart(maxLength [, fillString]) : String
      
      





maxLength - The length of the summary line after the current line is completed. If the value is less than the length of the current line, the current line will be returned unchanged.



fillString - A string to fill the current line. If this string is too long for the given length, it will be truncated. The default value is "" (0x0020 SPACE).



Examples



 'abc'.padStart(10); // " abc" 'abc'.padStart(10, "foo"); // "foofoofabc" 'abc'.padStart(6,"123465"); // "123abc" 'abc'.padStart(8, "0"); // "00000abc" 'abc'.padStart(1); // "abc"
      
      





Why aren't padding methods called padLeft and padRight?



For bidirectional or right-to-left languages, the terms โ€œleftโ€ and โ€œrightโ€ do not work. Therefore, the naming of padStart and padEnd follows existing names starting with startsWith and endsWith.



See String.prototype.padStart in the official specification , as well as in MDN Web Docs .



5. Object.getOwnPropertyDescriptor



The Object.getOwnPropertyDescriptor () method returns a property descriptor for its own property (that is, one located directly in the object, and not received through the prototype chain) of the passed object. If the property does not exist returns undefined.



 Object.getOwnPropertyDescriptor(obj, prop) : Object
      
      





obj - The object in which the property is searched.



prop - The name of the property whose description will be returned.



This method allows you to view the exact description of the property. A property in JavaScript consists of a string name and a property descriptor.



A property descriptor is a record with some of the following attributes:





Examples



 obj = { get foo() { return 10; } }; console.log(Object.getOwnPropertyDescriptor(obj, 'foo')); // {set: undefined, enumerable: true, configurable: true, get: ฦ’} obj2 = { bar: 42 }; console.log(Object.getOwnPropertyDescriptor(obj2, 'bar')); // {value: 42, writable: true, enumerable: true, configurable: true}
      
      





Use cases for Object.getOwnPropertyDescriptor ()



First use case: copy properties to an object

Starting with ES6, JavaScript already has a tool method for copying properties: Object.assign (). However, this method uses simple get and set operations to copy a property whose key is the key:



 const value = source[key]; // get target[key] = value; // set
      
      



This means that it does not correctly copy properties with attributes other than those specified by default (methods for obtaining, setting, writing, etc.). The following example illustrates this limitation. The source of the object has an installer whose key is foo:
 const source = { set foo(value) { console.log(value); } }; console.log(Object.getOwnPropertyDescriptor(source, 'foo')); // { get: undefined, set: [Function: foo], enumerable: true, configurable: true }
      
      





Using Object.assign () to copy the foo property to the target object fails:



 const target1 = {}; Object.assign(target1, source); console.log(Object.getOwnPropertyDescriptor(target1, 'foo')); // { value: undefined, writable: true, enumerable: true, configurable: true }
      
      



Fortunately, using Object.getOwnPropertyDescriptors () along with Object.defineProperties () works:



 const target2 = {}; Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source)); console.log(Object.getOwnPropertyDescriptor(target2, 'foo')); // { get: undefined, set: [Function: foo], enumerable: true, configurable: true }
      
      





Second use case: cloning objects

Shallow cloning is similar to copying properties, so Object.getOwnPropertyDescriptors () is also a good choice here.



This time we use Object.create (), which has two parameters:

The first parameter specifies the prototype of the returned object.



An optional second parameter is a collection of property descriptors, similar to those returned by Object.getOwnPropertyDescriptors ().



 const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
      
      





Third use case: cross-platform object literals with arbitrary prototypes.



The syntactically best way to use an object literal to create an object with an arbitrary prototype is to use the special __proto__ property:



 const obj = { __proto__: prot, foo: 123, };
      
      





Alas, this feature is guaranteed to be present only in browsers. The general workaround is Object.create () and assignment:



 const obj = Object.create(prot); obj.foo = 123;
      
      





But you can also use Object.getOwnPropertyDescriptors ():



 const obj = Object.create( prot, Object.getOwnPropertyDescriptors({ foo: 123, }) );
      
      





Another alternative is Object.assign ():



 const obj = Object.assign( Object.create(prot), { foo: 123, } );
      
      





Pitfall: copy methods using super.



The method that super uses is tightly bound to its home object (the object in which it is stored). There is currently no way to copy or move such a method to another object.



See Object.getOwnPropertyDescriptor in the official specification , as well as in MDN Web Docs .



6. Trailing commas



Trailing commas can be useful when adding new elements, parameters or properties to JavaScript code. If you want to add a new property, you simply add a new line without changing the previous one, if the hanging comma is already used in it. This makes the differences in version control cleaner and code changes can be less troublesome.



Hanging commas in literals



Arrays



JavaScript ignores hanging commas in arrays:



 var arr = [ 0, 1, 2, ]; console.log(arr); // [0, 1, 2] console.log(arr.length); // 3 var arr2 = [0, 1, 2,,,]; console.log(arr2.length); // 5 arr2.forEach((e) => console.log(e)); // 0 1 2 console.log(arr.map((e) => e)); // 0 1 2
      
      





If more than one dangling point is used, holes will be created. An array with โ€œholesโ€ is called sparse (a dense array does not have โ€œholesโ€). When iterating an array using, for example, Array.prototype.forEach () or Array.prototype.map (), holes will be skipped.



The objects



 var object = { foo: "bar", baz: "qwerty", age: 42, }; console.log(object); // {foo: "bar", baz: "qwerty", age: 42}
      
      





Hanging commas in functions



Parameter Definition



The following definitions of function parameters are valid and equivalent to each other. Dangling commas do not affect the length property of a function or their arguments object.



 function f(p) {} function f(p,) {} (p) => {}; (p,) => {};
      
      





Method definition



The hanging comma also works with defining methods for classes or objects.



 class C { one(a,) {}, two(a, b,) {}, } var obj = { one(a,) {}, two(a, b,) {}, };
      
      





Function call



The following function calls are valid and equivalent to each other.



 f(p); f(p,); Math.max(10, 20); Math.max(10, 20,);
      
      





Invalid hanging commas



Defining function parameters or calling a function containing only a comma will raise a SyntaxError. In addition, when using the remaining parameters, hanging commas are not allowed.



 function f(,) {} // SyntaxError: missing formal parameter (,) => {}; // SyntaxError: expected expression, got ',' f(,) // SyntaxError: expected expression, got ',' function f(...p,) {} // SyntaxError: parameter after rest parameter (...p,) => {} // SyntaxError: expected closing parenthesis, got ','
      
      





Hanging Commas in Destructuring



Hanging commas can also be used on the left when using destructive assignment.



 //      [a, b,] = [1, 2]; //      var o = { p: 42, q: true, }; var {p, q,} = o;
      
      





Once again, using the remaining parameters, a SyntaxError will be thrown.



 var [a, ...b,] = [1, 2, 3]; // Uncaught SyntaxError: Rest element must be last element
      
      





JSON Dangling Commas



Dangling commas in an object are only allowed in ECMAScript 5. Since JSON is based on JavaScript syntax older than ES5, dangling commas are not allowed in JSON.



Both lines throw a SyntaxError



 JSON.parse('[1, 2, 3, 4, ]'); JSON.parse('{"foo" : 1, }'); // Uncaught SyntaxError: Unexpected token ] in JSON // Uncaught SyntaxError: Unexpected token } in JSON
      
      





Why are hanging commas useful?



There are two benefits.



Firstly, rearranging elements is easier because you do not need to add or remove commas if the last element changes its position.



Secondly, it helps version control systems keep track of what has really changed. For example, from:



 [ 'Foo' ] : [ 'Foo', '' ]
      
      





causes both the line with 'foo' and the line with 'bar' to be marked as changed, although the only real change is to add the last line.



See Trailing commas at MDN Web Docs .



7. SharedArrayBuffer



The SharedArrayBuffer object is used to create a fixed-length split buffer for storing primitive binary data, similar to the ArrayBuffer object, but can be used to create a view on shared memory. Unlike ArrayBuffer, SharedArrayBuffer cannot be detached.
 new SharedArrayBuffer(length) : Object
      
      



length - The size, in bytes, to create the buffer array.



return - A new SharedArrayBuffer object of the specified length. Its contents after initialization is 0.



PostMessage and structured cloning are used to split memory using the SharedArrayBuffer object between one agent in the cluster and another (the agent can be either the main program of the web page or one of the web-workers).



The structured cloning algorithm accepts SharedArrayBuffers and TypedArrays mapped to SharedArrayBuffers. In both cases, the SharedArrayBuffer object is passed to the receiver, resulting in a new, private SharedArrayBuffer object inside the receiving agent (same as for ArrayBuffer). However, the shared data block referenced by both SharedArrayBuffer objects is the same data block, and third-party effects in the block in one of the agents will eventually become visible in the other agent.



 var sab = new SharedArrayBuffer(1024); worker.postMessage(sab);
      
      





Shared memory can be created and changed simultaneously in workers or the main thread. Depending on the system (CPU, OS, browser), it may take time until the changes are propagated to all contexts. For synchronization, atomic operations are needed.



Shared Array Buffers is a primitive building block for higher level parallelism abstractions. They allow you to share the bytes of a SharedArrayBuffer object between multiple workers and the main thread (the buffer is shared to access bytes, wrap it in a Typed Array). This type of exchange has two advantages:

You can exchange data between workers faster.



Coordination between workers is easier and faster (compared to postMessage ()).



The implementation of the worker is as follows.



 // worker.js self.addEventListener ('message', function (event) { const {sharedBuffer} = event.data; const sharedArray = new Int32Array (sharedBuffer); // ยทยทยท });
      
      





First, we extract the buffer of the shared array that was sent to us, and then wrap it in a typed array so that we can use it locally.



Properties and methods of SharedArrayBuffer.



SharedArrayBuffer.length - The length of the SharedArrayBuffer constructor whose value is 1.

SharedArrayBuffer.prototype - Allows additional properties for all SharedArrayBuffer objects.



SharedArrayBuffer Instances

The properties



SharedArrayBuffer.prototype.constructor - Defines a function that creates a prototype of an object. The initial value is the standard built-in SharedArrayBuffer constructor.



SharedArrayBuffer.prototype.byteLength (Read only) - The size of the array in bytes. This is set when the array is created and cannot be changed.



Methods



SharedArrayBuffer.prototype.slice () - Returns a new SharedArrayBuffer whose contents are a copy of the bytes of this SharedArrayBuffer from the beginning, including to the end, of the exclusive. If the beginning or end is negative, this refers to the index from the end of the array, not from the beginning. This method has the same algorithm as Array.prototype.slice ().



 //  SharedArrayBuffer     const buffer = new SharedArrayBuffer(16); const int32View = new Int32Array(buffer); //  view // produces Int32Array [0, 0, 0, 0] int32View[1] = 42; const sliced = new Int32Array(buffer.slice(4,12)); console.log(sliced); // Int32Array [42, 0]
      
      





 sab.slice([begin, end]) : Object
      
      





begin - The zero index at which extraction begins. You can use a negative index indicating the offset from the end of the sequence. slice (-2) extracts the last two elements in a sequence. If the beginning is not defined, the slice starts at index 0.

End - The zero-based index to which extraction should be completed.



For example, slice (1,4) retrieves the second element through the fourth element (elements with indices 1, 2, and 3). You can use a negative index indicating the offset from the end of the sequence. slice (2, -1) retrieves the third element through the second-last element in the sequence. If end is omitted, slice fetches through the end of the sequence (sab.byteLength).



Examples



 var sab = new SharedArrayBuffer(1024); sab.slice(); // SharedArrayBuffer { byteLength: 1024 } sab.slice(2); // SharedArrayBuffer { byteLength: 1022 } sab.slice(-2); // SharedArrayBuffer { byteLength: 2 } sab.slice(0, 1); // SharedArrayBuffer { byteLength: 1 }
      
      





See SharedArrayBuffer in the official specification , as well as in the MDN Web Docs .



8. Atomics



The Atomics object provides atomic operations as static methods. Used with a SharedArrayBuffer object.



Atomic operations are installed in the Atomics module. Unlike other global objects, Atomics is not a constructor. It cannot be used with the new operator or to call an Atomics object as a function. All Atomics properties and methods are static (like a Math object, for example).



When the memory is shared, several threads can read and write the same data to the memory. Atomic operations guarantee that the expected values โ€‹โ€‹will be written and read, and the operations completed before the next operation begins its work, and they will not be interrupted.



The properties



Atomics [Symbol.toStringTag] - The value of this property is Atomics.



Methods



Atomic operations





The Atomics.add () static method adds the value to the current one at the specified position in the array and returns the previous value at this position. This atomic operation ensures that no other write occurs until the modified value is written back.



 Atomics.add(typedArray, index, value) : mixed
      
      









Examples



 var sab = new SharedArrayBuffer(1024); var ta = new Uint8Array(sab); Atomics.add(ta, 0, 12); //  0,   Atomics.load(ta, 0); // 12
      
      





Atomics.add () in the specification , in MDN Web Docs .



Wait and notify



The wait () and wake () methods are modeled on the basis of futexes (โ€œfast user-space mutexโ€) Linux and provide ways to wait for a moment when a certain state does not become true, and is usually used as blocking constructs.



Atomics.wait ()

Checks if the value of the array that is still present is contained in the specified position and is sleeping pending or timeout. Returns ok, not-equal, or timed-out. If the wait is not allowed in the calling agent, then it will throw an exception error (most browsers do not allow wait () in the main stream of the browser).





Optimization problems



Optimization makes code unpredictable among workers. In single threads, compilers can perform optimizations that break multithreaded code.



Take, for example, the following code:



 while (sharedArray [0] === 123);
      
      





In a single thread, the value of sharedArray [0] never changes during loop execution (if sharedArray is an array or a typed array that has not been fixed in any way). Therefore, the code can be optimized as follows:



 const tmp = sharedArray [0]; while (tmp === 123);
      
      





However, in multi-threaded mode, this optimization does not allow us to use this template to wait for changes made in another thread.



Another example is the following code:



 // main.js sharedArray [1] = 11; sharedArray [2] = 22;
      
      





In one thread, you can rearrange these write operations because nothing is read between them. Multiple threads experience problems when you expect recordings to be performed in a specific order:



 // worker.js while (sharedArray [2]! == 22); console.log (sharedArray [1]); // 0  11
      
      





These types of optimization make it almost impossible to synchronize the actions of several workers working on the same buffer with a common array.



Solving optimization problems



Using the Atomics global variable, whose methods have three main use cases.



First use case: synchronization.



Atomics methods can be used to synchronize with other workers. For example, the following two operations allow you to read and write data and are never reordered by compilers:



 Atomics.load (TypedArray <T>, index) : T Atomics.store (TypedArray <T>, index, value: T) : T
      
      





The idea is to use ordinary operations to read and write most of the data, while Atomics operations (loading, storing and others) ensure that reading and writing are safe. Often you will use your own synchronization mechanisms, such as locks, which are based on Atomics.



This is a very simple example that always works thanks to Atomics (I skipped setting sharedArray):



 // main.js console.log ('notified...'); Atomics.store (sharedArray, 0, 123); // worker.js while (Atomics.load (sharedArray, 0)! == 123); console.log ('notified');
      
      





Second use case: waiting for notification .



Using a while loop to wait for a notification is not very efficient, so Atomics has operations that help: Atomics.wait (Int32Array, index, value, timeout) and Atomics.wake (Int32Array, index, count).



Third use case: atomic operations

Some Atomics operations do arithmetic and cannot be interrupted at the same time, which helps with synchronization. For example:



 Atomics.add (TypedArray <T>, index, value) : T
      
      





Roughly speaking, this operation performs: index + = value;



Problem with torn values.



Another problem effect with shared memory is torn values โ€‹โ€‹(garbage): when reading, you can see an intermediate value - neither the value before the new value was written to memory, nor the new value.



The Tear-Free Reads section of the specification states that there are no gaps if and only if:





In other words, torn values โ€‹โ€‹are a problem when the same buffer of a shared array is accessed via:





To avoid a gap in values โ€‹โ€‹in these cases, use Atomics or sync.



Shared Array Buffers in Uses



Shared Array Buffers and JavaScript semantics for executing a pending function. JavaScript has the so-called โ€œto completionโ€ execution semantics: each function can expect that it will not be interrupted by another thread until it is completed. Functions become transactions and can execute complete algorithms, while no one sees the data with which they work in an intermediate state.



Shared Array Buffers interrupt the cycle until completion (RTC): the data the function is working on can be changed by another thread during the execution of the function. However, the code fully controls whether this RTC violation occurs: if it does not use Shared Array Buffers, it is safe.



This is roughly similar to how asynchronous functions violate RTC. There you enable the lock operation using the await keyword.



Shared Array Buffers allow emscripten to compile pthreads in asm.js. Quoting the emscripten documentation page:



[En] [Shared Array Buffers allow] Emscripten applications to share the main memory heap between web workers. This along with primitives for low level atomics and futex support enables Emscripten to implement support for the Pthreads (POSIX threads) API.



[Ru] [Shared Array Buffers allow] Emscripten applications to share a bunch of main memory between web workers. Along with low-level atomic primitives and futex support, Emscripten enables support for the Pthreads API (POSIX threads).



That is, you can compile multithreaded C and C ++ code in asm.js.



There is ongoing discussion about how best to use multithreading in WebAssembly. Given that web workers are relatively heavy, it is possible that WebAssembly will introduce lightweight threads. You can also see that topics are on their way to the future of WebAssembly.



Exchange data other than integers



At the moment, only arrays of integers (up to 32 bits long) can be used. This means that the only way to share other kinds of data is to encode them as integers. Tools that may help include:





In the end, it is likely that additional - higher-level - mechanisms for data exchange will appear. And experiments will continue to figure out what these mechanisms should look like.



How much faster does code using Shared Array buffers work?



Lars T. Hansen wrote two implementations of the Mandelbrot algorithm (as described in his article โ€œ A Taste of JavaScript's New Parallel Primitives โ€, a serial version and parallel version that uses several web workers. Up to 4 web workers and, therefore, processor cores, acceleration increases almost linearly, from 6.9 frames per second (1 web worker) to 25.4 frames per second (4 web workers) More web workers bring additional productivity improvements, but more modest ones.



Hansen notes that the accelerations are impressive, but the parallel work is due to more complex code.



Additional information about Shared Array Buffers and supporting technologies:





Other JavaScript concurrency technologies:





See Atomics Object in the official specification , as well as in the MDN Web Docs .



9. Async functions



Creating an Async function using the AsyncFunction constructor



The AsyncFunction constructor creates a new async function object. In JavaScript, any asynchronous function is actually an AsyncFunction object.



Note that AsyncFunction is not a global object. It can be obtained by executing the following code.



 Object.getPrototypeOf(async function(){}).constructor
      
      





Syntax



 new AsyncFunction([arg1[, arg2[, ...argN]],] functionBody)
      
      





arg1, arg2, ... argN - Names used by the function as formal argument names. Each name must be a string that matches a valid JavaScript identifier or a list of such strings separated by commas; for example, โ€œx,โ€ โ€œtheValue,โ€ or โ€œa, b.โ€



functionBody - A string containing the definition of a function in the JavaScript source code.



Async function objects created using the AsyncFunction constructor will be parsed when the function is created. This is less efficient than declaring an asynchronous function using async function expression and calling it inside your code, since such functions are parsed with the rest of the code.



All arguments passed to the function are treated as the names of the parameter identifiers in the created function in the order in which they are passed.



Calling the AsyncFunction constructor as a function (without using the new operator) has the same effect as calling it as a constructor.



Async functions created using the AsyncFunction constructor do not short circuit to the contexts that create them; They are always created in the global scope. When they start, they will be able to access only their local variables and global variables, but do not have access to the scopes in which the AsyncFunction constructor was called. This is different than using eval with the code for the async function.



Example of creating an async function using the AsyncFunction constructor



 function resolveAfter2Seconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(x); }, 2000); }); } var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor var a = new AsyncFunction('a', 'b', 'return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b);'); a(10, 20).then(v => { console.log(v); //  30  4  });
      
      





Async function declaration



The async function declaration defines an asynchronous function that returns an AsyncFunction object. You can also define async functions using the async function expression.



Syntax



 async function name([param[, param[, ... param]]]) { // body }
      
      





name - The name of the function.

param - The name of the argument to be passed to the function.

statements - An expression containing the function body.



After the call, the async function returns Promise. When the result has been received, Promise is completed, returning the received value. When the async function throws an exception, Promise will fail with a throws value.



The async function may contain an await expression that pauses the execution of the async function and waits for a response from the passed Promise, then resumes the async function and returns the received value.



The await keyword is valid only in asynchronous functions. In another context, you will get a SyntaxError error.



The purpose of the async / await functions is to simplify the use of promises synchronously and to reproduce some action on the Promises group. Just like Promises are like structured callbacks, async / await is like a combination of generators and promises.



Example



 function resolveAfter2Seconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(x); }, 2000); }); } async function add1(x) { const a = await resolveAfter2Seconds(20); const b = await resolveAfter2Seconds(30); return x + a + b; } add1(10).then(v => { console.log(v); //  60  4  }); async function add2(x) { const a = resolveAfter2Seconds(20); const b = resolveAfter2Seconds(30); return x + await a + await b; } add2(10).then(v => { console.log(v); //  60  2  });
      
      





What is asynchrony for?



You can write your JS program in one .js file, but for sure your code will be split into several parts. And only one of the parts will be executed now, and the rest will be executed later. Function is the most used technique of dividing a program into parts.



The main problem for most developers who see JS for the first time is the lack of understanding of what will not happen immediately after now. In other words, tasks that cannot be completed now, by definition, will end asynchronously. And we will not have the blocking behavior of the program that we assume. (You-Dont-Know-JS / async & performance, Jake Archibald).



 // ajax(..)  Ajax-,   var data = ajax( "http://some.url.1" ); console.log( data );// !  `data`     Ajax-
      
      





What is the mistake? console.log () executed before we received the data from the request.



The obvious decision to โ€œwaitโ€ from now to later is to use callbacks:



 ajax( "http://some.url.1", function myCallbackFunction(data){ console.log( data ); // ,   ! } );
      
      





Consider various methods for solving synchronous code execution prematurely.



We have 3 functions getUser, getPosts, getComments.



 const { getUser, getPosts, getComments } = require('./db'); getUser(1, (error, user) => { if(error) return console.error(error); getPosts(user.id, (error, posts) => { if(error) return console.error(error); getComments(posts[0].id, (error, comment) => { if(error) return console.error(error); console.log(comments); }); }); });
      
      





In this example, itโ€™s hard not to notice the pyramid, which increases with the addition of new functions to it. This coding style is commonly called Callback Hell . This is a certain pattern that provides you with control over competing (asynchronous) requests, which ensures the sequence of their execution.



Part of the solution to the problem of nesting functions is to use Promise (which I examined in my last article , which removes it and makes the code cleaner. They also provide a more convenient way to handle errors. But many did not like this syntax.



 getUser(1) .then(user => getPosts(user,id)) .then(posts => getComments(posts[0].id)) .then(comments => console.log(comments)) .catch(error => console.error(error));
      
      





Generators became an alternative to Promise (which I also examined in a previous article . The generator itself is not suitable for writing asynchronous code, but if you use them together with Promise, we get something unique - asynchronous code that looks synchronous. At the same time, generators provide the familiar error handling mechanism using the try ... catch construct. Only generators have one big minus - in order to use them with Promise you will need a separate function that will control the process of the generator A. You can write this function yourself or use a third-party library, for example co . In this example, I wrote my implementation of such a function.



 co(function* () { try { let user = yield getUser(1); let posts = yield getPosts(user.id); let comments = yield getComments(posts[0].id); console.log(comments); } catch (error) { console.log(error); } }); function co(generator) { const iterator = generator(); return new Promise((resolve, reject) => { function run(prev) { const { value, done } = iterator.next(prev); if (done) resolve(value); else if (value instanceof Promise) value.then(run, reject); else run(value); } run(); }); }
      
      





Each of the methods of working with asynchronous code has its advantages and disadvantages.

Callback functions (Calback functions) - Easy to use, but with an increase in nested functions, readability begins to suffer.



Promises (Promises) - Elegant and comfortable, but difficult for beginners to understand.



Generators - Allow you to write asynchronous code synchronously, but they require a separate function, and the mechanism of operation of the generators is very confusing.



Asynchronous functions were created on the basis of Promises and Generators, in order to make working with asynchronous code simple and understandable.



In order to understand what asynchronous functions are, consider the following example:



 function getUser(id) { return { id: 1 }; } let user = getUser(1); console.log(user); // { id: 1 }
      
      





Now if you make the function asynchronous (adding the async keyword), the function will return a Promise that contains an object with the id property.



 async function getUser(id) { return { id: 1 }; } let user = getUser(1); console.log(user); // Promise { {id: 1} }
      
      





Thus, we can say that any asynchronous function returns Promis (or rather wraps in Promis the value that it should return). If the value returned to the asynchronous function is already a promise, then it will not be turned around again.



To get the value from the promise, we can use the then () method.



 async function getUser(id) { return { id: 1 }; } getUser(1) .then(user => console.log(user)); // { id: 1 }
      
      





Or we can use the await keyword, which will be discussed later.



Let us return to our first example (only this time we will use the real function to send an HTTP request.



 fetch(`https://jsonplaceholder.typicode.com/users/1`) .then(data => data.json()) .then(data => console.log(data));
      
      





This is what asynchronous code looks like using Promise.

But we can write asynchronous code as synchronous if we use asynchronous functions.



 async function sendRequest() { let responce = await fetch(`https://jsonplaceholder.typicode.com/users/1`); return responce.json(); } async function main() { var a = await sendRequest(); console.log(a); } main();
      
      





The only thing I donโ€™t like is that the async operator can only be used in asynchronous functions. Otherwise, I would not need to use the main () function. Of course, you can also use the then () method, but then the code will no longer look as asynchronous.



 async function sendRequest() { let responce = await fetch(`https://jsonplaceholder.typicode.com/users/1`); return responce.json(); } sendRequest() .then((data) => console.log(data));
      
      





The bottom line is that we do not use callback functions to get data from fetch (). Instead, we use the await keyword, which, as it were, tells the runtime: wait for the fetch () function to execute and write the result to the responce variable. And using the callback function, we say: wait for the fetch () function to execute and call the callback function to process the data.



Here is the obvious difference between using Promise and async function



 //  Promise function sendRequest() { return fetch(`https://jsonplaceholder.typicode.com/users/1`) .then(data => data.json()); } //  async function async function sendRequest() { let responce = await fetch(`https://jsonplaceholder.typicode.com/users/1`); return responce.json(); }
      
      





The await operator can only be used in the body of asynchronous functions, and its action can be used on any function that returns a promise.



To handle exceptions in asynchronous functions, it is customary to use the try ... catch construct.



 async function sendRequest() { let responce = await fetch(`https://jsonplaceholder.typicode.com/users/1`); try { throw new Error("Unexpected error"); return responce.json(); } catch(error) { console.log(error); // Error: Unexpected error at sendRequest } }
      
      





And finally ...



  //    async; await; async; await; async; await; async; await; In the System(); The function.sleep()s tonight~
      
      





See Async Function Definitions in the official specification , as well as in the MDN Web Docs .



All Articles