This commit is contained in:
Ilya Kantor 2015-07-13 16:25:17 +03:00
parent 549af795e0
commit c131e5a895
145 changed files with 412 additions and 162 deletions

View file

@ -1,12 +1,12 @@
# Set, Map, WeakSet и WeakMap # Set, Map, WeakSet и WeakMap
Новые типы коллекций в JavaScript: `Set`, `Map`, `WeakSet` и `WeakMap`. В ES-2015 появились новые типы коллекций в JavaScript: `Set`, `Map`, `WeakSet` и `WeakMap`.
## Map ## Map
`Map` -- коллекция для хранения записей вида `ключ: значение`. `Map` -- коллекция для хранения записей вида `ключ:значение`.
В отличие от объектов, в которых ключами могут быть только строки, в `Map` ключом может быть произвольное значение, например: В отличие от объектов, в которых ключами могут быть только строки, в `Map` ключом может быть произвольное значение, например:
@ -16,23 +16,32 @@
let map = new Map(); let map = new Map();
map.set('1', 'str1'); // строка map.set('1', 'str1'); // ключ-строка
map map.set(1, 'num1'); // число
.set(1, 'num1') // число map.set(true, 'bool1'); // булевое значение
.set(true, 'bool1'); // булевое
// в обычном объекте это было бы одно и то же // в обычном объекте это было бы одно и то же,
// map сохраняет тип ключа
alert( map.get(1) ); // 'num1' alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1' alert( map.get('1') ); // 'str1'
alert( map.size ); // 3 alert( map.size ); // 3
``` ```
Как видно из примера выше, для сохранения и чтения значений используются методы `get` и `set`, причём `set` можно чейнить. И ключи и значения сохраняются "как есть", без преобразований типов. Как видно из примера выше, для сохранения и чтения значений используются методы `get` и `set`. И ключи и значения сохраняются "как есть", без преобразований типов.
Свойство `map.size` хранит общее количество записей в `map`. Свойство `map.size` хранит общее количество записей в `map`.
**При создании `Map` можно сразу инициализовать списком значений.** Метод `set` можно чейнить:
```js
map
.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
```
При создании `Map` можно сразу инициализовать списком значений.
Объект `map` с тремя ключами, как и в примере выше: Объект `map` с тремя ключами, как и в примере выше:
@ -44,9 +53,9 @@ let map = new Map([
]); ]);
``` ```
Аргументом `new Map` должен быть итерируемый объект (не обязательно именно массив), которые должен возвратить объект с ключами `0`,`1` -- также не обязательно массив. Везде утиная типизация, максимальная гибкость. Аргументом `new Map` должен быть итерируемый объект (не обязательно именно массив). Везде утиная типизация, максимальная гибкость.
**В качестве ключей можно использовать и объекты:** **В качестве ключей `map` можно использовать и объекты:**
```js ```js
//+ run //+ run
@ -54,6 +63,7 @@ let map = new Map([
let user = { name: "Вася" }; let user = { name: "Вася" };
// для каждого пользователя будем хранить количество посещений
let visitsCountMap = new Map(); let visitsCountMap = new Map();
*!* *!*
@ -65,12 +75,12 @@ alert( visitsCountMap.get(user) ); // 123
``` ```
[smart header="Как map сравнивает ключи"] [smart header="Как map сравнивает ключи"]
Для проверки значений на эквивалентность используется алгоритм [SameValueZero](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-samevaluezero). Он аналогичен строгому равенству `===`, отличие -- в том, что `NaN` считается равным `NaN`. Для проверки значений на эквивалентность используется алгоритм [SameValueZero](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-samevaluezero). Он аналогичен строгому равенству `===`, отличие -- в том, что `NaN` считается равным `NaN`. Поэтому значение `NaN` также может быть использовано в качестве ключа.
Этот алгоритм жёстко фиксирован в стандарте, его нельзя изменять или задавать свою функцию для него. Этот алгоритм жёстко фиксирован в стандарте, его нельзя изменять или задавать свою функцию сравнения.
[/smart] [/smart]
Для удаления записей: Методы для удаления записей:
<ul> <ul>
<li>`map.delete(key)` удаляет запись с ключом `key`, возвращает `true`, если такая запись была, иначе `false`.</li> <li>`map.delete(key)` удаляет запись с ключом `key`, возвращает `true`, если такая запись была, иначе `false`.</li>
<li>`map.clear()` -- удаляет все записи, очищает `map`.</li> <li>`map.clear()` -- удаляет все записи, очищает `map`.</li>
@ -82,22 +92,9 @@ alert( visitsCountMap.get(user) ); // 123
<li>`map.has(key)` -- возвращает `true`, если ключ есть, иначе `false`.</li> <li>`map.has(key)` -- возвращает `true`, если ключ есть, иначе `false`.</li>
</ul> </ul>
Ещё раз заметим, что используемый алгоритм сравнения ключей аналогичен `===`, за исключением `NaN`, которое равно самому себе:
```js
//+ run
'use strict';
let map = new Map([ [NaN: 1] ]);
alert( map.has(NaN) ); // true
alert( map.get(NaN) ); // 1
alert( map.delete(NaN) ); // true
```
### Итерация ### Итерация
Для итерации используется один из трёх методов: Для итерации по `map` используется один из трёх методов:
<ul> <ul>
<li>`map.keys()` -- возвращает итерируемый объект для ключей,</li> <li>`map.keys()` -- возвращает итерируемый объект для ключей,</li>
<li>`map.values()` -- возвращает итерируемый объект для значений,</li> <li>`map.values()` -- возвращает итерируемый объект для значений,</li>
@ -116,14 +113,17 @@ let recipeMap = new Map([
['сметаны', '50 гр'] ['сметаны', '50 гр']
]); ]);
// цикл по ключам
for(let fruit of recipeMap.keys()) { for(let fruit of recipeMap.keys()) {
alert(fruit); // огурцов, помидоров, сметаны alert(fruit); // огурцов, помидоров, сметаны
} }
// цикл по значениям [ключ,значение]
for(let amount of recipeMap.values()) { for(let amount of recipeMap.values()) {
alert(amount); // 500 гр, 350 гр, 50 гр alert(amount); // 500 гр, 350 гр, 50 гр
} }
// цикл по записям
for(let entry of recipeMap) { // то же что и recipeMap.entries() for(let entry of recipeMap) { // то же что и recipeMap.entries()
alert(entry); // огурцов,500 гр , и т.д., массивы по 2 значения alert(entry); // огурцов,500 гр , и т.д., массивы по 2 значения
} }
@ -150,19 +150,11 @@ recipeMap.forEach( (value, key, map) => {
}); });
``` ```
У `Map` есть и другие свой методы:
<ul>
<li>`map.size()` -- возвращает количество записей,</li>
<li>`map.clear()` -- удаляет все записи.</li>
</ul>
## Set ## Set
`Set` -- коллекция для хранения множества значений, причём каждое значение может встречаться лишь один раз. `Set` -- коллекция для хранения множества значений, причём каждое значение может встречаться лишь один раз.
Например, к нам приходят посетители, и мы хотели бы сохранять всех, кто пришёл. Повторные визиты не должны приводить к дубликатам. Например, к нам приходят посетители, и мы хотели бы сохранять всех, кто пришёл. При этом повторные визиты не должны приводить к дубликатам, то есть каждого посетителя нужно "посчитать" ровно один раз.
`Set` для этого отлично подходит: `Set` для этого отлично подходит:
@ -176,13 +168,14 @@ let vasya = {name: "Вася"};
let petya = {name: "Петя"}; let petya = {name: "Петя"};
let dasha = {name: "Даша"}; let dasha = {name: "Даша"};
// посещения // посещения, некоторые пользователи заходят много раз
set.add(vasya); set.add(vasya);
set.add(petya); set.add(petya);
set.add(dasha); set.add(dasha);
set.add(vasya); set.add(vasya);
set.add(petya); set.add(petya);
// set сохраняет только уникальные значения
alert( set.size ); // 3 alert( set.size ); // 3
set.forEach( user => alert(user.name ) ); // Вася, Петя, Даша set.forEach( user => alert(user.name ) ); // Вася, Петя, Даша
@ -190,12 +183,12 @@ set.forEach( user => alert(user.name ) ); // Вася, Петя, Даша
В примере выше многократные добавления одного и того же объекта в `set` не создают лишних копий. В примере выше многократные добавления одного и того же объекта в `set` не создают лишних копий.
Альтернатива `Set` -- это массивы с поиском дубликата при каждом добавлении, но это гораздо хуже по производительности. Или же объекты, где в качестве ключа выступает какой-нибудь уникальный идентификатор посетителя. Но это менее удобно, чем простой и наглядный `Set`. Альтернатива `Set` -- это массивы с поиском дубликата при каждом добавлении, но они гораздо хуже по производительности. Или можно использовать обычные объекты, где в качестве ключа выступает какой-нибудь уникальный идентификатор посетителя. Но это менее удобно, чем простой и наглядный `Set`.
Основные методы: Основные методы:
<ul> <ul>
<li>`set.add(item)` -- добавляет в коллекцию `item`, возвращает `set` (чейнится).</li> <li>`set.add(item)` -- добавляет в коллекцию `item`, возвращает `set` (чейнится).</li>
<li>`set.delete(item) -- удаляет `item` из коллекции, возвращает `true`, если он там был, иначе `false`.</li> <li>`set.delete(item)` -- удаляет `item` из коллекции, возвращает `true`, если он там был, иначе `false`.</li>
<li>`set.has(item)` -- возвращает `true`, если `item` есть в коллекции, иначе `false`.</li> <li>`set.has(item)` -- возвращает `true`, если `item` есть в коллекции, иначе `false`.</li>
<li>`set.clear()` -- очищает `set`.</li> <li>`set.clear()` -- очищает `set`.</li>
</ul> </ul>
@ -214,17 +207,17 @@ set.forEach((value, valueAgain, set) => {
}); });
``` ```
Заметим, что в `Map` у функции в `.forEach` три аргумента: ключ, значение, объект `map`. Заметим, что в `Set` у функции в `.forEach` три аргумента: значение, ещё раз значение, и затем сам перебираемый объект `set`. При этом значение повторяется в аргументах два раза.
В `Set` для совместимости с `Map` сделано похожим образом -- у `.forEach`-функции также три аргумента. Но первые два всегда совпадают и содержат очередное значение множества. Так сделано для совместимости с `Map`, где у `.forEach`-функции также три аргумента. Но в `Set` первые два всегда совпадают и содержат очередное значение множества.
## WeakMap и WeakSet ## WeakMap и WeakSet
`WeakSet` -- особый вид `Set` не препятствующий сборщику мусора. То же самое -- `WeakMap` для `Map`. `WeakSet` -- особый вид `Set` не препятствующий сборщику мусора удалять свои элементы. То же самое -- `WeakMap` для `Map`.
То есть, если некий объект присутствует только в `WeakSet/WeakMap` -- он удаляется из памяти. То есть, если некий объект присутствует только в `WeakSet/WeakMap` -- он удаляется из памяти.
Это нужно для тех ситуаций, когда сами объекты используются где-то в другом месте кода, а здесь мы хотим хранить для них "вспомогательные" данные, существующие лишь пока жив объект. Это нужно для тех ситуаций, когда основное место для хранения и использования объектов находится где-то в другом месте кода, а здесь мы хотим хранить для них "вспомогательные" данные, существующие лишь пока жив объект.
Например, у нас есть элементы на странице или, к примеру, пользователи, и мы хотим хранить для них вспомогательную инфомацию, например обработчики событий или просто данные, но действительные лишь пока объект, к которому они относятся, существует. Например, у нас есть элементы на странице или, к примеру, пользователи, и мы хотим хранить для них вспомогательную инфомацию, например обработчики событий или просто данные, но действительные лишь пока объект, к которому они относятся, существует.
@ -260,6 +253,8 @@ activeUsers.splice(0, 1); // Петя более не активный поль
// weakMap теперь содержит только 1 элемент // weakMap теперь содержит только 1 элемент
``` ```
Таким образом, `WeakMap` избавляет нас от необходимости вручную удалять вспомогательные данные, когда удалён основной объект.
У WeakMap есть ряд ограничений: У WeakMap есть ряд ограничений:
<ul> <ul>
<li>Нет свойства `size`.</li> <li>Нет свойства `size`.</li>
@ -269,7 +264,7 @@ activeUsers.splice(0, 1); // Петя более не активный поль
Иными словами, `WeakMap` работает только на запись (`set`, `delete`) и чтение (`get`, `has`) элементов по конкретному ключу, а не как полноценная коллекция. Нельзя вывести всё содержимое `WeakMap`, нет соответствующих методов. Иными словами, `WeakMap` работает только на запись (`set`, `delete`) и чтение (`get`, `has`) элементов по конкретному ключу, а не как полноценная коллекция. Нельзя вывести всё содержимое `WeakMap`, нет соответствующих методов.
Это связано с тем, что содержимое `WeakMap` может быть модифицировано сборщиком мусора в любой момент, независимо от программиста. Сборщик мусора работает сам по себе. Он не гарантирует, что очистит объект сразу же, когда это стало возможным. Нет какого-то конкретного момента, когда такая очистка точно произойдёт -- это определяется внутренними алгоритмами сборщика и его сведениями о системе. Это связано с тем, что содержимое `WeakMap` может быть модифицировано сборщиком мусора в любой момент, независимо от программиста. Сборщик мусора работает сам по себе. Он не гарантирует, что очистит объект сразу же, когда это стало возможным. В равной степени он не гарантирует и обратное. Нет какого-то конкретного момента, когда такая очистка точно произойдёт -- это определяется внутренними алгоритмами сборщика и его сведениями о системе.
Поэтому содержимое `WeakMap` в произвольный момент, строго говоря, не определено. Может быть, сборщик мусора уже удалил какие-то записи, а может и нет. С этим, а также с требованиями к эффективной реализации `WeakMap`, и связано отсутствие методов, осуществляющих доступ ко всем записям. Поэтому содержимое `WeakMap` в произвольный момент, строго говоря, не определено. Может быть, сборщик мусора уже удалил какие-то записи, а может и нет. С этим, а также с требованиями к эффективной реализации `WeakMap`, и связано отсутствие методов, осуществляющих доступ ко всем записям.
@ -286,22 +281,4 @@ activeUsers.splice(0, 1); // Петя более не активный поль
</ul> </ul>

View file

@ -0,0 +1,27 @@
Для последовательной загрузки нужно организовать промисы в цепочку, чтобы они выполнялись строго -- один после другого.
Вот код, который это делает:
```js
// начало цепочки
let chain = Promise.resolve();
let results = [];
// в цикле добавляем задачи в цепочку
urls.forEach(function(url) {
chain = chain
.then(() => httpGet(url))
.then((result) => {
results.push(result);
});
});
// в конце — выводим результаты
chain.then(() => {
alert(results);
});
```
Использование `Promise.resolve()` как начала асинхронной цепочки -- очень распространённый приём.

View file

@ -0,0 +1,25 @@
function httpGet(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
if (this.status == 200) {
resolve(this.response);
} else {
var error = new Error(this.statusText);
error.code = this.status;
reject(error);
}
};
xhr.onerror = function() {
reject(new Error("Network Error"));
};
xhr.send();
});
}

View file

@ -0,0 +1,34 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="httpGet.js"></script>
<script>
'use strict';
let urls = ['guest.json', 'user.json'];
let chain = Promise.resolve();
let results = [];
urls.forEach(function(url) {
chain = chain
.then(() => httpGet(url))
.then((result) => {
results.push(result);
});
});
chain.then(() => {
alert(results);
});
</script>
</body>
</html>

View file

@ -0,0 +1,4 @@
{
"name": "guest",
"isAdmin": false
}

View file

@ -0,0 +1,25 @@
function httpGet(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
if (this.status == 200) {
resolve(this.response);
} else {
var error = new Error(this.statusText);
error.code = this.status;
reject(error);
}
};
xhr.onerror = function() {
reject(new Error("Network Error"));
};
xhr.send();
});
}

View file

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="httpGet.js"></script>
<script>
'use strict';
let urls = ['guest.json', 'user.json'];
// ... ваш код
</script>
</body>
</html>

View file

@ -0,0 +1,26 @@
# Загрузить массив последовательно
Есть массив URL:
```js
//+ run
'use strict';
let urls = [
'user.json',
'guest.json'
];
```
Напишите код, который все URL из этого массива загружает -- один за другим (последовательно), и сохраняет в результаты в массиве `results`, а потом выводит.
Вариант с параллельной загрузкой выглядел бы так:
```js
Promise.all( urls.map(httpGet) )
.then(alert);
```
В этой задаче загрузку нужно реализовать последовательно.

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

View file

@ -39,7 +39,7 @@ var promise = new Promise(function(resolve, reject) {
Универсальный метод для навешивания обработчиков: Универсальный метод для навешивания обработчиков:
``` ```js
promise.then(onFulfilled, onRejected) promise.then(onFulfilled, onRejected)
``` ```
@ -51,9 +51,9 @@ promise.then(onFulfilled, onRejected)
С его помощью можно назначить как оба обработчика сразу, так и только один: С его помощью можно назначить как оба обработчика сразу, так и только один:
```js ```js
// только на успешное выполнение // onFulfilled сработает при успешном выполнении
promise.then(onFulfilled) promise.then(onFulfilled)
// только на ошибку // onRejected сработает при ошибке
promise.then(null, onRejected) promise.then(null, onRejected)
``` ```
@ -65,7 +65,9 @@ promise.then(null, onRejected)
Если в функции промиса происходит синхронный `throw` (или иная ошибка), то вызывается `reject`: Если в функции промиса происходит синхронный `throw` (или иная ошибка), то вызывается `reject`:
```js ```js
//+ run //+ run
var p = new Promise((resolve, reject) => { 'use strict';
let p = new Promise((resolve, reject) => {
// то же что reject(new Error("o_O")) // то же что reject(new Error("o_O"))
throw new Error("o_O"); throw new Error("o_O");
}) })
@ -83,8 +85,10 @@ p.catch(alert); // Error: o_O
```js ```js
//+ run //+ run
'use strict';
// Создаётся объект promise // Создаётся объект promise
var promise = new Promise((resolve, reject) => { let promise = new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
// переведёт промис в состояние fulfilled с результатом "result" // переведёт промис в состояние fulfilled с результатом "result"
@ -135,7 +139,7 @@ promise
``` ```
Конечно, вместо `setTimeout` мог бы быть и запрос к серверу и ожидание ввода пользователя, или другой асинхронный процесс. Конечно, вместо `setTimeout` внутри функции промиса может быть и запрос к серверу и ожидание ввода пользователя, или другой асинхронный процесс. Главное, чтобы по своему завершению он вызвал `resolve` или `reject`, которые передадут результат обработчикам.
[smart header="Только один аргумент"] [smart header="Только один аргумент"]
Функции `resolve/reject` принимают ровно один аргумент -- результат/ошибку. Функции `resolve/reject` принимают ровно один аргумент -- результат/ошибку.
@ -153,7 +157,9 @@ promise
```js ```js
//+ run //+ run
var promise = new Promise((resolve, reject) => { 'use strict';
let promise = new Promise((resolve, reject) => {
*!* *!*
// через 1 секунду готов результат: result // через 1 секунду готов результат: result
@ -184,7 +190,9 @@ promise
```js ```js
//+ run //+ run
var promise = new Promise((resolve, reject) => { 'use strict';
let promise = new Promise((resolve, reject) => {
// reject вызван раньше, resolve будет проигнорирован // reject вызван раньше, resolve будет проигнорирован
setTimeout(() => reject(new Error("error")), 1000); setTimeout(() => reject(new Error("error")), 1000);
@ -262,7 +270,7 @@ httpGet("/article/promise/user.json")
[smart header="Метод `fetch`"] [smart header="Метод `fetch`"]
Заметим, что ряд современных браузеров уже поддерживает [fetch](/fetch) -- новый встроенный метод для AJAX-запросов, призванный заменить XMLHttpRequest. Он, конечно, гораздо мощнее, чем `httpGet`. И -- да, этот метод использует промисы. Полифилл для него доступен на [](https://github.com/github/fetch). Заметим, что ряд современных браузеров уже поддерживает [fetch](/fetch) -- новый встроенный метод для AJAX-запросов, призванный заменить XMLHttpRequest. Он гораздо мощнее, чем `httpGet`. И -- да, этот метод использует промисы. Полифилл для него доступен на [](https://github.com/github/fetch).
[/smart] [/smart]
@ -332,15 +340,15 @@ httpGet(...)
.then(...) .then(...)
``` ```
При чейнинге, то есть последовательных вызовах `.then...then..then`, в каждый следующий `then` переходит результат от предыдущего. При чейнинге, то есть последовательных вызовах `.then…then…then`, в каждый следующий `then` переходит результат от предыдущего. Вызовы `console.log` оставлены, чтобы при запуске можно было посмотреть конкретные значения, хотя они здесь и не очень важны.
**Причём, если очередной `then` вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.** **Если очередной `then` вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.**
В коде выше: В коде выше:
<ol> <ol>
<li>В первом `then` возвращается объект `user`, он переходит в следующий `then`.</li> <li>В первом `then` возвращается объект `user`, он переходит в следующий `then`.</li>
<li>Во втором `then` возвращается промис (результат `loadUser`). Когда он будет завершён (может пройти какое-то время), то будет вызван следующий `then`.</li> <li>Во втором `then` возвращается промис (результат нового вызова `httpGet`). Когда он будет завершён (может пройти какое-то время), то будет вызван следующий `then`.</li>
<li>Третий `then` ничего не возвращает.</li> <li>Третий `then` ничего не возвращает.</li>
</ol> </ol>
@ -348,13 +356,19 @@ httpGet(...)
<img src="promiseUserFlow.png"> <img src="promiseUserFlow.png">
Значком "песочные часы" помечены периоды ожидания. Если `then` возвращает промис, то до его выполнения может пройти некоторое время, оставшаяся часть цепочки ждёт. Значком "песочные часы" помечены периоды ожидания, которых всего два: в исходном `httpGet` и в подвызове далее по цепочке.
Обратим внимание, что последний `then` в нашем примере ничего не возвращает. Если мы хотим, чтобы после `setTimeout` `(*)` асинхронная цепочка могла быть продолжена, то последний `then` тоже должен вернуть промис. Если `then` возвращает промис, то до его выполнения может пройти некоторое время, оставшаяся часть цепочки будет ждать.
Это стандартный приём. Если внутри `then` стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис, который перейдёт в состояние "выполнен" после `setTimeout`. То есть, логика довольно проста:
<ul>
<li>В каждом `then` мы получаем текущий результат работы.</li>
<li>Можно его обработать синхронно и вернуть результат (например, применить `JSON.parse`). Или же, если нужна асинхронная обработка -- инициировать её и вернуть промис.</li>
</ul>
Обратим внимание, что последний `then` в нашем примере ничего не возвращает. Если мы хотим, чтобы после `setTimeout` `(*)` асинхронная цепочка могла быть продолжена, то последний `then` тоже должен вернуть промис. Это общее правило: если внутри `then` стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис.
В данном случае промис должен перейти в состояние "выполнен" после срабатывания `setTimeout`.
Строку `(*)` для этого нужно переписать так: Строку `(*)` для этого нужно переписать так:
```js ```js
@ -369,7 +383,9 @@ httpGet(...)
// после таймаута — вызов resolve, // после таймаута — вызов resolve,
// можно без результата, чтобы управление перешло в следующий then // можно без результата, чтобы управление перешло в следующий then
// (или можно передать данные пользователя дальше по цепочке) // (или можно передать данные пользователя дальше по цепочке)
*!*
resolve(); resolve();
*/!*
}, 3000); }, 3000);
}); });
}) })
@ -431,12 +447,12 @@ httpGet('/page-not-exists')
[smart header="А что после `catch`?"] [smart header="А что после `catch`?"]
Обработчик `.catch(onRejected)` получает ошибку и должен обработать её. Обработчик `.catch(onRejected)` получает ошибку и должен обработать её.
Здесь два варианта развития событий: Есть два варианта развития событий:
<ol> <ol>
<li>Если ошибка не критичная, то обработчик возвращает значение через `return`, и управление переходит в ближайший `.then(onFulfilled)`.</li> <li>Если ошибка не критичная, то `onRejected` возвращает значение через `return`, и управление переходит в ближайший `.then(onFulfilled)`.</li>
<li>Если продолжить выполнение с такой ошибкой нельзя, то он делает `throw`, и тогда ошибка переходит в ближайший `.catch(onRejected)`. <li>Если продолжить выполнение с такой ошибкой нельзя, то он делает `throw`, и тогда ошибка переходит в следующий ближайший `.catch(onRejected)`.
</li> </li>
</ol> </ol>
@ -462,11 +478,11 @@ httpGet('/page-not-exists')
<img src="promiseEcma.png"> <img src="promiseEcma.png">
Когда функция-executor вызывает reject или resolve, то `PromiseState` становится `"resolved"` или `"rejected"`, а все функции-обработчики из соответствующего списка перемещаются в специальную системную очередь `"PromiseJobs"`. Когда функция-executor вызывает `reject` или `resolve`, то `PromiseState` становится `"resolved"` или `"rejected"`, а все функции-обработчики из соответствующего списка перемещаются в специальную системную очередь `"PromiseJobs"`.
Эта очередь автоматически выполняется, когда интерпретатору "нечего делать". Иначе говоря, все функции выполнятся асинхронно, одна за другой, по завершении текущего кода, примерно как `setTimeout(..,0)`. Эта очередь автоматически выполняется, когда интерпретатору "нечего делать". Иначе говоря, все функции-обработчики выполнятся асинхронно, одна за другой, по завершении текущего кода, примерно как `setTimeout(..,0)`.
Исключение из этого правила -- если `resolve` возвращает другой Promise. Тогда дальнейшее выполнение ожидает его результата (в очередь помещается специальная задача), и функции-обработчики выполняются уже с ним. Исключение из этого правила -- если `resolve` возвращает другой `Promise`. Тогда дальнейшее выполнение ожидает его результата (в очередь помещается специальная задача), и функции-обработчики выполняются уже с ним.
Добавляет обработчики в списки один метод: `.then(onResolved, onRejected)`. Метод `.catch(onRejected)` -- всего лишь сокращённая запись `.then(null, onRejected)`. Добавляет обработчики в списки один метод: `.then(onResolved, onRejected)`. Метод `.catch(onRejected)` -- всего лишь сокращённая запись `.then(null, onRejected)`.
@ -519,7 +535,9 @@ promise.then( function f2(result) {
<img src="promiseTwo.png"> <img src="promiseTwo.png">
На этой иллюстрации можно увидеть, что `.then` если один из обработчиков не указан, добавляет его "от себя", следующим образом: На этой иллюстрации можно увидеть добавленные нами обработчики `f1`, `f2`, а также -- автоматические добавленные обработчики ошибок `"Thrower"`.
Дело в том, что `.then`, если один из обработчиков не указан, добавляет его "от себя", следующим образом:
<ul> <ul>
<li>Для успешного выполнения -- функция `Identity`, которая выглядит как `arg => return arg`, то есть возвращает аргумент без изменений.</li> <li>Для успешного выполнения -- функция `Identity`, которая выглядит как `arg => return arg`, то есть возвращает аргумент без изменений.</li>
<li>Для ошибки -- функция `Thrower`, которая выглядит как `arg => throw arg`, то есть генерирует ошибку.</li> <li>Для ошибки -- функция `Thrower`, которая выглядит как `arg => throw arg`, то есть генерирует ошибку.</li>
@ -529,15 +547,19 @@ promise.then( function f2(result) {
Обратим внимание, в этом примере намеренно *не используется чейнинг*. То есть, обработчики добавляются именно на один и тот же промис. Обратим внимание, в этом примере намеренно *не используется чейнинг*. То есть, обработчики добавляются именно на один и тот же промис.
Поэтому оба `alert` выдадут одно значение `1`. Алгоритм такой -- все функции из списка обработчиков вызываются с результатом промиса, одна за другой. Никакой передачи результатов между обработчиками одного промиса нет, а сам результат промиса (`PromiseResult`) после установки не меняется. Поэтому оба `alert` выдадут одно значение `1`.
Все функции из списка обработчиков вызываются с результатом промиса, одна за другой. Никакой передачи результатов между обработчиками в рамках одного промиса нет, а сам результат промиса (`PromiseResult`) после установки не меняется.
Поэтому, чтобы продолжить работу с результатом, используется чейнинг.
**Для того, чтобы результат обработчика передать следующей функции, `.then` создаёт новый промис и возвращает его.** **Для того, чтобы результат обработчика передать следующей функции, `.then` создаёт новый промис и возвращает его.**
В примере выше создаётся два таких промиса, каждый из которых даёт свою ветку выполнения: В примере выше создаётся два таких промиса (т.к. два вызова `.then`), каждый из которых даёт свою ветку выполнения:
<img src="promiseTwoThen.png"> <img src="promiseTwoThen.png">
Изначально эти новые промисы -- пустые. Когда в будущем выполнятся обработчики `f1, f2`, то их результат будет передан в новые промисы по стандартному принципу: Изначально эти новые промисы -- "пустые", они ждут. Когда в будущем выполнятся обработчики `f1, f2`, то их результат будет передан в новые промисы по стандартному принципу:
<ul> <ul>
<li>Если вернётся обычное значение (не промис), новый промис перейдёт в `"resolved"` с ним.</li> <li>Если вернётся обычное значение (не промис), новый промис перейдёт в `"resolved"` с ним.</li>
@ -547,6 +569,8 @@ promise.then( function f2(result) {
<img src="promiseHandlerVariants.png"> <img src="promiseHandlerVariants.png">
Дальше выполнятся уже обработчики на новом промисе, и так далее.
Чтобы лучше понять происходящее, посмотрим на цепочку, которая получается в процессе написания кода для показа github-аватара. Чтобы лучше понять происходящее, посмотрим на цепочку, которая получается в процессе написания кода для показа github-аватара.
Первый промис и обработка его результата: Первый промис и обработка его результата:
@ -561,20 +585,16 @@ httpGet('/article/promise/user.json')
Если промис завершился через `resolve`, то результат -- в `JSON.parse`, если `reject` -- то в Thrower. Если промис завершился через `resolve`, то результат -- в `JSON.parse`, если `reject` -- то в Thrower.
Как было сказано выше, `Thrower` -- это стандартная внутренняя функция, которая автоматически используется, если второй обработчик не указан. Можно сказать, что второй обработчик выглядит так: Как было сказано выше, `Thrower` -- это стандартная внутренняя функция, которая автоматически используется, если второй обработчик не указан.
Можно считать, что второй обработчик выглядит так:
```js ```js
*!*
function thrower(err) {
throw err;
}
*/!*
httpGet('/article/promise/user.json') httpGet('/article/promise/user.json')
.then(JSON.parse, thrower) .then(JSON.parse, *!*err => throw err*/!*)
``` ```
Заметим, что когда обработчик в промисах делает `throw`, то ошибка не "валит" скрипт и не выводится в консоли. Она просто будет передана в ближайший следующий обработчик `onRejected`. Заметим, что когда обработчик в промисах делает `throw` -- в данном случае, при ошибке запроса, то такая ошибка не "валит" скрипт и не выводится в консоли. Она просто будет передана в ближайший следующий обработчик `onRejected`.
Добавим в код ещё строку: Добавим в код ещё строку:
@ -663,8 +683,12 @@ httpGet('/article/promise/userNoGithub.json')
В консоли тоже ничего не будет, так как ошибка остаётся "внутри" промиса, ожидая добавления следующего обработчика `onRejected`, которому будет передана. В консоли тоже ничего не будет, так как ошибка остаётся "внутри" промиса, ожидая добавления следующего обработчика `onRejected`, которому будет передана.
Итак, мы рассмотрели основные приёмы использования промисов. Далее -- посмотрим некоторые полезные вспомогательные методы.
## Параллельное выполнение
Что, если мы хотим осуществить несколько асинхронных процессов одновременно и обработать их результат?
## Вспомогательные методы
В классе `Promise` есть следующие статические методы. В классе `Promise` есть следующие статические методы.
@ -681,11 +705,44 @@ Promise.all([
httpGet('/article/promise/user.json'), httpGet('/article/promise/user.json'),
httpGet('/article/promise/guest.json') httpGet('/article/promise/guest.json')
]).then(results => { ]).then(results => {
results = results.map(JSON.parse); alert(results);
alert( results[0].name + ', ' + results[1].name ); // iliakan, guest
}); });
``` ```
Допустим, у нас есть массив с URL.
```js
let urls = [
'/article/promise/user.json',
'/article/promise/guest.json'
];
```
Чтобы загрузить их параллельно, нужно:
<ol>
<li>Создать для каждого URL соответствующий промис.</li>
<li>Обернуть массив таких промисов в `Promise.all`.</li>
</ol>
Получится так:
```js
//+ run
'use strict';
let urls = [
'/article/promise/user.json',
'/article/promise/guest.json'
];
*!*
Promise.all( urls.map(httpGet) )
*/!*
.then(results => {
alert(results);
});
```
Заметим, что если какой-то из промисов завершился с ошибкой, то результатом `Promise.all` будет эта ошибка. При этом остальные промисы игнорируются. Заметим, что если какой-то из промисов завершился с ошибкой, то результатом `Promise.all` будет эта ошибка. При этом остальные промисы игнорируются.
Например: Например:
@ -706,7 +763,7 @@ Promise.all([
### Promise.race(iterable) ### Promise.race(iterable)
Как и `Promise.all` получает итерируемый объект с промисами и возвращает новый промис. Вызов `Promise.race`, как и `Promise.all`, получает итерируемый объект с промисами, которые нужно выполнить, и возвращает новый промис.
Но, в отличие от `Promise.all`, результатом будет только первый успешно выполнившийся промис из списка. Остальные игнорируются. Но, в отличие от `Promise.all`, результатом будет только первый успешно выполнившийся промис из списка. Остальные игнорируются.
@ -724,7 +781,7 @@ Promise.race([
}); });
``` ```
### Promise.resolve(value) ## Promise.resolve(value)
Вызов `Promise.resolve(value)` создаёт успешно выполнившийся промис с результатом `value`. Вызов `Promise.resolve(value)` создаёт успешно выполнившийся промис с результатом `value`.
@ -746,7 +803,7 @@ Promise.resolve(window.location) // начать с этого значения
.then(alert) // и вывести результат .then(alert) // и вывести результат
``` ```
### Promise.reject(error) ## Promise.reject(error)
Аналогично `Promise.resolve(value)` создаёт уже выполнившийся промис, но не с успешным результатом, а с ошибкой `error`. Аналогично `Promise.resolve(value)` создаёт уже выполнившийся промис, но не с успешным результатом, а с ошибкой `error`.
@ -760,7 +817,7 @@ Promise.reject(new Error("..."))
Метод `Promise.reject` используется очень редко, гораздо реже чем `resolve`, потому что ошибка возникает обычно не в начале цепочки, а в процессе её выполнения. Метод `Promise.reject` используется очень редко, гораздо реже чем `resolve`, потому что ошибка возникает обычно не в начале цепочки, а в процессе её выполнения.
### Итого ## Итого
<ul> <ul>
<li>Промис -- это специальный объект, который хранит своё состояние, текущий результат (если есть) и коллбэки.</li> <li>Промис -- это специальный объект, который хранит своё состояние, текущий результат (если есть) и коллбэки.</li>
@ -770,12 +827,9 @@ Promise.reject(new Error("..."))
<li>Для передачи результата от одного обработчика к другому используется чейнинг.</li> <li>Для передачи результата от одного обработчика к другому используется чейнинг.</li>
</ul> </ul>
В современной JavaScript-разработки промисы в явном виде используются, как ни странно, довольно редко. У промисов есть некоторые ограничения. В частности, стандарт не предусматривает какой-то метод для "отмены" промиса, хотя в ряде ситуаций (http-запросы) это было бы довольно удобно. Возможно, он появится в следующей версии стандарта JavaScript.
Тем не менее, понимать промисы нужно обязательно, так как бывают ситуации, когда без них сложно.
Промисы служат основой для более продвинутых способов написания асинхронного кода, использующих генераторы. Мы рассмотрим их далее в этом разделе.
В современной JavaScript-разработке сложные цепочки с промисами используются редко, так как они куда проще описываются при помощи генераторов с библиотекой `co`, которые рассмотрены в [соответствующей главе](/generator). Можно сказать, что промисы лежат в основе более продвинутых способов асинхронной разработки.
[head] [head]
<style> <style>

View file

@ -0,0 +1,4 @@
{
"name": "guest",
"isAdmin": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -0,0 +1,4 @@
{
"name": "iliakan",
"isAdmin": true
}

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,4 @@
{
"name": "iliakan",
"isAdmin": true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View file

@ -1,4 +0,0 @@
{
"name": "an-unknown-person-32662",
"isAdmin": false
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -3,46 +3,54 @@
Новый примитивный тип данных Symbol служит для создания уникальных идентификаторов. Новый примитивный тип данных Symbol служит для создания уникальных идентификаторов.
Мы вначале рассмотрим объявление и особенности символов, а затем -- их использование.
## Объявление ## Объявление
Синтаксис: Синтаксис:
```js ```js
//+ run let sym = Symbol();
'use strict';
let name = Symbol();
alert( typeof name ); // symbol
``` ```
Обратим внимание, не `new Symbol`, а просто `Symbol`, так как это -- примитив. Обратим внимание, не `new Symbol`, а просто `Symbol`, так как это -- примитив.
Каждый символ -- уникален. У символов есть и соответствующий `typeof`:
У функции `Symbol` есть необязательный аргумент "имя символа". Можно его использовать для описания символа, в целях отладки:
```js ```js
//+ run //+ run
'use strict'; 'use strict';
let name = Symbol("name"); let sym = Symbol();
alert( name.toString() ); // Symbol(name) alert( typeof sym ); // symbol
``` ```
При этом если у двух символов одинаковое имя, то они *не равны*:
Каждый символ -- уникален. У функции `Symbol` есть необязательный аргумент "имя символа". Можно его использовать для описания символа, в целях отладки:
```js
//+ run
'use strict';
let sym = Symbol("name");
alert( sym.toString() ); // Symbol(name)
```
...Но при этом если у двух символов одинаковое имя, то это не значит, что они равны:
```js ```js
//+ run //+ run
alert( Symbol("name") == Symbol("name") ); // false alert( Symbol("name") == Symbol("name") ); // false
``` ```
То есть, ещё раз заметим, что каждый символ -- уникален. Если хочется из разных частей программы использовать именно одинаковый символ, то можно передавать между ними объект символа или же -- использовать "глобальные символы" и "реестр глобальных символов", которые мы рассмотрим далее.
## Глобальные символы ## Глобальные символы
Существует "глобальный реестр" символов, который позволяет, при необходимости, разделять символы между частями программы. Существует "глобальный реестр" символов, который позволяет, при необходимости, разделять символы между частями программы.
Для чтения (или создания, если нет) "глобального" символа служит вызов `Symbol.for(имя)`: Для чтения (или создания, при отсутствии) "глобального" символа служит вызов `Symbol.for(имя)`.
Например:
```js ```js
//+ run //+ run
@ -55,7 +63,7 @@ let name = Symbol.for("name");
alert( Symbol.for("name") == name ); // true alert( Symbol.for("name") == name ); // true
``` ```
Вызов `Symbol.keyFor(sym)` позволяет получить по глобальному символу его имя: Вызов `Symbol.for` возвращает символ по имени. Обратным для него является вызов `Symbol.keyFor(sym)` позволяет получить по глобальному символу его имя:
```js ```js
@ -78,13 +86,13 @@ alert( Symbol.keyFor(name) ); // name
alert( Symbol.keyFor(Symbol.for("name")) ); // name, глобальный alert( Symbol.keyFor(Symbol.for("name")) ); // name, глобальный
alert( Symbol.keyFor(Symbol("name2")) ); // undefined, обычный символ alert( Symbol.keyFor(Symbol("name2")) ); // undefined, обычный символ
``` ```
Таким образом, имя символа, если этот символ не глобальный, не имеет особого применения, оно полезно лишь в целях вывода и отладки.
[/warn] [/warn]
## Использование символов ## Использование символов
Символы используются в качестве имён для методов и свойств объекта, которые являются системными, или которые необходимо скрыть. Символы можно использовать в качестве имён для свойств объекта, вот так:
Особенность символов -- в том, что если в объект записать свойство-символ, то оно не участвует в итерации:
```js ```js
//+ run //+ run
@ -94,17 +102,38 @@ let isAdmin = Symbol("isAdmin");
let user = { let user = {
name: "Вася", name: "Вася",
age: 30,
[isAdmin]: true [isAdmin]: true
}; };
alert( Object.keys(user) ); // name, age alert(user[isAdmin]); // true
for(let key in user) alert(key); // name, age
``` ```
В примере выше выведутся все свойства, кроме символьного. Особенность символов -- в том, что если в объект записать свойство-символ, то оно не участвует в итерации:
В современно JavaScript есть много системных символов. Их список есть в спецификации, в таблице [Well-known Symbols](http://www.ecma-international.org/ecma-262/6.0/index.html#table-1). В спецификации принято символы для краткости обозначать их как '@@имя', например `@@iterator`, но доступны они как свойства `Symbol`. ```js
//+ run
'use strict';
let user = {
name: "Вася",
age: 30,
[Symbol.for("isAdmin")]: true
};
// в цикле for..in также не будет символа
alert( Object.keys(user) ); // name, age
// доступ к свойству через глобальный символ — работает
alert( user[Symbol.for("isAdmin")] );
```
Кроме того, свойство-символ недоступно, если обратиться к его названию: `user.isAdmin` не существует.
Зачем всё это, почему не просто использовать строки?
Резонный вопрос. На ум могут прийти соображения производительности, так как символы -- это по сути специальные идентификаторы, они компактнее, чем строка. Но при современных оптимизациях объектов это редко имеет значение.
Самое широкое применение символов предусмотрено внутри самого стандарта JavaScript. В современном стандарте есть много системных символов. Их список есть в спецификации, в таблице [Well-known Symbols](http://www.ecma-international.org/ecma-262/6.0/index.html#table-1). В спецификации принято символы для краткости обозначать их как '@@имя', например `@@iterator`, но доступны они как свойства `Symbol`.
Например: Например:
<ul> <ul>
@ -113,35 +142,41 @@ for(let key in user) alert(key); // name, age
<li>...и т.п.</li> <li>...и т.п.</li>
</ul> </ul>
Мы легко поймём смысл введения нового типа "символ", если поставим себя на место создателей языка JavaScript. **Мы легко поймём смысл введения нового типа "символ", если поставим себя на место создателей языка JavaScript.**
Допустим, надо добавить к объекту "особый" функционал, например, функцию, которая задаёт преобразование объекта к примитиву. Допустим, в новом стандарте нам надо добавить к объекту "особый" функционал, например, функцию, которая задаёт преобразование объекта к примитиву. Как `obj.toString`, но для преобразования в примитивы.
Можно, конечно, сказать, что "свойство obj.toPrimitive теперь системное, оно делает то-то и то-то". Но это опасно. Мало ли, вполне возможно, что свойство с таким именем уже используется в существующем коде. И если сделать его системным, то он сломается. Мы ведь не можем просто сказать, что "свойство obj.toPrimitive теперь будет задавать преобразование к примитиву и автоматически вызываться в таких-то ситуациях". Это опасно. Мы не можем так просто взять и придать особый смысл свойству. Мало ли, вполне возможно, что свойство с таким именем уже используется в существующем коде, и если сделать его особым, то он сломается.
Нельзя просто взять и зарезервировать какие-то свойства существующих объектов для нового функционала. Нельзя просто взять и зарезервировать какие-то свойства существующих объектов для нового функционала.
Поэтому ввели целый тип "символы". Их можно использовать для задания свойств, которые уникальны, не участвуют в итерации и заведомо не конфликтуют со старым кодом. Поэтому ввели целый тип "символы". Их можно использовать для задания таких свойств, так как они:
<ul>
<li>а) уникальны,</li>
<li>б) не участвуют в циклах,</li>
<li>в) заведомо не сломают старый код, который о них слыхом не слыхивал.</li>
</ul>
Продемонстрируем отсутствие конфликта для нового системного свойства `Symbol.iterator`:
Например:
```js ```js
//+ run //+ run
'use strict'; 'use strict';
let obj = { let obj = {
iterator: 1, iterator: 1,
[Symbol.iterator]: function() {} [Symbol.iterator]() {}
} }
alert(obj.iterator); // 1 alert(obj.iterator); // 1
alert(obj[Symbol.iterator]) // function, символ не конфликтует alert(obj[Symbol.iterator]) // function, символ не конфликтует
``` ```
Выше мы использовали системный символ `Symbol.iterator`, поскольку он один из самых широко поддерживаемых. Мы подробно разберём его смысл в следующих главах, пока же -- это просто пример символа. Выше мы использовали системный символ `Symbol.iterator`, поскольку он один из самых широко поддерживаемых. Мы подробно разберём его смысл в главе про [итераторы](/iterator), пока же -- это просто пример символа.
Чтобы получить все символы объекта, есть особый вызов [Object.getOwnPropertySymbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols). Чтобы получить все символы объекта, есть особый вызов [Object.getOwnPropertySymbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols).
Эта функция возвращает все символы в объекте. Заметим, что `getOwnPropertyNames` символы не возвращает. Эта функция возвращает все символы в объекте (и только их). Заметим, что старая функция `getOwnPropertyNames` символы не возвращает, что опять же гарантирует отсутствие конфликтов со старым кодом.
```js ```js
//+ run //+ run
@ -167,7 +202,9 @@ alert( Object.getOwnPropertyNames(obj) ); // iterator
<li>Существует глобальный реестр символов, доступных через метод `Symbol.for(name)`. Для глобального символа можно получить имя вызовом и `Symbol.keyFor(sym)`.</li> <li>Существует глобальный реестр символов, доступных через метод `Symbol.for(name)`. Для глобального символа можно получить имя вызовом и `Symbol.keyFor(sym)`.</li>
</ul> </ul>
Основная область использования символов -- это системные свойства объектов. Поддержка у них пока небольшая, но она растёт. Системные символы позволяют добавлять в стандарт новые "особые" свойства объектов, при этом не резервируя соответствующие названия. Основная область использования символов -- это системные свойства объектов, которые задают разные аспекты их поведения. Поддержка у них пока небольшая, но она растёт. Системные символы позволяют разработчикам стандарта добавлять новые "особые" свойства объектов, при этом не резервируя соответствующие строковые значения.
Но, конечно, мы можем создавать и свои локальные и глобальные символы, использовать их в своих объектах. Системные символы доступны как свойства функции `Symbol`, например `Symbol.iterator`.
Мы можем создавать и свои символы, использовать их в объектах. Записывать их как свойства `Symbol`, разумеется, нельзя, если нужен глобально доступный символ, то используется `Symbol.for(имя)`.

View file

@ -5,7 +5,7 @@
Итерируемые или, иными словами, "перебираемые" объекты -- это те, содержимое которых можно перебрать в цикле. Итерируемые или, иными словами, "перебираемые" объекты -- это те, содержимое которых можно перебрать в цикле.
Например, массив, но не только он. В браузере существует множество объектов, которые не являются массивами, но содержимое которых можно перебрать (к примеру, список DOM-узлов), а итераторы дают возможность сделать "перебираемыми" любые объекты. Например, перебираемым объектом является массив. Но не только он. В браузере существует множество объектов, которые не являются массивами, но содержимое которых можно перебрать (к примеру, список DOM-узлов).
Для перебора таких объектов добавлен новый синтаксис цикла: `for..of`. Для перебора таких объектов добавлен новый синтаксис цикла: `for..of`.
@ -38,22 +38,29 @@ for(let char of "Привет") {
Практически везде, где нужен перебор, он осуществляется через итераторы. Это включает в себя не только строки, массивы, но и вызов функции с оператором spread `f(...args)`, и многое другое. Практически везде, где нужен перебор, он осуществляется через итераторы. Это включает в себя не только строки, массивы, но и вызов функции с оператором spread `f(...args)`, и многое другое.
В отличие от массивов, "перебираемые" объекты могут не иметь "длины" `length`. Как мы увидим далее, итераторы дают возможность сделать "перебираемыми" любые объекты.
## Свой итератор ## Свой итератор
Допустим, у нас есть некий объект, который надо "умным способом" перебрать. Допустим, у нас есть некий объект, который надо "умным способом" перебрать.
Например, `range` -- диапазон чисел от `range.from` до `range.to`: Например, `range` -- диапазон чисел от `from` до `to`, и мы хотим, чтобы `for(let num of range)` "перебирал", этот объект. При этом под перебором мы подразумеваем перечисление чисел от `from` до `to`.
Объект `range` без итератора:
```js ```js
let range = { let range = {
from: 1, from: 1,
to: 5 to: 5
}; };
// хотим сделать перебор
// for(let num of range) ...
``` ```
Для возможности использовать объект в `for..of` ему ставится свойство с названием `Symbol.iterator`. `Symbol.iterator` -- системный символ, который вызывает `for..of` в начале выполнения. Для возможности использовать объект в `for..of` нужно создать в нём свойство с названием `Symbol.iterator` (системный символ).
При вызове метода `Symbol.iterator` перебираемый (итерируемый) объект должен возвращать другой объект ("итератор"), который умеет осуществлять перебор. При вызове метода `Symbol.iterator` перебираемый объект должен возвращать другой объект ("итератор"), который умеет осуществлять перебор.
По стандарту у такого объекта должен быть метод `next()`, который при каждом вызове возвращает очередное значение и окончен ли перебор. По стандарту у такого объекта должен быть метод `next()`, который при каждом вызове возвращает очередное значение и окончен ли перебор.
@ -97,11 +104,11 @@ for (let num of range) {
} }
``` ```
...То есть, здесь имеет место разделение сущностей: Как видно из кода выше, здесь имеет место разделение сущностей:
<ul> <ul>
<li>Перебираемый объект сам не реализует методы для своего перебора. Для этого существует другой объект, который хранит текущее состояние перебора и возвращает значение.</li> <li>Перебираемый объект `range` сам не реализует методы для своего перебора.</li>
<li>Этот объект называется итератором и создаётся при вызове метода `Symbol.iterator`.</li> <li>Для этого создаётся другой объект, который хранит текущее состояние перебора и возвращает значение. Этот объект называется итератором и возвращается при вызове метода `range[Symbol.iterator]`.</li>
<li>У итератора должен быть метод `next()`, который при каждом вызове возвращает объект со свойствами: <li>У итератора должен быть метод `next()`, который при каждом вызове возвращает объект со свойствами:
<ul> <ul>
<li>`value` -- очередное значение, <li>`value` -- очередное значение,
@ -110,11 +117,11 @@ for (let num of range) {
</li> </li>
</ul> </ul>
Конструкция `for..of` получает итератор и вызывает метод `next()` прозрачно и автоматически до получения `done: true`. Конструкция `for..of` в начале своего выполнения автоматически вызывает `Symbol.iterator()`, получает итератор и далее вызывает метод `next()` до получения `done: true`. Такова внутренняя механика. Внешний код при переборе через `for..of` видит только значения.
Впрочем, хоть такое отделение функционала перебора от самого объекта даёт дополнительную гибкость, например, объект может возвращать разные итераторы в зависимости от своего настроения и времени суток, зачастую оно не нужно. Такое отделение функционала перебора от самого объекта даёт дополнительную гибкость, например, объект может возвращать разные итераторы в зависимости от своего настроения и времени суток. Однако, бывают ситуации, когда оно не нужно.
Чтобы функционал по перебору заключить в самом объекте, можно вернуть `this` в качестве итератора: Если функционал по перебору (метод `next`) предоставляется самим объектом, то можно вернуть `this` в качестве итератора:
```js ```js
@ -166,19 +173,23 @@ alert( Math.max(...range) ); // 5 (*)
В данном случае это работает, но для большей гибкости и понятности кода рекомендуется, всё же, выделять итератор в отдельный объект со своим состоянием и кодом. В данном случае это работает, но для большей гибкости и понятности кода рекомендуется, всё же, выделять итератор в отдельный объект со своим состоянием и кодом.
В последней строке `(*)` можно видеть, что итерируемый объект передаётся через spread для `Math.max`. При этом spread превращает итератор в массив. То есть пройдёт цикл по итератору, с вызовами `next`, и его результаты будут использованы в качестве списка аргументов. [smart header="Оператор spread `...` и итераторы"]
В последней строке `(*)` примера выше можно видеть, что итерируемый объект передаётся через spread для `Math.max`.
При этом `...range` автоматически превращает итерируемый объект в массив. То есть произойдёт цикл `for..of` по `range`, и его результаты будут использованы в качестве списка аргументов.
[/smart]
[smart header="Бесконечные итераторы"] [smart header="Бесконечные итераторы"]
Возможны и бесконечные итераторы. Например, пример выше при `range.to = Infinity` будет таковым. Или можно сделать итератор, генерирующий бесконечную последовательность псевдослучайных чисел. Возможны и бесконечные итераторы. Например, пример выше при `range.to = Infinity` будет таковым. Или можно сделать итератор, генерирующий бесконечную последовательность псевдослучайных чисел. Тоже полезно.
Нет никаких ограничений на `next`, он может возвращать всё новые и новые значения. Нет никаких ограничений на `next`, он может возвращать всё новые и новые значения, и это нормально.
Разумеется, цикл `for..of` по такому итератору сам по себе не завершится, нужно его прерывать, например, через `break`. Разумеется, цикл `for..of` по такому итератору тоже будет бесконечным, нужно его прерывать, например, через `break`.
[/smart] [/smart]
## Встроенные итераторы ## Встроенные итераторы
Итератор можно получить и без `for..of`, прямым вызовом `Symbol.iterator`. Встроенные в JavaScript итераторы можно получить и явным образом, без `for..of`, прямым вызовом `Symbol.iterator`.
Например, этот код получает итератор для строки и вызывает его полностью "вручную": Например, этот код получает итератор для строки и вызывает его полностью "вручную":
@ -200,13 +211,15 @@ while(true) {
} }
``` ```
То же самое будет работать и для массивов.
## Итого ## Итого
<ul> <ul>
<li>*Итератор* -- объект, предназначенный для перебора другого объекта.</li> <li>*Итератор* -- объект, предназначенный для перебора другого объекта.</li>
<li>У итератора должен быть метод `next()`, возвращающий `{done: Boolean, value: any}`, где `value` -- очередное значение, а `done: true` в конце.</li> <li>У итератора должен быть метод `next()`, возвращающий `{done: Boolean, value: any}`, где `value` -- очередное значение, а `done: true` в конце.</li>
<li>Метод `Symbol.iterator` предназначен для получения итератора из объекта. Цикл `for..of` делает это автоматически, но можно и вызвать его напрямую.</li> <li>Метод `Symbol.iterator` предназначен для получения итератора из объекта. Цикл `for..of` делает это автоматически, но можно и вызвать его напрямую.</li>
<li>В современном стандарте есть много мест, где вместо массива используются более абстрактные "итерируемые" (со свойством `Symbol.iterator`) объекты.</li> <li>В современном стандарте есть много мест, где вместо массива используются более абстрактные "итерируемые" (со свойством `Symbol.iterator`) объекты, например оператор spread `...`.</li>
<li>Встроенные объекты, такие как массивы и строки, являются итерируемыми, в соответствии с описанным выше.</li> <li>Встроенные объекты, такие как массивы и строки, являются итерируемыми, в соответствии с описанным выше.</li>
</ul> </ul>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more