Как стать автором
Обновить

Введение в ECMAScript 2017 (ES8)

Время на прочтение27 мин
Количество просмотров13K

Оглавление


Предисловие
Краткий обзор ES7
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

Предисловие


Здравствуйте, в прошлом я уже рассматривал нововведения в ES6 и теперь время разобрать ES8 так как он принёс много нового. Рассматривать отдельно ES7 (2016), я не стал так как этот релиз принёс всего 2 нововведения. Это Array.prototype.includes() и оператор возведения в степень. Но всё же прежде чем приступить к ES8, давайте рассмотрим нововведения из ES7.

Краткий обзор ES7


Метод includes() определяет, содержит ли массив определённый элемент, возвращая в зависимости от этого true или false.

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

searchElement — Искомый элемент.

fromIndex — Позиция в массиве, с которой начинать поиск элемента searchElement. При отрицательных значениях поиск производится начиная с индекса array.length + fromIndex по возрастанию. Значение по умолчанию равно 0.

Примеры

[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() может быть применён к другим типам объектов (например, к массивоподобным объектам). Пример: использование метода includes() на объекте arguments.

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

Оператор возведения в степень (**) возвращает степень с основанием a и натуральным показателем b. Возведение a в степень b.

a ** b

Примеры

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() возвращает массив, элементами которого являются массивы, соответствующие перечисляемому свойству пары [key, value], найденной прямо в object. Порядок свойств тот же, что и при прохождении циклом по свойствам объекта вручную.

Object.entries(obj) : Array

obj — Объект, чьи перечислимые свойства будут возвращены в виде массива [key, value].

Object.entries() возвращает свойства в том же порядке, что и в цикле for...in (разница в том, что for-in также перечисляет свойства из цепочки прототипов). Порядок элементов в массиве, который возвращается Object.entries() не зависит от того как объект объявлен. Если необходим определенный порядок, то массив должен быть отсортирован до вызова метода.

Примеры

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

Преобразование Object в Map

Конструктор new Map() принимает повторение значений. С Object.entries вы легко можете преобразовать Object в Map. Это более лаконично, чем использование массива из 2-х элементных массивов, но ключи могут быть только строками.

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

Почему возвращаемое значение Object.entries () является массивом, а не итератором?
Соответствующим прецедентом в этом случае является Object.keys (), а не, например, Map.prototype.entries ().

Почему Object.entries () возвращает только перечисляемые собственные свойства со строковыми ключами?

Опять же, это сделано для соответствия Object.keys (). Этот метод также игнорирует свойства, ключи которых являются символами. В конце концов, может существовать метод Reflect.ownEntries (), который возвращает все собственные свойства.

Смотрите object.entries в официальной спецификации, а также в MDN Web Docs.

2. Object.values


Object.values() возвращает массив, чьи элементы — это значения перечисляемых свойств найденных в объекте. Порядок такой же как если пройтись по объекту циклом вручную.

Object.values(obj) : Array

obj — Объект, чьи значения перечисляемых свойств будут возвращены.

Метод Object.values() возвращает массив значений перечисляемых свойств объекта в том же порядке что и цикл for...in. Разница между циклом и методом в том, что цикл перечисляет свойства и из цепочки прототипов.

Примеры

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']

Отличие Object.entries от Object.values() в том, что первый возвращает массив массивов содержащий название и значение свойства, тогда как второй возвращает только массив со значением свойств.

Пример отличия Object.values() и 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] ]

Смотрите Object.values() в официальной спецификации, а также в MDN Web Docs.

3. String.prototype.padEnd


Метод padEnd() дополняет текущую строку с помощью заданной строки (в конечном счете повторяя), так чтобы результирующая строка достигла заданной длины. Дополнение применяется в конце (справа) текущей строки.

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

maxLength — Длина результирующей строки, после того как текущая строка была дополнена. Если этот параметр меньше длины текущей строки, то будет возвращена текущая строка, как она есть.
fillString — Строка для дополнения текущей строки с. Если эта строка слишком длинная, она будет урезана и будет применяться ее левая большая часть. " " (0x0020 SPACE) — значение по умолчанию для этого параметра.

Примеры

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

Варианты использования для заполнения строк включают в себя:

  • Добавление счетчика или идентификатора к имени файла или URL-адресу: 'file 001.txt'
  • Выравнивание вывода консоли: «Test 001: ✓»
  • Печать шестнадцатеричных или двоичных чисел, имеющих фиксированное количество цифр: '0x00FF'

Смотрите String.prototype.padEnd в официальной спецификации, а также в MDN Web Docs.

4. String.prototype.padStart


