В ES6 появился удобный синтаксис для создания классов:
class Suitcase {
constructor(volume) {
this.volume = volume;
this.state = "closed";
}
open() {
this.state = "opened";
console.log(`Suitcase is ${this.state}`);
}
}
let myCase = new Suitcase(20);
При создании экземпляра запускается внутренний constructor, внутри которого производится инициализация нового объекта. Методы класса добавляются в прототип.
Такой класс ведет себя как let: обладает блочной видимостью и попадает во временную мертвую зону. Создать новый экземпляр без new нельзя. Методы имеют доступ к свойствам прототипа super и не являются перечислимыми.
По аналогии с функциями можно использовать альтернативный синтаксис создания класса - "Class Expression":
let Suitcase = class {
constructor(volume) {
this.volume = volume;
this.state = "closed";
}
open() {
this.state = "opened";
console.log(`Suitcase is ${this.state}`);
}
}
let myCase = new Suitcase(20);
Такой класс не имеет имени, но по аналогии с именованными функциональными выражениями имя можно добавить, однако это имя будет доступно только внути самого класса.
Как и для обектов, для классов можно задавать геттеры, сеттеры и вычисляемые свойства:
let someMethod = "open";
let Suitcase = class {
constructor(volume, label) {
this.volume = volume;
this.label = label;
this.state = "closed";
}
get info() {
return `"${this.label}", ${this.volume}l`;
}
set info(newVolume) {
this.volume = newVolume;
}
[someMethod]() {
this.state = "opened";
console.log(`Suitcase is ${this.state}`);
}
};
let myCase = new Suitcase(20, "Aloha");
Свойства экземпляра класса можно задать только в constructor.
Для созданного с помощью class класса можно создавать статические методы. Для этого используется ключевое слово static:
let Suitcase = class {
constructor(volume, label) {
this.volume = volume;
this.label = label;
this.state = "closed";
Suitcase.oneMoreObject();
}
static oneMoreObject() {
if(!("objectCount" in Suitcase)){
Suitcase.objectCount = 0;
}
Suitcase.objectCount++;
}
static get count() {
return Suitcase.objectCount || 0;
}
open() {
this.state = "opened";
console.log(`Suitcase is ${this.state}`);
}
};
let myCase = new Suitcase(20, "Aloha");
Наследование реализуется использованием ключевого слова extends:
class Case {
constructor(volume) {
this.volume = volume;
this.state = "closed";
}
open() {
this.state = "opened";
}
}
class Suitcase extends Case {
constructor(volume, label) {
super(volume);
this.label = label;
}
tag() {
this.taged = true;
}
open() {
super.open();
console.log(`Suitcase is ${this.state}`);
}
}
let myCase = new Suitcase(20, "Cosy");
В случае наследования через extends если конструктор дочернего класса не указан, то автоматически будет вызван конструктор родительского. Явно вызвать конкструктор родительского класса super можно только внутри конструктора дочернего. При этом this не будет существовать до вызова super.
Символ (анг. Symbol) — это уникальный и неизменяемый тип данных, который может быть использован как идентификатор для свойств объектов.
Синтаксис создания символа:
let symbol1 = Symbol();
let symbol2 = Symbol("my symbol");
let symbol3 = Symbol("my symbol");
console.log(symbol2 == symbol3); // false
Символ создается без ключевого слова new явно указывая на то, что символ — примитивный тип. Можно передать необязательный параметр-строку описания, который можно использовать для отладки, но который нельзя использовать для идентификации символа.
Символы, созданные с передачей только описания, не будут доступны глобально. В случае необходимости создания символа, доступного из разных частей программы, используют глобальный символьный реестр.
let symbolGlobal = Symbol.for("my symbol");
console.log(symbolGlobal == Symbol.for("my symbol")); // true
Символы, созданые через Symbol.for доступны глобально, при передаче одного и того же аргумента-имени символа в for символы равны.
Метод keyFor позволяет получить имя символа:
let symbolGlobal = Symbol.for("my symbol");
console.log(Symbol.keyFor(symbolGlobal)); // my symbol
Для неглобального символа keyFor вернет undefined.
Символы могут использоваться для создания вычислимых свойств объектов.
let secretProp = Symbol("secret");
let suitcase = {
volume: 20,
[secretProp]: "secret value"
};
Такое свойство не будет перечислимым.
Свойства-символы используются разработчиками языка JavaScript. Список известных символов приведен здесь, они обозначаются как @@SYMBOL_NAME, но доступны как свойства Symbol. При добавлении новых свойств, если давать им имена в виде строк, то существует риск переопределить свойства, созданные разработчиком программы. Потому было принято ввести символьный тип и для добавления нового функционала объектам использовать его.
Т.к. свойства, созданные с помощью символов, неперечислимы, то для получения списка таких свойств используют Object.getOwnPropertySymbols.
В стандарт было добавлено понятие "итерабельности" (перечислимости) объектов как способности пройти по свойствам объекта в цикле. К итерабельным относятся как классические массивы, так и псевдо-массивы типа arguments или коллекций DOM-элементов.
Для перебора таких объектов используется цикл for..of:
let myArray = [1, 2, 3, 4, 5];
for (let elem of myArray) {
console.log(elem);
}
Отличие такого цикла от for..in в том, что если, например, массиву будут добавлены какие-то дополнительные свойства, то при использовании for..in эти свойства также будут отображены (если не блокировать enumerable для них в дескрипторе). for..of использует собственный алгоритм прохода, потому даже перечислимые свойства массива не будут отображены.
Такие типы как String, Array, Map, Set имеют встроенный итератор, т.е. их можно перебирать с помощью for ..of. Для массива будут выведены только элементы, а для строки - ее символы, как если бы использовался массив символов:
let myString = "welcome";
for (let symb of myString) {
console.log(symb);
}
Использование for..of гарантирует, что в перечисление не попадет ничего "лишнего".
Итерируемым можно сделать любой объект. Для этого объекту нужно добавить метод Symbol.iterator, который будет возвращать объект с методом next. В свою очередь next должен возвращать объект с двумя свойствами: value - значение на текущей итерации и done - признак конца итерирования. Все это называется протоколом итерирования и реализуется следующим образом.
Допустим, есть объект с двумя свойствами start и finish. Создадим для него итератор, который будет последовательно выводить целые числа в диапазоне от start до finish:
let counter = {
start: 0,
finish: 10
};
counter[Symbol.iterator] = function () {
let ctx = this;
let i = ctx.start - 1;
let iterator = {
next() {
i++;
return {
done: i > ctx.finish,
value: i
}
}
}
return iterator;
};
for (let value of counter) {
console.log(value);
}
При запуске перебора for..of код запускает counter[Symbol.iterator] и получает next, который запускает на каждом шаге, пока возвращаемый done не станет равным true.
Если указать finish как Infinity или в коде итератора задать done: false, то можно получить бесконечный итератор. В этом случае необходимо останавливать его внутри for..of с помощью break.
ES6 предлагает новые типы коллекций: Map, Set, WeakMap и WeakSet.
При создании свойств для объекта их названия приводятся к строке. Использование новых видов коллекций помогает обойти эти ограничения.
Map — это простой ассоциативный массив. Любые примитивы и объекты могут быть как ключами, так и значениями такого массива. В качестве параметра может принимать любой итерируемый объект, содержащий пары ключ-значение, на основании которого будет создан новый Map.
let emptyMap = new Map();
let fullMap = new Map([
['first key', 'first value'],
[{key: 'second key'}, 'second value'],
[null, 'third value'],
]);
Для работы с Map используются следующие свойства и методы:
Метод set возвращает текущую коллекцию, благодаря чему вызовы set можно чейнить.
При переборе в итераторе элементы коллекции идут в том же порядке, в котором они были добавлены. Перебор свойств в обычных объектах не мог гарантировать этого.
Set — коллекция для хранения уникальных значений, как примитивов, так и объектов.
let myObject = {myProp: 'bI'};
let emptySet = new Set();
let fullSet = new Set([
'first value',
{key: 'second value'},
myObject,
'forth value',
'first value',
{key: 'second value'},
myObject
]);
Как и для Map, для Set в качестве необязательного аргумента можно передать итерируемый объект, из которого будет создан новый Set.
Set сам контролирует уникальность элементов при добавлении. Приведенный выше код создаст коллекцию из 5 элементов, строка 'first value' и объект myObject повторно добавлены не будут.
Основные свойства и методы работы с коллекцией Set:
WeakMap и WeakSet — разновидности Map и Set, работа с которыми специальным образом оптимизирована сборщиком мусора. Если какой-то объект есть только в коллекции WeakMap или WeakSet, то он удаляется из памяти.
Например, WeakMap может использоваться как дополнительное хранилище свойств объекта, которое при удалении объекта так же необходимо очистить. Ключами WeakMap могут быть только объекты.
let stuff = new WeakMap();
let developer = { htmlSkill: 5};
stuff.set(developer, { age: 66 });
console.log( stuff.get(developer) );
delete window.developer; // удалит элемент и из stuff
Т.е. при использовании WeakMap нет необходимости удалять вручную привязанные дополнительные свойства.
WeakMap имеет ряд особенностей:
По прежнему можно удалять элементы через delete и проверять через has.
Недоступность коллекции обусловлено особенностью WeakMap. Сборщик мусора может модифицировать этот объект в любой момент времени, потому невозможно получить его достовеное состояние.
Объект Promise (обещание) используется для отложенных и асинхронных вычислений.
Promise может находиться в трёх состояниях:
При изменении состояния промиса происходит либо событие onFulfilled, либо onRejected.
При необходимости добавления асинхронного кода его помещают в Promise, а из основного кода навешивают обработчики событий, которые отработают при изменении состояния промиса:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Done!");
}, 1000);
});
promise.then(
result => {
console.log(result);
},
error => {
console.error(error);
}
);
Метод then промиса получает два атрибута: на первом месте функция-callback для состояния "fulfilled", на втором месте - callback для ошибки. Для перехвата только ошибки можно использовать метод catch.
Генерация ошибки через throw внутри промиса сгенерирует вызов callback-функции error
Изменить состояние промиса после перехода в fulfilled/rejected нельзя. Если после запуска resolve вызвать reject, но состояние промиса не поменяется и соответствующий callback не будет вызван.
Промисы удобно использовать для любого асинхронного кода, в т.ч. и для ajax-запросов:
let ajax = (url) => {
let ajaxPromise = new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open("GET", url, true);
request.onload = () => {
if (this.status == 200) {
resolve(this.response);
} else {
let error = new Error(this.statusText);
error.code = this.status;
reject(error);
}
};
request.onerror = () => {
reject(new Error("Connection error"));
};
request.send();
});
return ajaxPromise;
};
let ajaxExample = ajax("http://example.com");
ajaxExample.then(console.log).catch(console.error);
Промисы так же можно делать вложенными. Для этого необходимо, чтобы результат в then возвращался в виде промиса:
let promises = new Promise((f, r) => {
setTimeout(() => {
f("Hello");
}, 1000);
});
promises.then(result => {
return new Promise((f2, r2) => {
setTimeout(() => {
f2(result + ", world!");
}, 1000);
});
}).then(result2 => {
console.log(result2);
});
При возникновении ошибки внутри цепочки промисов ее перехватит ближайший catch. Если ошибка некритичная, то catch передаст управление следующему then, в противном случае catch может сгенерировать ошибку, и управление перейдет на следующий catch.
С помощью нескольких then можно не только чейнить промисы, но и навешивать несколько обработчиков на один промис.
У конструктора Promise есть статические методы:
Генераторы — новый вид функций, способных приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени. Для создания такой функции используют *:
function* myFunc() {}
При вызове такой функции-генератора она не выполнится, но вернет специальный объект-генератор. У такого объекта есть метод next для "продвижения" внутри функции-генератора, который выполняет код внутри нее до ближайшего ключевого слова yield и возвращает текущее значение:
function* myFunc() {
let i = 0;
console.log(`Checkpoint ${i}`);
yield i;
i++;
console.log(`Checkpoint ${i}`);
yield i;
i++;
console.log(`Checkpoint ${i}`);
yield i;
i++;
console.log(`Checkpoint ${i}`);
return i;
}
let generator = myFunc();
generator.next(); // {value: 0, done: false}
generator.next(); // {value: 1, done: false}
generator.next(); // {value: 2, done: false}
generator.next(); // {value: 3, done: true}
Звездочку можно ставить как сразу после ключевого слова function* (), так и сразу перед скобками function *().
Наличие метода next указывает на то, что генератор является итератором. Но при проходе через for..of значение из return не будет возвращено. Чтобы его получить необходимо заменить return на yield
Генераторы могут содержать в себе другие генераторы. Такая техника называется композицией генераторов.
Например, имеется функция-генератор для создания ряда чисел. С помощью оператора "троеточие" такой ряд модно преобразовать к массиву:
function* genRow(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
let arr = [...genRow(1,5)];
console.log(arr); // 1, 2, 3, 4, 5
Если необходимо свормировать несколько таких рядов, то можно обернуть запуск генератора в другой генератор:
function* genRow(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
function* moreRows() {
yield* genRow(10, 15);
yield* genRow(20, 25);
yield* genRow(30, 35);
}
let arr = [];
for(let row of moreRows()) {
arr.push(row);
}
console.log(arr); // 10..15,20..25,30..35
Конструкция yield* выполняет генератор и все его yield и возвращает промежуточное значение в for..of.
Модули — удобный способ организации частей большой программы.
Функционал модулей был востребован давно, для его реализации использовались различные специальные библиотеки: require.js, common.js и т.д.
Модулем называется файл с кодом, который можно импортировать в другую часть программы. Для организации работы с модулями используются понятия экспорта и импорта.
Ключевое слово export ставят либо перед переменными, которые нужно экспортировать, либо отдельно (чаще всего - в конце файла) с перечислением переменных для экспорта в фигурных скобках:
export let someValue = 911;
let someValue = 911;
export {someValue};
Данный пример экспортирует во внешний файл одну переменную. Для экспорта нескольких переменных использую запятую.
let someValue = 911;
let anotherValue = 38;
export {someValue, anotherValue};
С помощью ключевого слова as можно указать имена, под которым переменная будет доступна в импортирующем коде:
let someValue = 911;
let anotherValue = 38;
export {someValue as var1, anotherValue as var2};
Нельзя экспортировать безымянные функции и классы.
Чтобы подключить модуль используют ключевое слово import:
import {someValue, anotherValue} from "./values";
В фигурных скобках перечисляются переменные для импорта, а после from указывают путь к файлу импорта без расширения. Для приведенного примера в подключающем файле будут доступны переменные someValue и anotherValue со своими значениями из файла-модуля.
При импорте так же можно переименовать импортируемые переменные:
import {someValue as var1, anotherValue as var2} from "./values";
Получить все переменные в виде объекта можно, воспользовавшись *:
import * as values from "./values";
Если из модуля необходимо импортировать единственное значение, то передать его з файла модуля можно с помощью export default:
export default let someValue = 911;
В импортирующем файле в этом случае можно не указывать фигурные скобки:
import someValue from "./values";