Перебираемые (или итерируемые) объекты – это концепция, которая позволяет использовать любой объект в цикле for..of.
Например, у нас есть объект. Это не массив, но он выглядит подходящим для for..of.
Например, объект range, который представляет собой диапазон чисел:
let range = {
from: 1,
to: 5
};
// Мы хотим, чтобы работал for..of:
// for(let num of range) ... num=1,2,3,4,5
Чтобы сделать range итерируемым (и позволить for..of работать с ним), нам нужно добавить в объект метод с именем Symbol.iterator (специальный встроенный Symbol, созданный как раз для этого).
-
Когда цикл for..of запускается, он вызывает этот метод один раз (или выдаёт ошибку, если метод не найден). Этот метод должен вернуть итератор – объект с методом next.
-
Дальше for..of работает только с этим возвращённым объектом.
-
Когда for..of хочет получить следующее значение, он вызывает метод next() этого объекта.
-
Результат вызова next() должен иметь вид {done: Boolean, value: any}, где done=true означает, что итерация закончена, в противном случае value содержит очередное значение.
Вот полная реализация range
let range = {
from: 1,
to: 5
};
// 1. вызов for..of сначала вызывает эту функцию
range[Symbol.iterator] = function() {
// ...она возвращает объект итератора:
// 2. Далее, for..of работает только с этим итератором, запрашивая у него новые значения
return {
current: this.from,
last: this.to,
// 3. next() вызывается на каждой итерации цикла for..of
next() {
// 4. он должен вернуть значение в виде объекта {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// теперь работает!
for (let num of range) {
alert(num); // 1, затем 2, 3, 4, 5
}
У самого range нет метода next(). Вместо этого другой объект, так называемый «итератор», создаётся вызовом rangeSymbol.iterator, и именно его next() генерирует значения.
Обычные функции возвращают только одно-единственное значение (или ничего).
Генераторы могут порождать (yield) множество значений одно за другим, по мере необходимости.
Для объявления генератора используется специальная синтаксическая конструкция: function*, которая называется «функция-генератор».
Выглядит она так:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Функции-генераторы ведут себя не так, как обычные. Когда такая функция вызвана, она не выполняет свой код. Вместо этого она возвращает специальный объект, так называемый «генератор», для управления её выполнением.
Вот, посмотрите:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "функция-генератор" создаёт объект "генератор"
let generator = generateSequence();
alert(generator); // [object Generator]
Выполнение кода функции ещё не началось:
function* quips(name) {
yield "привет, " + name + "!";
yield "я надеюсь, вам нравятся статьи";
if (name) {
yield `как круто, что ваше имя - ${name}`;
}
yield "увидимся!";
}
Внутри функции-генератора есть ключевое слово yield с синтаксисом, похожим на return.
Отличие в том, что функция (в том числе функция-генератор) может вернуть значение только один раз, но отдать значение функция-генератор может любое количество раз.
Выражение yield приостанавливает выполнение генератора, так что его можно позже возобновить.
Что произойдёт, если запустить функцию-генератор quips()?
var iter = quips("jorendorff");
// [object Generator]
iter.next()
// { value: "привет, jorendorff!", done: false }
iter.next()
// { value: "я надеюсь, вам нравятся статьи", done: false }
iter.next()
// { value: "увидимся!", done: false }
iter.next()
// { value: undefined, done: true }
Каждый раз, как вы вызываете метод .next() у объекта Generator, вызов функции оттаивает и выполняется, пока не достигнет следующего выражения yield.
При последнем вызове iter.next() мы, наконец, достигли конца функции-генератора, так что поле .done результата стало равно true.
Добраться до конца функции — это всё равно что вернуть undefined, и именно поэтому поле .value результата равно undefined.
-
value: значение из yield.
-
done: true, если выполнение функции завершено, иначе false.
Например, здесь мы создаём генератор и получаем первое из возвращаемых им значений:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
На данный момент мы получили только первое значение, выполнение функции остановлено на второй строке:
Повторный вызов generator.next() возобновит выполнение кода и вернёт результат следующего yield:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
Нет разницы, оба синтаксиса корректны.
Как вы, наверное, уже догадались по наличию метода next(), генераторы являются перебираемыми объектами.
Возвращаемые ими значения можно перебирать через for..of:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, затем 2
}
…Но обратите внимание: пример выше выводит значение 1, затем 2. Значение 3 выведено не будет!
Это из-за того, что перебор через for..of игнорирует последнее значение, при котором done: true.
Поэтому, если мы хотим, чтобы были все значения при переборе через for..of, то надо возвращать их через yield:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, затем 2, затем 3
}
Так как генераторы являются перебираемыми объектами, мы можем использовать всю связанную с ними функциональность, например оператор расширения ...:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
Давайте вспомним код:
let range = {
from: 1,
to: 5,
// for..of range вызывает этот метод один раз в самом начале
[Symbol.iterator]() {
// ...он возвращает перебираемый объект:
// далее for..of работает только с этим объектом, запрашивая следующие значения
return {
current: this.from,
last: this.to,
// next() вызывается при каждой итерации цикла for..of
next() {
// нужно вернуть значение как объект {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// при переборе объекта range будут выведены числа от range.from до range.to
alert([...range]); // 1,2,3,4,5
Вот тот же range, но с гораздо более компактным итератором:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // краткая запись для [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
Это работает, потому что rangeSymbol.iterator теперь возвращает генератор, и его методы – в точности то, что ожидает for..of:
-
у него есть метод .next()
-
который возвращает значения в виде {value: ..., done: true/false}
Мы можем создавать пользовательские HTML-элементы, описываемые нашим классом, со своими методами и свойствами, событиями и так далее.
Как только пользовательский элемент определён, мы можем использовать его наравне со встроенными HTML-элементами.
Мы можем определить их с помощью специального класса, а затем использовать, как если бы они всегда были частью HTML.
Существует два вида пользовательских элементов:
-
Автономные пользовательские элементы – «полностью новые» элементы, расширяющие абстрактный класс HTMLElement.
-
Пользовательские встроенные элементы – элементы, расширяющие встроенные, например кнопку HTMLButtonElement и т.п.
Чтобы создать пользовательский элемент, нам нужно сообщить браузеру ряд деталей о нём: как его показать, что делать, когда элемент добавляется или удаляется со страницы и т.д.
Вот набросок с полным списком:
class MyElement extends HTMLElement {
constructor() {
super();
// элемент создан
}
connectedCallback() {
// браузер вызывает этот метод при добавлении элемента в документ
// (может вызываться много раз, если элемент многократно добавляется/удаляется)
}
disconnectedCallback() {
// браузер вызывает этот метод при удалении элемента из документа
// (может вызываться много раз, если элемент многократно добавляется/удаляется)
}
static get observedAttributes() {
return [/* массив имён атрибутов для отслеживания их изменений */];
}
attributeChangedCallback(name, oldValue, newValue) {
// вызывается при изменении одного из перечисленных выше атрибутов
}
adoptedCallback() {
// вызывается, когда элемент перемещается в новый документ
// (происходит в document.adoptNode, используется очень редко)
}
// у элемента могут быть ещё другие методы и свойства
}
После этого нам нужно зарегистрировать элемент:
// сообщим браузеру, что <my-element> обслуживается нашим новым классом
customElements.define("my-element", MyElement);
Имя пользовательского элемента должно содержать дефис -
Имя пользовательского элемента должно содержать дефис -, например, my-element и super-button – валидные имена, а myelement – нет.
Например, элемент уже существует в HTML для даты/времени. Но сам по себе он не выполняет никакого форматирования.
Давайте создадим элемент , который отображает время в удобном формате с учётом языка:
<script>
class TimeFormatted extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
}
customElements.define("time-formatted", TimeFormatted); // (2)
</script>
<!-- (3) -->
<time-formatted datetime="2019-12-01"
year="numeric" month="long" day="numeric"
hour="numeric" minute="numeric" second="numeric"
time-zone-name="short"
></time-formatted>
-
Класс имеет только один метод connectedCallback() – браузер вызывает его, когда элемент добавляется на страницу (или когда HTML-парсер обнаруживает его), и он использует встроенный форматировщик данных Intl.DateTimeFormat, хорошо поддерживаемый в браузерах, чтобы показать красиво отформатированное время.
-
Нам нужно зарегистрировать наш новый элемент, используя customElements.define(tag, class).
-
И тогда мы сможем использовать его везде.
Если браузер сталкивается с элементами до customElements.define, то это не ошибка. Но элемент пока неизвестен, как и любой нестандартный тег.
Такие «неопределённые» элементы могут быть стилизованы с помощью CSS селектора :not(:defined).
Когда вызывается customElements.define, они «обновляются»: для каждого создаётся новый экземпляр TimeFormatted и вызывается connectedCallback. Они становятся :defined.
Чтобы получить информацию о пользовательских элементах, есть следующие методы:
customElements.get(name) – возвращает класс пользовательского элемента с указанным именем name, customElements.whenDefined(name) – возвращает промис, который переходит в состояние «успешно выполнен» (без значения), когда определён пользовательский элемент с указанным именем name.
Веб-компоненты — это семейство API, предназначенных для описания новых элементов DOM, подходящих для повторного использования.
Функционал таких элементов отделён от остального кода, их можно применять в веб-приложениях собственной разработки.
-
Shadow DOM (теневой DOM)
-
HTML Templates (HTML-шаблоны)
-
Custom Elements (пользовательские элементы)
-
HTML Imports (HTML-импорт)