Метод padStart() заполняет текущую строку другой строкой (несколько раз, если нужно) так, что итоговая строка достигает заданной длины. Заполнение осуществляется в начале (слева) текущей строки.

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

maxLength — Длина итоговой строки после дополнения текущей строки. Если значение меньше, чем длина текущей строки, текущая строка будет возвращена без изменений.

fillString — Строка для заполнения текущей строки. Если эта строка слишком длинная для заданной длины, она будет обрезана. Значение по умолчанию — " " (0x0020 SPACE).

Примеры

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

Почему методы заполнения не называются padLeft и padRight?

Для двунаправленных языков или языков справа налево термины «левый» и «правый» не работают. Следовательно, именование padStart и padEnd следует существующим именам начиная с startsWith и endsWith.

Смотрите String.prototype.padStart в официальной спецификации, а также в MDN Web Docs.

5. Object.getOwnPropertyDescriptor


Метод Object.getOwnPropertyDescriptor() возвращает дескриптор свойства для собственного свойства (то есть такого, которое находится непосредственно в объекте, а не получено через цепочку прототипов) переданного объекта. Если свойства не существует возвращает undefined.

Object.getOwnPropertyDescriptor(obj, prop) : Object

obj — Объект, в котором ищется свойство.

prop — Имя свойства, чьё описание будет возвращено.

Этот метод позволяет просмотреть точное описание свойства. Свойство в JavaScript состоит из строкового имени и дескриптора свойства.

Дескриптор свойства — это запись с некоторыми из следующих атрибутов:

  • value — Значение, ассоциированное со свойством (только в дескрипторе данных).
  • writable — Значение true, если значение, ассоциированное со свойством, может быть изменено, иначе false (только в дескрипторе данных).
  • get — Функция, возвращающая значение свойства, либо undefined, если такая функция отсутствует (только в дескрипторе доступа).
  • set — Функция, изменяющая значение свойства, либо undefined, если такая функция отсутствует (только в дескрипторе доступа).
  • configurable — Значение true, если тип дескриптора этого свойства может быть изменён и если свойство может быть удалено из содержащего его объекта, иначе false.
  • enumerable — Значение true, если это свойство доступно при перечислении свойств содержащего его объекта, иначе false.

Примеры

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}

Варианты использования Object.getOwnPropertyDescriptor()


Первый вариант использования: копирование свойств в объект
Начиная с ES6, в JavaScript уже есть инструментальный метод для копирования свойств: Object.assign (). Однако этот метод использует простые операции get и set для копирования свойства, ключ которого является ключом:

const value = source[key]; // get
target[key] = value; // set
Это означает, что он неправильно копирует свойства с атрибутами, отличными от заданных по умолчанию (методы получения, установки, записи и т. Д.). Следующий пример иллюстрирует это ограничение. У источника объекта есть установщик, ключ которого foo:
const source = {
    set foo(value) {
        console.log(value);
    }
};
console.log(Object.getOwnPropertyDescriptor(source, 'foo'));
// { get: undefined, set: [Function: foo], enumerable: true, configurable: true }

Использование Object.assign () для копирования свойства foo в целевой объект завершается неудачно:

const target1 = {};
Object.assign(target1, source);
console.log(Object.getOwnPropertyDescriptor(target1, 'foo'));
// { value: undefined, writable: true, enumerable: true, configurable: true }
К счастью, использование Object.getOwnPropertyDescriptors () вместе с Object.defineProperties () работает:

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

Второй вариант использования: клонирование объектов
Мелкое клонирование аналогично копированию свойств, поэтому Object.getOwnPropertyDescriptors () также является хорошим выбором здесь.

На этот раз мы используем Object.create (), который имеет два параметра:
Первый параметр указывает прототип возвращаемого объекта.

Необязательный второй параметр — это коллекция дескрипторов свойств, подобная тем, которые возвращаются Object.getOwnPropertyDescriptors ().

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

Третий вариант использования: кроссплатформенные литералы объектов с произвольными прототипами.

Синтаксически лучший способ использования литерала объекта для создания объекта с произвольным прототипом — использовать специальное свойство __proto__:

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

Увы, эта фича гарантированно присутствует только в браузерах. Общий обходной путь — Object.create () и присваивание:

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

Но вы также можете использовать Object.getOwnPropertyDescriptors ():

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

Другой альтернативой является Object.assign ():

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

Подводный камень: методы копирования, использующие super.

Метод, который использует super, прочно связан со своим домашним объектом (объектом, в котором он хранится). В настоящее время нет способа скопировать или переместить такой метод в другой объект.

Смотрите Object.getOwnPropertyDescriptor в официальной спецификации, а также в MDN Web Docs.

6. Trailing commas


Висящие запятые (Trailing commas) — могут быть полезны при добавлении новых элементов, параметров или свойств в код JavaScript. Если вы хотите добавить новое свойство, вы просто добавляете новую строчку без изменения предыдущей, если в ней уже использована висящая запятая. Это делает различия в контроле версий чище и изменение кода может быть менее хлопотным.

Висящие запятые в литералах


Массивы

JavaScript игнорирует висящие запятые в массивах:

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

Если использовано больше одной висящей запятой, будут созданы «дырки». Массив с «дырками» называется разреженным (sparse) (плотный массив не имеет «дырок»). При итерации массива при помощи, например, Array.prototype.forEach() или Array.prototype.map(), «дырки» будут пропущены.

Объекты

var object = { 
  foo: "bar", 
  baz: "qwerty",
  age: 42,
};

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

Висящие запятые в функциях


Определение параметров

Следующие определения параметров функций допустимы и равнозначны друг другу. Висящие запятые не влияют на свойство length функции или их объект arguments.

function f(p) {}
function f(p,) {} 

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

Определением методов

Висящая запятая также работает с определением методов для классов или объектов.

class C {
  one(a,) {},
  two(a, b,) {},
}

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

Вызов функци

Следующие вызововы функций допустимы и равнозначны друг другу.

f(p);
f(p,);

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

Недопустимые висящие запятые

Определение параметров функции или вызов функции, содержащих только запятую будет генерировать SyntaxError. Кроме того, при использовании оставшихся параметров не допускается использовать висящие запятые.

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 ','

Висящие запятые в деструктурировании


Висящие запятые так же можно использовать слева при использовании деструктурирующего присваивания.

// массив деструктурируется с висящей запятой
[a, b,] = [1, 2];

// объект деструктурируется с висящей запятой
var o = {
  p: 42, 
  q: true,
};
var {p, q,} = o;

Ещё раз, при использовании оставшихся параметров будет сгенерирована SyntaxError.

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

Висящие запятые в JSON


Висящие запятые в объекте допустимы только в ECMAScript 5. Так как JSON основан на синтаксисе JavaScript старше, чем ES5, висящие запятые недопускаются в JSON.

Обе строки генерируют SyntaxError

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

Почему висящие запятые полезны?


Есть два преимущества.

Во-первых, переставить элементы проще, потому что вам не нужно добавлять и удалять запятые, если последний элемент меняет свою позицию.

Во-вторых, это помогает системам контроля версий отслеживать, что действительно изменилось. Например, из:

[
    'Foo'
]
в:
[
    'Foo',
    'бар'
]

приводит к тому, что и строка с 'foo', и строка с 'bar' помечаются как измененные, хотя единственное реальное изменение — это добавление последней строки.

Смотрите Trailing commas в MDN Web Docs.

7. SharedArrayBuffer


Объект SharedArrayBuffer используется для создания разделенного буфера фиксированной длины для хранения примитивных бинарных данных, подобно объекту ArrayBuffer, но в отличие от него экземпляры SharedArrayBuffer могут быть использованы для создания view на разделенную память. SharedArrayBuffer не может быть отсоединен.
new SharedArrayBuffer(length) : Object
length — Размер, в байтах, для создания буферного массива.

return — Новый объект SharedArrayBuffer указаной длины. Его содержимое после инициализации равно 0.

Для разделения памяти с помощью объекта SharedArrayBuffer между одним агентом в кластере и другим (агент может быть, как основной программой web-страницы, так и одним из web-workers), используются postMessage и structured cloning.

Алгоритм структурированного клонирования принимает SharedArrayBuffers и TypedArrays, отображенный в SharedArrayBuffers. В обоих случаях, объект SharedArrayBuffer передается получателю, в результате чего создается новый приватный объект SharedArrayBuffer внутри агента-получателя (так же как для ArrayBuffer). Однако, блок общих данных, на который ссылаются оба объекта SharedArrayBuffer, это один и тот же блок данных, и сторонние эффекты в блоке в одном из агентов в итоге станут заметны в другом агенте.

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

Разделенная память может быть создана и изменена одновременно в workers или основном потоке. В зависимости от системы (ЦПУ, ОС, браузер) может уйти время пока изменения будут распространены по всем контекстам. Для синхронизации необходимы атомарные операции.

Shared Array Buffers — это примитивный строительный блок для абстракций параллелизма более высокого уровня. Они позволяют вам совместно использовать байты объекта SharedArrayBuffer между несколькими рабочими и основным потоком (буфер совместно используется, чтобы получить доступ к байтам, обернуть его в Typed Array). Этот вид обмена имеет два преимущества:
Вы можете быстрее обмениваться данными между workers.

Координация между workers становится проще и быстрее (по сравнению с postMessage ()).

Реализация worker'а выглядит следующим образом.

// worker.js

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

     // ···
});

Сначала мы извлекаем буфер общего массива, который был отправлен нам, а затем оборачиваем его в типизированный массив, чтобы мы могли использовать его локально.

Свойства и методы SharedArrayBuffer.

SharedArrayBuffer.length — Длина конструктора SharedArrayBuffer, чье значение равно 1.
SharedArrayBuffer.prototype — Позволяет дополнительные свойства для всех объектов SharedArrayBuffer.

Экземпляры SharedArrayBuffer
Свойства

SharedArrayBuffer.prototype.constructor — Определяет функцию, которая создает прототип объекта. Начальное значение — стандартный встроенный конструктор SharedArrayBuffer.

SharedArrayBuffer.prototype.byteLength (Read only) — Размер массива в байтах. Это устанавливается при создании массива и не может быть изменено.

Методы

SharedArrayBuffer.prototype.slice() — Возвращает новый SharedArrayBuffer, содержимое которого является копией байтов этого SharedArrayBuffer от начала, включительно, до конца, эксклюзива. Если начало или конец отрицательны, это относится к индексу с конца массива, а не с начала. Этот метод имеет тот же алгоритм, что и 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 — Нулевой индекс, с которого начинается извлечение. Можно использовать отрицательный индекс, указывающий смещение от конца последовательности. slice (-2) извлекает последние два элемента в последовательности. Если начало не определено, срез начинается с индекса 0.
End — Индекс на основе нуля, до которого нужно завершить извлечение.

Например, slice(1,4) извлекает второй элемент через четвертый элемент (элементы с индексами 1, 2 и 3). Можно использовать отрицательный индекс, указывающий смещение от конца последовательности. slice(2, -1) извлекает третий элемент через второй-последний элемент в последовательности. Если end пропущен, slice извлекает через конец последовательности (sab.byteLength).

Примеры

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 }

Смотрите SharedArrayBuffer в официальной спецификации, а также в MDN Web Docs.

8. Atomics


Объект Atomics предоставляет атомарные операции как статические методы. Используется вместе с объектом SharedArrayBuffer.

Атомарные операции установлены в модуле Atomics. В отличие от других глобальных объектов, Atomics не является конструктором. Его нельзя использовать вместе с оператором new или вызывать объект Atomics как функцию. Все свойства и методы Atomics статические (как у объекта Math, к примеру).

Когда память разделена, несколько потоков могут читать и записывать одни и те же данные в память. Атомарные операции гарантируют, что ожидаемые значения будут записаны и прочитаны, а операции завершены, прежде чем следующая операция начнет свою работу, и они не будут прерваны.

Свойства


Atomics[Symbol.toStringTag] — Значение этого свойства — «Atomics».

Методы


Атомарные операции

  • Atomics.add() — Добавляет представленное значение к текущему по указанной позиции в массиве. Возвращает предыдущее значение в этой позиции.
  • Atomics.and() — Вычисляет побитовое AND в указанной позиции массива. Возвращает предыдущее значение в этой позиции.
  • Atomics.compareExchange() — Сохраняет представленное значение в указанную позицию массива, если оно эквивалентно представленному значению. Возвращает предыдущее значение.
  • Atomics.exchange() — Сохраняет представленное значение в указанную позицию массива. Возвращает предыдущее значение.
  • Atomics.load() — Возвращает значение из указной позиции массива.
  • Atomics.or() — Вычисляет побитовое OR в указанной позиции массива. Возвращает предыдущее значение в этой позиции.
  • Atomics.store() — Сохраняет представленное значение в указанную позицию массива. Возвращает значение.
  • Atomics.sub() — Вычитает представленное значение из текущего по указанной позиции в массиве. Возвращает предыдущее значение в этой позиции.
  • Atomics.xor() — Вычисляет побитовое XOR в указанной позиции массива. Возвращает предыдущее значение в этой позиции.

Статический метод Atomics.add() добавляет значение к текущему по указанной позиции в массиве и возвращает предыдущее значение в этой позиции. Эта атомарная операция гарантирует, что никакой другой записи не произойдет, пока измененное значение не будет записано обратно.

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

  • typedArray — Разделенный массив целых чисел. Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array или Uint32Array.
  • index — Позиция в typedArray для добавления value.
  • value — Число для добавления.
  • return — Предыдущее значение в указанной позиции (typedArray[index]).

  • Выбрасывает TypeError, если тип typedArray не является одним из допустимых целочисленных типов.
  • Выбрасывает TypeError, если тип typedArray не общего типа.
  • Выбрасывает RangeError, если index вне typedArray.

Примеры

var sab = new SharedArrayBuffer(1024);
var ta = new Uint8Array(sab);

Atomics.add(ta, 0, 12); // возвращает 0, предыдущее значение
Atomics.load(ta, 0); // 12

Atomics.add() в спецификации, в MDN Web Docs.

Wait и notify


wait() и wake() методы моделируются на основе futexes («fast user-space mutex» — быстрый mutex пользовательского пространства) Linux и предоставляют собой способы ожидания момента, когда определенное состояние не станет true, и обычно используется как блокирующие конструкции.

Atomics.wait()
Проверяет, содержится в указанной позиции массива все еще представленное значение и спит в ожидании или тайм-аут. Возвращает «ok», «not-equal» или «timed-out». Если ожидание не разрешено в вызывающем агенте, тогда выбросит ошибку исключения (большинство браузеров не разрешают wait() в главном потоке браузера).

  • Atomics.wait() — Проверяет, содержится в указанной позиции массива все еще представленное значение и спит в ожидании или тайм-аут. Возвращает «ok», «not-equal» или «timed-out». Если ожидание не разрешено в вызывающем агенте, тогда выбросит ошибку исключения (большинство браузеров не разрешают wait() в главном потоке браузера).
  • Atomics.wake() — Пробуждает некоторых агентов, которые спят в очереди ожидания в указанной позиции массива. Возвращает количество агентов, которые были разбужены.
  • Atomics.isLockFree(size) — Оптимизационный примитив, который может быть использован для определения использовать ли блокирующие операции или атомарные. Возвращает true, если атомарные операции над массивами с указанным размерами элементов будут выполнены с использованием аппаратных атомарных операций (как противоположность блокирующим). Только для специалистов.

Проблемы оптимизации


Оптимизация делает код непредсказуемым среди workers. В одиночных потоках компиляторы могут выполнять оптимизацию, которая нарушает многопоточный код.

Взять, к примеру, следующий код:

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

В одном потоке значение sharedArray [0] никогда не изменяется во время выполнения цикла (если sharedArray — это массив или типизированный массив, который каким-либо образом не был исправлен). Поэтому код можно оптимизировать следующим образом:

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

Однако в многопоточном режиме эта оптимизация не позволяет нам использовать этот шаблон для ожидания изменений, внесенных в другом потоке.

Другой пример — следующий код:

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

В одном потоке вы можете переставить эти операции записи, потому что между ними ничего не читается. Для нескольких потоков возникают проблемы, когда вы ожидаете, что записи будут выполняться в определенном порядке:

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

Эти виды оптимизации делают практически невозможным синхронизацию действий нескольких workers, работающих на одном буфере с общим массивом.

Решение проблем оптимизации


Использование глобальной переменной Atomics, методы которой имеют три основных варианта использования.

Первый вариант использования: синхронизация.

Методы Atomics могут быть использованы для синхронизации с другими workers. Например, следующие две операции позволяют вам читать и записывать данные и никогда не переупорядочиваются компиляторами:

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

Идея состоит в том, чтобы использовать обычные операции для чтения и записи большинства данных, в то время как операции Atomics (загрузка, хранение и другие) гарантируют, что чтение и запись будут выполнены безопасно. Часто вы будете использовать собственные механизмы синхронизации, такие как блокировки, реализация которых основана на Atomics.

Это очень простой пример, который всегда работает, благодаря Atomics (я пропустил настройку sharedArray):

// main.js
console.log ('notified...');
Atomics.store (sharedArray, 0, 123);

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

Второй вариант использования: ожидание уведомления.

Использование цикла while для ожидания уведомления не очень эффективно, поэтому в Atomics есть операции, которые помогают: Atomics.wait (Int32Array, index, value, timeout) и Atomics.wake (Int32Array, index, count).

Третий вариант использования: atomic operations
Некоторые операции Atomics выполняют арифметику и не могут быть прерваны при этом, что помогает с синхронизацией. Например:

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

Грубо говоря, эта операция выполняет: index += value;

Проблема c порванными значениями.

Еще один проблемный эффект с разделяемой памятью — порванные значения (мусор): при чтении вы можете увидеть промежуточное значение — ни значение до нового значения не было записано в память, ни новое значение.

В разделе «Tear-Free Reads» в спецификации указано, что разрывов нет, если и только если:

  • Как чтение, так и запись происходит через Typed Arrays (не DataViews).
  • Оба типизированных массива выровнены со своими буферами общего массива: sharedArray.byteOffset% sharedArray.BYTES_PER_ELEMENT === 0
  • Оба типизированных массива имеют одинаковое количество байтов на элемент.

Другими словами, порванные значения являются проблемой, когда к одному и тому же буферу общего массива обращаются через:

  • Один или несколько DateViews;
  • Есть один или несколько не выровненных типизированных массивов;
  • Типизированные массивы с различными размерами элементов;

Чтобы избежать разрыва значений в этих случаях, используйте Atomics или синхронизируйте.

Shared Array Buffers в использований


Shared Array Buffers и семантика JavaScript для выполнения функции «до завершения». JavaScript имеет так называемую семантику выполнения «до завершения»: каждая функция может рассчитывать на то, что она не будет прервана другим потоком до ее завершения. Функции становятся транзакциями и могут выполнять полные алгоритмы, при этом никто не видит данные, с которыми они работают, в промежуточном состоянии.

Shared Array Buffers прерывают цикл до завершения (RTC): данные, над которыми работает функция, могут быть изменены другим потоком во время выполнения функции. Однако код полностью контролирует, происходит ли это нарушение RTC: если он не использует Shared Array Buffers, это безопасно.

Это примерно похоже на то, как асинхронные функции нарушают RTC. Там вы включаете операцию блокировки с помощью ключевого слова await.

Shared Array Buffers позволяют emscripten компилировать pthreads в asm.js. Цитирование страницы документации emscripten:

[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 позволяют] Приложениям Emscripten разделять кучу основной памяти между веб-работниками. Наряду с примитивами для атомарного уровня низкого уровня и поддержкой futex позволяет Emscripten реализовать поддержку API Pthreads (потоков POSIX).

То есть вы можете скомпилировать многопоточный код C и C ++ в asm.js.

Продолжается дискуссия о том, как лучше всего использовать многопоточность в WebAssembly. Учитывая, что web workers относительно тяжелые, возможно, что WebAssembly представит облегченные потоки. Вы также можете видеть, что темы находятся на пути к будущему WebAssembly.

Обмен данными, отличными от целых чисел


На данный момент могут использоваться только массивы целых чисел (длиной до 32 бит). Это означает, что единственный способ поделиться другими видами данных — это кодировать их как целые числа. Инструменты, которые могут помочь, включают в себя:

  • TextEncoder и TextDecoder: первый преобразует строки в экземпляры Uint8Array, последний делает наоборот.
  • stringview.js: библиотека, которая обрабатывает строки как массивы символов. Использует массив буферов.
  • FlatJS: расширяет JavaScript за счет способов хранения сложных структур данных (структур, классов и массивов) в плоской памяти (ArrayBuffer и SharedArrayBuffer). JavaScript + FlatJS скомпилирован в простой JavaScript. JavaScript диалекты (TypeScript и т. Д.) Поддерживаются.
  • TurboScript: это JavaScript-диалект для быстрого параллельного программирования. Он компилируется в asm.js и WebAssembly.

В конце концов, вероятно, появятся дополнительные — более высокого уровня — механизмы для обмена данными. И эксперименты будут продолжать выяснять, как должны выглядеть эти механизмы.

Насколько быстрее работает код, использующий буферы Shared Array?

Ларс Т. Хансен написал две реализации алгоритма Мандельброта (как описано в его статье "A Taste of JavaScript’s New Parallel Primitives”», последовательная версия и параллельная версия, которая использует несколько web workers. До 4 web workers и, следовательно, процессорных ядер, ускорение увеличивается почти линейно, с 6,9 кадра в секунду (1 web worker) до 25,4 кадра в секунду (4 web workers). Больше web workers приносят дополнительные улучшения производительности, но более скромные.

Хансен отмечает, что ускорения впечатляют, но параллельная работа происходит за счет более сложного кода.

Дополнительная информация о Shared Array Buffers и поддерживающих технологиях:


Другие технологии JavaScript, связанные с параллелизмом:

  • The Path to Parallel JavaScript” by Dave Herman [общий обзор того, куда JavaScript движется после отказа от PJS]
  • Write massively-parallel GPU code for the browser with WebGL” by Steve Sanderson [захватывающий доклад, объясняющий, как заставить WebGL выполнять вычисления для вас на GPU].
  • Concurrency is not parallelism” by Rob Pike [Rob Pike использует термины «concurrency» и «parallelism» предоставляя интересное дополнительное представление].
  • "Using Web Workers" from MDN [документация от MDN, показывающая как вы множите использовать web workers].
  • "Shared memory and atomics" by Axel Rauschmayer [Отличная статья описывающая использование параллелизма js с помощью Shared Array Buffers и Atomics]

Смотрите Atomics Object в официальной спецификации, а также в MDN Web Docs.

9. Async functions


Создание Async function с помощью AsyncFunction constructor


Конструктор AsyncFunction создает новый объект async function. В JavaScript любая асинхронная функция фактически является объектом AsyncFunction.

Обратите внимание, что AsyncFunction не является глобальным объектом. Её можно получить, выполнив следующий код.

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

Синтаксис

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

arg1, arg2,… argN — Имена, используемые функцией как формальные имена аргументов. Каждое имя должно быть строкой, которая соответствует действительному идентификатору JavaScript или списку таких строк, разделенных запятой; например, «x», «theValue», или «a,b».

functionBody — Строка, содержащая в себе определение функции в исходном коде JavaScript.

Объекты async function, созданные с помощью AsyncFunction constructor будут распарсены в момент, когда функция создается. Это менее эффективно, чем объявлять асинхронную функцию с помощью async function expression и вызывать ее внутри вашего кода, поскольку такие функции анализируются с остальной частью кода.

Все аргументы, переданные функции, рассматриваются как имена идентификаторов параметров в создаваемой функции в том порядке, в котором они передаются.

Вызов AsyncFunction constructor как функции (без использования оператора new) имеет тот же эффект, что и вызов его как конструктора.

Объекты async functions созданные с помощью AsyncFunction constructor, не создают замыкания на создающие их контексты; Они всегда создаются в глобальной области видимости. При их запуске они смогут получить доступ только к своим локальным переменным и к глобальным переменным, но не имеют доступа к тем областям видимости, в которых был вызван AsyncFunction constructor. Это отличается от использования eval с кодом для async function.

Пример создание async function с помощью 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


Объявление async function определяет асинхронную функцию, которая возвращает объект AsyncFunction. Вы также можете определить async-функции, используя выражение async function.

Синтаксис

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

name — Имя функции.
param — Имя аргумента, который будет передан в функцию.
statements — Выражение, содержащее тело функции.

После вызова функция async возвращает Promise. Когда результат был получен, Promise завершается, возвращая полученное значение. Когда функция async выбрасывает исключение, Promise ответит отказом с выброшенным (throws) значением.

Функция async может содержать выражение await, которое приостанавливает выполнение функции async и ожидает ответа от переданного Promise, затем возобновляя выполнение функции async и возвращая полученное значение.

Ключевое слово await допустимо только в асинхронных функциях. В другом контексте вы получите ошибку SyntaxError.

Цель функций async/await упростить использование promises синхронно и воспроизвести некоторое действие над группой Promises. Точно так же как Promises подобны структурированным callback-ам, async/await подобна комбинации генераторов и promises.

Пример

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 секунды
});

Для чего нужна асинхронность?


Вы можете написать свою JS-программу в одном .js файле, но наверняка ваш код будет разбит на несколько частей. И только одна из частей будет выполняться сейчас, а оставшаяся часть выполнится потом. Функция является наиболее используемым приемом деления программы на части.

Основная проблема большинства разработчиков, которые впервые видят JS — это непонимание того, что потом не произойдет немедленно после сейчас. Другими словами, задачи, которые не могут быть завершены сейчас, по определению, будут завершаться асинхронно. И у нас не будет блокирующего поведения программы, которое мы предполагаем. (You-Dont-Know-JS/async & performance, Jake Archibald).

// ajax(..) некоторая Ajax-функция, предоставляемая библиотекой
var data = ajax( "http://some.url.1" );

console.log( data );// Упс! в `data` не будут записаны результаты Ajax-операции

В чём тут ошибка? console.log() выполнилось раньше, чем мы получили данные с запроса.

Очевидным решением «подождать» от сейчас до потом, это использование колбэков:

ajax( "http://some.url.1", function myCallbackFunction(data){

	console.log( data ); // Даа, я получил данные!

} );

Рассмотрим различные методы решения выполнения синхронного кода преждевремеонно.

У нас есть 3 функции 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);
		});
	});
});

В данном примере тяжело не заметить пирамиду, которая увеличивается с добавлением новых функции в неё. Этот стиль написания кода принято называть Callback Hell. Это некий паттерн, предоставляющий вам управления конкурирующими (асинхронными) запросами, который обеспечивает последовательность их выполнения.

От части решение проблемы вложенности функции является использование Promise (которые я рассматривал в своей прошлой статье, которые её убирают и делают код более чистым. Также они предоставляют более удобный способ обработки ошибок. Но многим такой синтаксис пришёлся не по душе.

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

Альтернативой Promise стали Generators (которые я тоже уже рассматривал в прошлой статье. Сами по себе генератор не приспособлены для написания асинхронного кода, но если использовать их вместе с Promise, то мы получаем нечто уникальное — асинхронный код, который выглядит синхронно. При этом генераторы предоставляют знакомый механизм обработки ошибок с помощью конструкции try...catch. Только у генераторов есть один большой минус — для того что бы использовать их с Promise вам понадобится отдельная функция, которая будет управлять процессом работы генератора. Эту функцию вы можите написать сами или использовать стороннюю библиотеку, к примеру co. В данном примере я написал свою реализацию такой функции.

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();
	});
}

У каждого из способов работы с асинхронным кодом есть свой преимущества и недостатки.
Функции обратного вызова (Calback functions) — Просты в использовании, но с увеличением вложенных функции удобочитаемость начинает страдать.

Обещания (Promises) — Элегантны и удобны, но трудны для понимания начинающим.

Генераторы (Generators) — Позволяют писать асинхронный код синхронно, но для них требуется отдельная функция, да и сам механизм работы генераторов весьма запутан.

Асинхронные функции были созданы на основе обещании (Promises) и генераторов (Generators), для того, чтобы сделать работу с асинхронным кодом простой и понятной.

Для того что бы понять, что из себя представляют асинхронные функции, рассмотрим следующий пример:

function getUser(id) {
	return { id: 1 };
}

let user = getUser(1);

console.log(user); // { id: 1 }

Теперь если сделать функцию асинхронной (добавив ключевое слово async), то функция вернёт Promise который содержит объект со своиством id.

async function getUser(id) {
	return { id: 1 };
}

let user = getUser(1);

console.log(user); // Promise { {id: 1} }

Таким образом, можно сказать, что любая асинхронная функция возвращает Promis (или скорее оборачивает в Promis значение которое она должна вернуть). Если возвращаемое асинхронной функции значение уже и так является обещанием, то повторно оно оборачиваться не будет.

Для получения значения из обещания мы можем использовать метод then().

async function getUser(id) {

	return { id: 1 };
}

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

Или мы можем использовать ключевое слово await которое будет рассмотрено далее.

Вернёмся к нашему первому примеру (только на этот раз будем использовать настоящую функцию для отправки HTTP запроса.

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

Именно так выглядит асинхронный код с использованием Promise.
Но мы можем писать асинхронный код как синхронный, если будем использовать асинхронные функции.

async function sendRequest() {
	let response= await fetch(`https://jsonplaceholder.typicode.com/users/1`);

	return response.json();
}

async function main() {
	var a = await sendRequest();

	console.log(a);
}

main();

Единственное что мне не нравится так это то что оператор async можно использовать только в асинхронных функциях. Иначе мне бы не понадобилась использовать функцию main(). Конечно вы также можете использовать метод then(), но тогда код уже не будет выглядеть как асинхронный.

async function sendRequest() {
	let response= await fetch(`https://jsonplaceholder.typicode.com/users/1`);

	return response.json();
}

sendRequest()
	.then((data) => console.log(data));

Суть в том что мы не используем колбэк функции для получения данных из fetch(). Вместо этого мы используем ключевое слово await, которое как бы говорит среде выполнения: подожди выполнения функции fetch() и запиши результат в переменную response. А с использованием колбэк функции мы говорим: подожди выполнения функции fetch() и вызови функцию колбэка для обработки данных.

Вот вам очевидное отличие использования Promise и async function

// Использование Promise 
function sendRequest() {
        return fetch(`https://jsonplaceholder.typicode.com/users/1`)
		.then(data => data.json());
}

// Использование async function
async function sendRequest() {
	let response = await fetch(`https://jsonplaceholder.typicode.com/users/1`);

	return response.json();
}

Оператор await можно использовать только в теле асинхронных функции, а использовать его действие можно на любой функции возвращающей обещание.

Для обработки исключений в асинхронных функциях принято использовать конструкцию try...catch.

async function sendRequest() {
	let response = await fetch(`https://jsonplaceholder.typicode.com/users/1`);
	try {	
	throw new Error("Unexpected error");

	return response.json();
	} catch(error) {
		console.log(error); // Error: Unexpected error at sendRequest
	}
}

Ну и напоследок…

 // Читать с выражением
async; await;
async; await;
async; await;
async; await;
In the System();
The function.sleep()s tonight~

Смотрите Async Function Definitions в официальной спецификации, а также в MDN Web Docs.
Теги:
Хабы:
Всего голосов 11: ↑9 и ↓2+7
Комментарии4

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань