Skip to content

Asver77/Lesson-JavaScript

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 

Repository files navigation

Памятка по современному JavaScript

Памятка по современному JavaScript

Дополнительные ресурсы

Если вам сложно разобраться с каким-то понятием, рекомендую искать ответы на вопросы на следующих ресурсах:

Содержание

Понятия

Объявление переменных: var, const, let

В JavaScript есть три ключевых слова, отвечающих за объявление переменных, и у каждого из них свои особенности. Эти слова − var, let и const.

Краткое объяснение

Переменным, объявленным с помощью ключевого слова const, нельзя позже присвоить новое значение, в то время как переменным, объявленным с помощью let или var, можно.

Я рекомендую всегда объявлять переменные ключевым словом const, а let использовать только в том случае, если позже эту переменную понадобится изменить или переопределить.

Область видимости Можно переопределять Можно изменять Временная мертвая зона
`const` Блок Нет Да Да
`let` Блок Да Да Да
`var` Функция Да Да Нет

Пример кода

const person = "Коля";
person = "Ваня" // Вызовет ошибку, переменной person нельзя присвоить новое значение.
let person = "Коля";
person = "Ваня";
console.log(person) // -> "Ваня", присвоение нового значения разрешено в случае с let.

Подробное объяснение

Область видимости переменной определяет, где эта переменная доступна в коде.

var

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

Можно думать об этом вот так: если у переменной область видимости Х, то эта переменная — как бы свойство Х.

function myFunction() {
  var myVar = "Коля";
  console.log(myVar); // -> "Коля" — myVar доступна внутри функции.
}
console.log(myVar); // ReferenceError, myVar недоступна снаружи функции.

Вот менее очевидный пример области видимости переменных:

function myFunction() {
  var myVar = "Коля";
  if (true) {
      var myVar = "Ваня";
      console.log(myVar); // -> "Ваня"
      /* На самом деле, область видимости myVar — функция,
      мы всего лишь удалили предыдущее значение переменной myVar "Коля"
      и заменили его на "Ваня". */
    }
    console.log(myVar); // -> "Ваня" — обратите внимание, как код в блоке if повлиял на это значение.
  }
  console.log(myVar); // ->  
  /* ReferenceError, переменная myVar недоступна
  за пределами функции, в которой определена. */

Кроме этого, переменные, объявленные с помощью ключевого слова var, при выполнении кода перемещаются в начало области видимости. Это называется поднятие переменных.

Этот фрагмент кода:

console.log(myVar) // -> undefined — ошибок нет.
var myVar = 2;

при выполнении понимается как:

var myVar;
console.log(myVar) // -> undefined — ошибок нет.
myVar = 2;
let

var и let примерно одинаковы, в то время как переменные, объявленные словом let:

  • имеют в качестве области видимости блок;
  • недоступны до объявления;
  • не могут быть повторно объявлены в той же области видимости.

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

function myFunction() {
  let myVar = "Коля";
  if (true) {
    let myVar = "Ваня";
    console.log(myVar); // -> "Ваня"
    /* Поскольку myVar имеет блочную область видимости,
    здесь мы только что создали новую переменную myVar.
    Эта переменная недоступна вне блока и никак не зависит
    от первой переменной myVar, которую мы создали до этого! */
  }
  console.log(myVar); // -> "Коля" — обратите внимание: инструкции в блоке if НЕ повлияли на значение переменной.
}
console.log(myVar); // -> ReferenceError, myVar недоступна за пределами функции.

Теперь разберемся, что значит «переменные, объявленные с помощью let и const, недоступны до их объявления»:

console.log(myVar) // Вызовет ReferenceError!
let myVar = 2;

В отличие от переменных, объявленных через var, попытка обратиться к переменной let или const до её объявления вызовет ошибку. Этот феномен часто называют Временной мёртвой зоной.

Примечание: строго говоря, объявления переменных с использованием let и const тоже поднимаются, однако их инициализация — нет. Они сделаны так, что использовать их до инициализации нельзя. Поэтому интуитивно кажется, что такие переменные не поднимаются, но на самом деле это не так. Больше информации можно найти в этом очень подробном объяснении.

В дополнение к сказанному: нельзя повторно объявить переменную, объявленную с помощью let:

let myVar = 2;
let myVar = 3; // Вызовет SyntaxError.
const

Переменные, объявленные через const, ведут себя так же, как переменные, объявленные через let, но к тому же их нельзя переопределять.

Итак, переменные, объявленные с помощью const:

  • имеют в качестве области видимости блок;
  • недоступны до объявления;
  • не могут быть повторно объявлены в той же области видимости;
  • не могут быть переопределены.
const myVar = "Коля";
myVar = "Ваня" // Вызовет ошибку, переопределять переменную нельзя.
const myVar = "Коля";
const myVar = "Ваня" // Вызовет ошибку, объявить переменную можно только один раз.

Но есть одна тонкость: переменные, объявленные с помощью const, не являются неизменными! А именно, это означает, что объекты и массивы, объявленные с помощью const, могут быть изменены.

В случае объектов:

const person = {
  name: 'Коля',
};
person.name = 'Ваня'; // Сработает! Переменная person не полностью переопределяется, а просто меняется.
console.log(person.name); // -> "Ваня"
person = "Сандра"; // Вызовет ошибку, потому что переменные, объявленные через const, переопределять нельзя.

В случае массивов:

const person = [];
person.push('Ваня'); // Сработает!  Переменная person не полностью переопределяется, а просто меняется.
console.log(person[0]); // -> "Ваня"
person = ["Коля"]; // Вызовет ошибку, потому что переменные, объявленные через const, переопределять нельзя.

Дополнительные материалы

Стрелочные функции

В обновлении JavaScript ES6 добавлены стрелочные функции — новый синтаксис записи функций. Вот некоторые их преимущества:

  • краткость;
  • this берется из окружающего контекста;
  • неявный возврат.

Пример кода

  • Краткость и неявный возврат.
function double(x) { return x * 2; } // Обычный способ.
console.log(double(2)); // -> 4
const double = x => x * 2; /* Та же функция, записанная в виде стрелочной функции с неявным возвратом. */
console.log(double(2)); // -> 4
  • Использование this.

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

function myFunc() {
  this.myVar = 0;
  setTimeout(() => {
    this.myVar++;
    console.log(this.myVar); // -> 1
  }, 0);
}

Подробное объяснение

Краткость

Стрелочные функции во многих отношениях более краткие, чем обычные. Рассмотрим все возможные случаи:

  • Явный и неявный возврат.

Функция может явно возвращать результат с использованием ключевого слова return.

function double(x) {
  return x * 2; // Эта функция явно возвращает x * 2, использовано ключевое слово *return*.
}

При обычном способе написания функций возврат всегда был явным. Со стрелочными функциями его можно сделать неявным. Это значит, что для возврата значения не нужно использовать ключевое слово return.

const double = (x) => {
  return x * 2; // Явный возврат.
}

Поскольку здесь нет ничего, кроме возвращаемого значения, можно вернуть значение без явного указания.

const double = (x) => x * 2; // Всё верно, вернётся x * 2.

Для этого нам просто нужно убрать фигурные скобки и ключевое слово return. Поэтому это и называется неявным возвратом: ключевого слова return нет, но функция все равно вернет x * 2.

Примечание: Если ваша функция не возвращает никакого значения (с побочными эффектами), то в ней нет ни явного, ни неявного возврата.

Кроме того, если вы хотите неявно вернуть объект, вы должны заключить его в круглые скобки, так как иначе он будет конфликтовать с фигурными скобками блоков:

const getPerson = () => ({ name: "Коля", age: 24 })
console.log(getPerson())
// { name: "Коля", age: 24 } — объект, неявно возвращенный стрелочной функцией.
  • Только один аргумент.

Если ваша функция принимает только один аргумент, то скобки вокруг него можно опустить. Возвращаясь к функции double в коде выше:

const double = (x) => x * 2; // Эта стрелочная функция принимает только один аргумент.

Скобки вокруг этого аргумента можно опустить:

const double = x => x * 2; // Эта стрелочная функция принимает только один аргумент.
  • Без аргументов.

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

() => { // Скобки есть, все хорошо.
  const x = 2;
  return x;
}
=> { // Скобок нет, так работать не будет!
  const x = 2;
  return x;
}
Использование this

Чтобы понять эту тонкость поведения стрелочных функций, нужно понимать, как this ведёт себя в JavaScript.

Внутри стрелочной функции значение this равно значению this внешнего окружения. Это значит, что стрелочная функция не создает новый this, а получает его из окружения.

Без использования стрелочных функций для получения доступа к переменной через this в функции, вложенной в другую функцию, придется использовать хак that = this или self = this.

Вот, к примеру, использование функции setTimeout внутри функции myFunc:

function myFunc() {
  this.myVar = 0;
  var that = this; // Тот самый хак *that = this*
  setTimeout(
    function() { // В этой области видимости функции создается новый *this*.
      that.myVar++;
      console.log(that.myVar); // -> 1
      console.log(this.myVar); // -> undefined — см. объявление функции выше.
    },
    0
  );
}

Но в случае стрелочных функций this берется из окружения:

function myFunc() {
  this.myVar = 0;
  setTimeout(
    () => { // this берется из окружения. В данном случае — из myFunc.
      this.myVar++;
      console.log(this.myVar); // -> 1
    },
    0
  );
}

Полезные ресурсы

Значение аргументов функции по умолчанию

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

function myFunc(x = 10) {
  return x;
}
console.log(myFunc()); /* -> 10 — никакое значение не передается,
поэтому в myFunc х присваивается значение по умолчанию, т.е. 10 */
console.log(myFunc(5)); /* -> 5 — передается значение,
поэтому в myFunc х присваивается значение 5 */

console.log(myFunc(undefined)); /* -> 10 — передается значение undefined,
поэтому х присваивается значение по умолчанию */
console.log(myFunc(null)); // -> null — передается значение null. Подробнее см. ниже.

Значения по умолчанию применяются только в двух случаях:

  • значение не передано;
  • передано значение undefined.

Другими словами, если передать в функцию параметр null, то параметр по умолчанию не применится.

Примечание: Присваивать значение по умолчанию можно в том числе и при работе с деструктурированными параметрами (см. пример в следующем понятии).

Дополнительные материалы

Деструктуризация объектов и массивов

Деструктуризация — это удобный способ создания новых переменных путем извлечения значений из объектов или массивов.

На практике деструктуризацию можно использовать, чтобы присваивать переменным разбитые на части параметры функции или this.props в React-проектах.

Объяснение с помощью примера кода

  • Объект.

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

const person = {
  firstName: "Коля",
  lastName: "Андреев",
  age: 35,
  sex: "М",
};

Без деструктуризации:

const first = person.firstName;
const age = person.age;
const city = person.city || "Санкт-Петербург";

С деструктуризацией всё поместится в одну строку:

const { firstName: first, age, city = "Санкт-Петербург" } = person; // И всё!
console.log(age); /* -> 35 — Создана новая переменная age,
и ей присвоено значение, равное person.age. */
console.log(first); /* -> "Коля" — Создана новая переменная first,
и ей присвоено значение, равное person.firstName. */
console.log(firstName); /* -> ReferenceError — person.firstName существует,
НО новая созданная переменная называется first. */
console.log(city); /* -> "Санкт-Петербург" — Создана новая переменная city,
и, поскольку свойство person.city ранее не было определено,
переменной присвоено альтернативное значение "Санкт-Петербург". */

Примечание: В const { age } = person; скобки после ключевого слова const используются не для обозначения объекта или блока. Это синтаксис деструктуризации.

  • Параметры функции.

Деструктуризация часто используется для разбиения параметров функции на части.

Без деструктуризации:

function joinFirstLastName(person) {
  const firstName = person.firstName;
  const lastName = person.lastName;
  return `${firstName}${lastName}`;
}
joinFirstLastName(person); // -> "Коля-Андреев"

Если деструктурировать параметр person, то функция получится куда более лаконичной:

function joinFirstLastName({ firstName, lastName }) { /* Мы создали переменные firstName и lastName
  из частей параметра person. */
  return `${firstName}${lastName}`;
}
joinFirstLastName(person); // -> "Коля-Андреев"

Ещё удобнее использовать деструктуризацию со стрелочными функциями:

const joinFirstLastName = ({ firstName, lastName }) => `${firstName}${lastName}`;
joinFirstLastName(person); // -> "Коля-Андреев"
  • Массив.

Давайте рассмотрим следующий массив:

const myArray = ["a", "b", "c"];

Без деструктуризации:

const x = myArray[0];
const y = myArray[1];

С использованием деструктуризации:

const [x, y] = myArray; // Вот и всё!

console.log(x); // -> "a"
console.log(y); // -> "b"

Полезные ресурсы

Методы массивов — map / filter / reduce

map, filter и reduce — это методы массивов, пришедшие из парадигмы функционального программирования.

Перечислю их:

  • Array.prototype.map() принимает массив, каким-нибудь образом преобразует его элементы и возвращает новый массив трансформированных элементов.
  • Array.prototype.filter() принимает массив, просматривает каждый элемент и решает, убрать его или оставить. Возвращает массив оставшихся значений.
  • Array.prototype.reduce() принимает массив и вычисляет на основе его элементов какое-то единое значение, которое и возвращает.

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

Вооружившись этими тремя методами, вы можете обойтись без использования for и forEach в большинстве ситуаций. Когда в следующий раз соберётесь запустить цикл for, попробуйте решить задачу с помощью map, filter и reduce. Поначалу это будет трудно, потому что вам придётся научиться мыслить по-другому, но, разобравшись один раз, вы сможете применять эти методы без особых усилий.

Пример кода

const numbers = [0, 1, 2, 3, 4, 5, 6];
const doubledNumbers = numbers.map(n => n * 2); // -> [0, 2, 4, 6, 8, 10, 12]
const evenNumbers = numbers.filter(n => n % 2 === 0); // -> [0, 2, 4, 6]
const sum = numbers.reduce((prev, next) => prev + next, 0); // -> 21

Давайте посчитаем сумму баллов всех студентов, которые набрали больше 10 баллов, используя map, filter и reduce:

const students = [
  { name: "Коля", grade: 10 },
  { name: "Ваня", grade: 15 },
  { name: "Юля", grade: 19 },
  { name: "Наташа", grade: 9 },
];

const aboveTenSum = students
  .map(student => student.grade) // Создаём массив оценок из массива студентов с помощью метода map.
  .filter(grade => grade >= 10) // Выбираем только оценки выше 10 при помощи метода filter.
  .reduce((prev, next) => prev + next, 0); // Суммируем все оценки выше 10 друг с другом.

console.log(aboveTenSum); /* -> 44: 10 (Коля) + 15 (Ваня) + 19 (Юля),
оценка Наташи меньше 10 и была проигнорирована */

Объяснение

Давайте использовать в качестве примера следующий массив:

const numbers = [0, 1, 2, 3, 4, 5, 6];
Array.prototype.map()
const doubledNumbers = numbers.map(function(n) {
  return n * 2;
});
console.log(doubledNumbers); // -> [0, 2, 4, 6, 8, 10, 12]

Что же здесь происходит? Мы применяем к массиву numbers метод map, который взаимодействует с каждым элементом массива, передавая его в нашу функцию. Цель функции — произвести расчёт и вернуть новое значение, чтобы map мог подставить его вместо переданного в функцию.

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

const doubleN = function(n) { return n * 2; };
const doubledNumbers = numbers.map(doubleN);
console.log(doubledNumbers); // -> [0, 2, 4, 6, 8, 10, 12]

numbers.map(doubleN) создаёт [doubleN(0), doubleN(1), doubleN(2), doubleN(3), doubleN(4), doubleN(5), doubleN(6)], что равняется [0, 2, 4, 6, 8, 10, 12].

Примечание: Если вам не нужно возвращать новый массив и вы просто хотите перебрать существующий массив, совершая с его элементами некоторые действия, можете просто использовать for / forEach вместо метода map.

Array.prototype.filter()
const evenNumbers = numbers.filter(function(n) {
  return n % 2 === 0; // Истинно, если n чётное; ложно, если n нечётное.
});
console.log(evenNumbers); // -> [0, 2, 4, 6]

Мы применяем filter к массиву numbers. Метод filter взаимодействует с каждым элементом массива и передаёт его в нашу функцию. Функция возвращает булево значение, определяющее, будет ли элемент сохранён в массиве. Затем filter возвращает массив отфильтрованных значений.

Array.prototype.reduce()

Цель метода reduce заключается в том, чтобы вычислить на основе массива какое-то одно значение. Какие именно вычисления метод произведет с элементами, зависит только от вас.

const sum = numbers.reduce(
  function(acc, n) {
    return acc + n;
  },
0 // Значение аккумулирующей переменной на первом шаге цикла.
);
console.log(sum); // -> 21

Так же, как методы .map и .filter, метод .reduce применяется к массиву и в качестве первого параметра принимает функцию.

На этот раз, впрочем, кое-что изменилось:

  • .reduce принимает два параметра.

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

Второй параметр — это значение аккумулирующей переменной (acc в нашем случае) на первом шаге цикла (чтобы разобраться, читайте далее).

  • Параметры функции.

Функция, которую вы передаёте в качестве первого параметра метода .reduce, принимает два аргумента. Первый аргумент — это аккумулирующая переменная (acc в нашем примере), второй аргумент — текущий элемент.

Аккумулирующая переменная равна значению, возвращённому нашей функцией на предыдущем шаге цикла. В самом начале каждого цикла acc равна значению, которое было передано в качестве второго параметра .reduce.

На первом шаге

acc = 0, потому что мы передали 0 в качестве второго параметра метода reduce.

n = 0 — первый элемент массива number.

Функция возвращает acc + n --> 0 + 0 --> 0.

На втором шаге

acc = 0, потому что это значение функция вернула на предыдущем шаге.

n = 1 — второй элемент массива number.

Функция возвращает acc + n --> 0 + 1 --> 1.

На третьем шаге

acc = 1, потому что это значение функция вернула на предыдущем шаге.

n = 2 — третий элемент массива number.

Функция возвращает acc + n --> 1 + 2 --> 3.

На четвертом шаге

acc = 3, потому что это значение функция вернула на предыдущем шаге.

n = 3 — четвёртый элемент массива number.

Функция возвращает acc + n --> 3 + 3 --> 6.

На последнем шаге

acc = 15, потому что это значение функция вернула на предыдущем шаге.

n = 6 — последний элемент массива number.

Функция возвращает acc + n --> 15 + 6 --> 21.

Поскольку это был последний шаг, .reduce возвращает 21.

Дополнительные материалы

Оператор расширения ...

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

Пример кода

const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // -> ["a", "b", "c", "d", "e", "f"]
function myFunc(x, y, ...params) {
  console.log(x); // -> "a"
  console.log(y); // -> "b"
  console.log(params); // -> ["c", "d", "e", "f"]
}

myFunc("a", "b", "c", "d", "e", "f");
// "a"
// "b"
// ["c", "d", "e", "f"]
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // -> 1
console.log(y); // -> 2
console.log(z); // -> { a: 3, b: 4 }
const n = { x, y, ...z };
console.log(n); // -> { x: 1, y: 2, a: 3, b: 4 }

Объяснение

В итерируемых объектах (например, массивах)

Если у нас есть два следующих массива:

const arr1 = ["a", "b", "c"];
const arr2 = [arr1, "d", "e", "f"]; // -> [["a", "b", "c"], "d", "e", "f"]

Первый элемент массива arr2 — это массив, потому что arr1 напрямую вставляется в arr2. Но мы хотим, чтобы arr2 состоял только из букв. Чтобы добиться этого, мы можем развернуть элементы массива arr1 в массиве arr2.

С использованием оператора расширения:

const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // -> ["a", "b", "c", "d", "e", "f"]
Оставшиеся аргументы функции

Для объединения аргументов можно использовать оператор оставшихся аргументов функции. Этот оператор позволяет представить любое число аргументов в виде массива, элементы которого можно перебрать при помощи цикла. Вообще, к каждой функции уже привязан объект arguments — массив, состоящий из всех аргументов, переданных функции.

function myFunc() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(arguments[i]);
  }
}
myFunc("Коля", "Андреев", 10, 12, 6);
// "Коля"
// "Андреев"
// 10
// 12
// 6

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

Именно это позволяет нам сделать оператор оставшихся аргументов!

function createStudent(firstName, lastName, ...grades) {
  /* firstName = "Коля"
  lastName = "Андреев"
  [10, 12, 6] — оператор `...` берет все остальные параметры, переданные функции,
  и создает переменную grades с массивом, в котором они хранятся. */

  const avgGrade = grades.reduce((acc, curr) => acc + curr, 0) / grades.length;
  // Вычисляет средний балл из всех оценок.

  return {
    firstName: firstName,
    lastName: lastName,
    grades: grades,
    avgGrade: avgGrade,
  }
}

const student = createStudent("Коля", "Андреев", 10, 12, 6);
console.log(student);
/* {
firstName: "Коля",
lastName: "Андреев",
grades: [10, 12, 6],
avgGrade: 9,33
} */

Примечание: createStudent — плохая функция, потому что мы не проверяем, существует ли grades.length и отличается ли от 0. Но так функцию легче прочитать, поэтому я не учитывал эти случаи.

Расширение свойств объектов

Чтобы понять эту часть, рекомендую прочитать предыдущие объяснения о применении оператора оставшихся аргументов к итерируемым объектам и параметрам функций.

const myObj = { x: 1, y: 2, a: 3, b: 4 };
const { x, y, ...z } = myObj; // Деструктуризация объекта.
console.log(x); // -> 1
console.log(y); // -> 2
console.log(z); // -> { a: 3, b: 4 }

// z - это остаток от деструктурированного объекта: объект myObj минус деструктурированные свойства х и у.

const n = { x, y, ...z };
console.log(n); // -> { x: 1, y: 2, a: 3, b: 4 }

// Здесь свойства объекта z расширяются в n

Дополнительные материалы

Сокращенная запись свойств объектов

При записи переменной в свойство объекта, если у переменной то же имя, что и у свойства, можно сделать следующее:

const x = 10;
const myObj = { x };
console.log(myObj.x) // -> 10

Объяснение

Раньше (до ES2015), если вы хотели при объявлении нового литерала объекта использовать переменные в качестве его свойств, вам пришлось бы писать подобный код:

const x = 10;
const y = 20;

const myObj = {
  x: x, // Запись значения переменной х в myObj.x.
  y: y, // Запись значения переменной у в myObj.y.
};

console.log(myObj.x); // -> 10
console.log(myObj.y); // -> 20

Как видите, приходится повторять одно и то же, потому что имена свойств объекта совпадают с именами переменных, которые вы хотите записать в эти свойства.

С ES2015, если имя переменной совпадает с именем свойства, можно использовать такую сокращенную запись:

const x = 10;
const y = 20;

const myObj = {
  x,
  y,
};

console.log(myObj.x); // -> 10
console.log(myObj.y); // -> 20

Дополнительные материалы

Промисы

Промис (promise) — это объект, который может быть синхронно возвращён из асинхронной функции (Ссылка).

Промисы могут использоваться, чтобы избежать «ада обратных вызовов», и они всё чаще и чаще используются в современных JavaScript-проектах.

Пример кода

const fetchingPosts = new Promise((res, rej) => {
  $.get("/posts")
  .done(posts => res(posts))
  .fail(err => rej(err));
});

fetchingPosts
  .then(posts => console.log(posts))
  .catch(err => console.log(err));

Пояснение

Когда вы делаете AJAX-запрос, ответ будет несинхронным, так как вы запрашиваете ресурс, на обработку которого требуется некоторое время. Ответ может быть вообще не получен, если запрашиваемый ресурс недоступен по какой-то причине (404).

Чтобы избежать таких ситуаций, в ES2015 были добавлены промисы. Промисы могут иметь 3 различных состояния:

  • выполняется;
  • выполнено;
  • отклонено.

Предположим, мы хотим использовать промисы для обработки AJAX-запроса для получения ресурса X.

Создание промиса

Сначала создадим промис. Будем использовать GET-метод jQuery для создания AJAX-запроса к ресурсу X.

const xFetcherPromise = new Promise(
// Создаём промис с помощью ключевого слова new и сохраняем его в переменную
  function(resolve, reject) {
    /* Конструктор промиса принимает в виде параметра функцию, которая, в свою очередь,
    принимает 2 параметра: resolve и reject. */
    $.get("X") // Запускаем AJAX-запрос
      .done(function(X) { // Как только запрос выполнен...
        resolve(X); // ... выполняем промис со значением X в качестве параметра.
      })
      .fail(function(error) { // Если запрос не прошёл...
        reject(error); // ... отклоняем промис со значением error.
      });
  }
)

Как видно из рассмотренного примера, объект Promise принимает функцию-исполнитель, в свою очередь принимающую два параметра: resolve и reject. Эти параметры — функции, которые при вызове изменяют состояние промиса со значения выполняется на выполнено или отклонено соответственно.

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

Использование обработчиков промисов

Чтобы получить результат (или ошибку) промиса, нужно назначить ему обработчики следующим образом:

xFetcherPromise
  .then(function(X) {
    console.log(X);
  })
  .catch(function(err) {
    console.log(err);
  })

Если вызов прошёл успешно, вызывается resolve и выполняется функция, переданная в метод .then.

Если вызов не прошёл, вызывается reject и выполняется функция, переданная в .catch.

Примечание: Если промис уже выполнен или отклонён на момент назначения соответствующего обработчика, обработчик всё равно будет вызван. Так что между выполнением асинхронной операции и назначением обработчиков не возникает состояние гонки. (Ссылка: MDN)

Дополнительные материалы

Шаблонные строки

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

Другими словами, это новый синтаксис записи строк, с которым удобно использовать любые выражения JavaScript (например, переменные).

Пример кода

const name = "Коля";
`Привет, ${name}, следующее выражение равно четырем: ${2+2}.`;

// -> Привет, Коля, следующее выражение равно четырем: 4.

Дополнительные материалы

Тегированные шаблонные строки

Шаблонные теги — это функции, которые могут быть префиксом к шаблонной строке. Когда функция вызывается таким образом, первый параметр представляет собой массив строк, которые выводятся между интерполированными переменными, а последующие параметры — значения выражений, вставленных в строку. Для захвата всех этих значений используйте оператор расширения .... (Ссылка: MDN).

Примечание: Известная библиотека, которая называется стилизованные компоненты, основана на этой возможности.

Ниже приведен пример работы тегированных шаблонных строк:

function highlight(strings, ...values) {
  const interpolation = strings.reduce((prev, current) => {
    return prev + current + (values.length ? "<mark>" + values.shift() + "</mark>" : "");
  }, "");

  return interpolation;
}

const meal = "круассаны";
const drink = "кофе";

highlight`Я люблю ${meal} с ${drink}.`;
// -> <mark>Я люблю круассаны с кофе.</mark>

Более интересный пример:

function comma(strings, ...values) {
  return strings.reduce((prev, next) => {
    let value = values.shift() || [];
    value = value.join(", ");
    return prev + next + value;
  }, "");
}

const snacks = ["яблоки", "бананы", "апельсины"];
comma`Я люблю ${snacks} на десерт.`;
// -> Я люблю яблоки, бананы, апельсины на десерт.

Дополнительные материалы

Импорт / экспорт

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

Крайне рекомендую почитать ресурсы MDN об экспорте/импорте (см. Дополнительные материалы ниже), в них содержится четкая и полная информация.

Объяснение с примером кода

Именованный экспорт

Именованный экспорт используется для экспорта нескольких значений из модуля.

Примечание: Вы можете именовать экспорт только объектами первого класса, у которых есть имя.

// mathConstants.js
export const pi = 3.14;
export const exp = 2.7;
export const alpha = 0.35;

// -------------

// myFile.js
import { pi, exp } from './mathConstants.js';
// Именованный импорт — с синтаксисом, похожим на деструктуризацию.
console.log(pi) // -> 3.14
console.log(exp) // -> 2.7

// -------------

// mySecondFile.js
import * as constants from './mathConstants.js';
// Все экспортированные значения записываются в переменную constants.
console.log(constants.pi) // -> 3.14
console.log(constants.exp) // -> 2.7

Хотя именованный импорт выглядит как деструктуризация, это не одно и то же. Кроме того, именованный импорт имеет другой синтаксис, не поддерживает значения по умолчанию и глубокую деструктуризацию.

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

import { foo as bar } from 'myFile.js';
// foo импортируется и записывается в новую переменную bar.
Импорт / экспорт по умолчанию

Что касается экспорта по умолчанию, то для каждого модуля (файла) может быть только один экспорт. Экспортом по умолчанию может быть функция, класс, объект или что-то еще. Это значение считается «основным», поскольку его будет проще всего импортировать. Ссылка: MDN.

// coolNumber.js
const ultimateNumber = 42;
export default ultimateNumber;

// ------------

// myFile.js
import number from './coolNumber.js';
/* В переменную number автоматически попадает экспорт по умолчанию —
вне зависимости от его имени в исходном модуле. */
console.log(number) // -> 42

Экспорт функций:

// sum.js
export default function sum(x, y) {
  return x + y;
}

// -------------

// myFile.js
import sum from './sum.js';
const result = sum(1, 2);
console.log(result) // -> 3

Дополнительные материалы

this в JavaScript

Оператор this в JavaScript ведет себя не так, как в других языках. В большинстве случаев он определяется тем, как вызвана функция (Ссылка: MDN).

Это сложное понятие с множеством тонкостей, так что я крайне рекомендую вам тщательно изучить приведенные ниже Дополнительные материалы. Я покажу вам, как сам лично определяю, чему равно this. Этому меня научила вот эта статья Yehuda Katz.

function myFunc() {
  ...
}

// После каждого выражения находим значение this в myFunc.

myFunc.call("myString", "привет");
// myString — в this записывается значение первого параметра .call.

// В non-strict-режиме.
myFunc("привет");
// window — myFunc() — это синтаксический сахар для myFunc.call(window, "привет").

// В strict-режиме.
myFunc("привет");
// undefined — myFunc() — это синтаксический сахар для myFunc.call(undefined, "привет").
var person = {
  myFunc: function() { ... }
}

person.myFunc.call(person, "test");
// person Object — в this записывается значение первого параметра call.
person.myFunc("test");
// person Object — person.myFunc() — это синтаксический сахар для person.myFunc.call(person, "test").

var myBoundFunc = person.myFunc.bind("привет");
// Создает новую функцию, в которой мы записываем "привет" в значение this.
person.myFunc("test");
// person Object — Метод bind не влияет на первоначальный метод.
myBoundFunc("test");
// "hello" — myBoundFunc — это person.myFunc, в которой this привязана к "привет".

Дополнительные материалы

Класс

JavaScript — это язык, основанный на прототипах (в то время как, например, Java — язык, основанный на классах). В обновлении ES6 представлены классы JavaScript, которые являются синтаксическим сахаром для наследования на основе прототипов, а не новой моделью наследования на основе классов (Ссылка).

Если вы знакомы с классами в других языках, слово «класс» может ввести вас в заблуждение. Постарайтесь не делать предположений о работе классов в JavaScript на основе других языков. Считайте это совершенно другим понятием.

Поскольку этот документ не является попыткой научить вас языку с нуля, я надеюсь, что вы знаете, что такое прототипы и как они себя ведут. Если нет, смотрите дополнительные материалы после примеров.

Примеры

До ES6, синтаксис на основе прототипов:

var Person = function(name, age) {
  this.name = name;
  this.age = age;
};
Person.prototype.stringSentence = function() {
  return "Привет, меня зовут " + this.name + " и мне " + this.age;
};

Начиная с ES6, синтаксис на основе классов:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  stringSentence() {
    return `Привет, меня зовут ${this.name} и мне ${this.age}`;
  }
}

const myPerson = new Person("Маша", 23);
console.log(myPerson.age); // -> 23
console.log(myPerson.stringSentence()); // -> "Привет, меня зовут Маша и мне 23

Дополнительные материалы

Для понимания прототипов:

Для понимания классов:

Ключевые слова Extends и super

Ключевое слово extends используется в объявлении класса или в выражениях класса для создания дочернего класса (Ссылка: MDN). Дочерний класс наследует все свойства родительского класса и дополнительно может добавлять новые свойства или изменять унаследованные.

Ключевое слово super используется для вызова функций родителя объекта, включая его конструктор.

  • В конструкторе ключевое слово super должно использоваться раньше, чем ключевое слово this.
  • Вызов super() вызывает конструктор родительского класса. Если вы хотите передать какие-то аргументы из конструктора класса в конструктор родительского класса, то нужно вызывать функцию следующим образом: super(arguments).
  • Если у родительского класса есть метод X (даже статический), для его вызова в дочернем классе можно использовать super.X().

Пример кода

class Polygon {
  constructor(height, width) {
    this.name = 'Многоугольник';
    this.height = height;
    this.width = width;
  }

  getHelloPhrase() {
    return `Привет, я — ${this.name}`;
  }
}

class Square extends Polygon {
  constructor(length) {
    /* Здесь вызывается конструктор родительского класса со значением length,
    передаваемым для переменных width и height класса Polygon. */

    super(length, length);
    /* Примечание: в производных классах перед тем, как использовать 'this',
    нужно вызвать функцию super(), иначе это приведёт к ошибке. */

    this.name = 'Квадрат';
    this.length = length;
  }

  getCustomHelloPhrase() {
    const polygonPhrase = super.getHelloPhrase();
    // Получение доступа к родительскому методу с помощью синтаксиса super.X().
    return `${polygonPhrase} с длиной стороны ${this.length}`;
  }

  get area() {
    return this.height * this.width;
  }
}

const mySquare = new Square(10);
console.log(mySquare.area) // -> 100
console.log(mySquare.getHelloPhrase())
/* -> 'Привет, я — Квадрат'
Класс Square наследуется от класса Polygon и имеет доступ к его методам.*/
console.log(mySquare.getCustomHelloPhrase())
// -> 'Привет, я — Квадрат с длиной стороны'

Примечание: Если бы мы попытались использовать this перед вызовом super() в классе Square, произошёл бы ReferenceError:

class Square extends Polygon {
  constructor(length) {
    this.height;
    // ReferenceError, сначала нужно вызывать super!

    /* Здесь вызывается конструктор родительского класса со значением length
    в качестве значений width и height класса Polygon. */
    // Here, it calls the parent class' constructor with lengths
    super(length, length);

    /* Примечание: в производных класса super() должен быть вызван до использования 'this'.
    Иначе это приведёт к ошибке. */
    this.name = 'Квадрат';
  }
}

Дополнительные материалы

Async Await

Помимо Промисов вам может встретиться еще один синтаксис для обработки асинхронного кода — async/await.

Цель функций async/await — упростить синхронное использование промисов и выполнить какое-либо действие над группой промисов. Точно так же, как промисы похожи на структурированные функции обратного вызова, async/await похожи на комбинацию генераторов и промисов. (Ссылка: MDN)

Примечание: перед тем как пытаться понять async/await, вы должны понимать, что такое промисы и как они работают, поскольку async/await основаны на промисах.

Примечание 2: await должен использоваться в async функции, что означает, что вы не можете использовать await на верхнем уровне вашего кода, так как он не находится внутри async-функции.

Пример кода

async function getGithubUser(username) {
  // Ключевое слово async позволяет использовать await в функции и означает, что функция возвращает промис.
  const response = await fetch(`https://api.github.com/users/${username}`);
  // «Синхронное» ожидание промиса перед переходом на новую строку.
  return response.json();
}

getGithubUser('mbeaudru')
  .then(user => console.log(user))
  /* Логирование пользователя — не может использовать синтаксис await,
  так как этот код не находится внутри async-функции. */
  .catch(err => console.log(err));
  // Если в нашей асинхронной функции возникнет ошибка, то мы перехватим ее здесь.

Объяснение с помощью примера кода

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

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

async function myFunc() {
  // Можно использовать оператор await, так как это async-функция.
  return "hello world";
}

myFunc().then(msg => console.log(msg));
// "Привет, мир!" — возвращаемое значение myFunc превращается в промис из-за оператора async.

Когда будет достигнут оператор return async-функции, промис выполняется с возвращаемым значением. Если внутри async-функции генерируется ошибка, состояние промиса изменится на rejected. Если async-функция не возвращает никакого значения, промис всё равно будет возвращен и выполнится без значения, когда выполнение async-функции будет завершено.

Оператор await используется для ожидания выполнения Промиса и может быть использован только в теле async-функции. При этом выполнение кода приостанавливается, пока не будет выполнен промис.

Примечание: fetch — это функция, возвращающая промис, который позволяет выполнить AJAX-запрос.

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

function getGithubUser(username) {
  return fetch(`https://api.github.com/users/${username}`).then(response => response.json());
}

getGithubUser('mbeaudru')
  .then(user => console.log(user))
  .catch(err => console.log(err));

Вот эквивалент с использованием async/await:

async function getGithubUser(username) {
  // Превращение в промис + разрешено использование ключевого слова await.
  const response = await fetch(`https://api.github.com/users/${username}`);
  // Выполнение останавливается здесь, пока не закончится выполнение промиса.
  return response.json();
}

getGithubUser('mbeaudru')
  .then(user => console.log(user))
  .catch(err => console.log(err));

Синтаксис async/await особенно удобен для построения цепочек взаимозависимых промисов.

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

Примечание: Выражение await должно быть заключено в круглые скобки для вызова методов и свойств разрешенных значений в одной строке.

async function fetchPostById(postId) {
  const token = (await fetch('token_url')).json().token;
  const post = (await fetch(`/posts/${postId}?token=${token}`)).json();
  const author = (await fetch(`/users/${post.authorId}`)).json();

  post.author = author;
  return post;
}

fetchPostById('gzIrzeo64')
  .then(post => console.log(post))
  .catch(err => console.log(err));
Обработка ошибок

Если мы не добавим блок try / catch вокруг выражения await, неперехваченные исключения — неважно, были ли они выброшены в теле вашей async-функции или во время ожидания выполнения await — отклонят промис, возвращенный из async-функции. Использование состояния throw в асинхронной функции — то же самое, что возврат промиса, который был отклонен. (Ссылка: PonyFoo).

Примечание: Промисы ведут себя так же!

С помощью промисов вот как бы мы обработали ошибки:

function getUser() { // Этот промис будет отклонен!
  return new Promise((res, rej) => rej("Пользователь не найден!"));
}

function getAvatarByUsername(userId) {
  return getUser(userId).then(user => user.avatar);
}

function getUserAvatar(username) {
  return getAvatarByUsername(username).then(avatar => ({ username, avatar }));
}

getUserAvatar('mbeaudru')
  .then(res => console.log(res))
  .catch(err => console.log(err)); // -> "Пользователь не найден!"

Эквивалент с использованием async/await:

async function getUser() {
  // Возвращенный промис будет отклонен!
  throw "User not found !";
}

async function getAvatarByUsername(userId) => {
  const user = await getUser(userId);
  return user.avatar;
}

async function getUserAvatar(username) {
  var avatar = await getAvatarByUsername(username);
  return { username, avatar };
}

getUserAvatar('mbeaudru')
  .then(res => console.log(res))
  .catch(err => console.log(err)); // -> "Пользователь не найден!"

Дополнительные материалы

Истина / Ложь

В JavaScript «истинность» или «ложность» значения определяется при вычислении этого значения в булевом контексте. Примером булева контекста может быть вычисление в условии if.

Любое значение будет приведено к true (истина), кроме:

  • false (ложь);
  • 0;
  • "" (пустая строка);
  • null;
  • undefined;
  • NaN.

Вот примеры булева контекста:

  • значение условия if.
if (myVar) {}

Значение myVar может быть любым объектом первого класса (переменная, функция, логическое значение), но оно будет преобразовано в логическое значение, поскольку вычисляется в булевом контексте.

  • После логического оператора NOT !.

Этот оператор возвращает значение «ложь», если его единственный операнд может быть преобразован к значению «истина»; иначе он возвращает значение «истина».

!0 // -> «истина»: 0 — это «ложь», поэтому вернется "истина".
!!0 // -> «ложь»: 0 — это «ложь», следовательно !0 возвращает истину, а !(!0) возвращает «ложь».
!!"" // -> «ложь»: пустая строка — «ложь», поэтому НЕ (НЕ «ложь») равно «ложь».
  • Конструктор объектов типа Boolean.
new Boolean(0); // «ложь»
new Boolean(1); // «истина»
  • Тернарный оператор.
myVar ? "истина" : "ложь"

Значение myVar вычисляется в булевом контексте.

Будьте внимательны при сравнении двух значений. Значения объектов (которые должны быть приведены к истине), не приводятся к булеву типу, а приводятся к примитивному типу в соответствии со спецификацией. Внутри, когда объект сравнивается с булевым значением, например, [] == true, выполняется [].toString() == true, происходит следующее:

let a = [] == true // a ложно, так как [].toString() возвращает пустую строку ("").
let b = [1] == true // b истинно, так как [1].toString() возвращает "1".
let c = [2] == true // c ложно, так как [2].toString() возвращает "2".

Дополнительные материалы

Анаморфизмы и катаморфизмы

Анаморфизмы

Анаморфизмы — это фунции, которые отображают некоторый объект на более сложную структуру, содержащую тип объекта. Это процесс разворачивания простой структуры в более сложную.

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

Пример кода
function downToOne(n) {
  const list = [];

  for (let i = n; i > 0; --i) {
    list.push(i);
  }

  return list;
}

downToOne(5)
  //-> [ 5, 4, 3, 2, 1 ]

Катаморфизмы

Катаморфизмы противоположны анаморфизмам: они берут объекты более сложной структуры и складывают их в более простые структуры.

Рассмотрим следующий пример функции product, которая принимает список целых чисел и возвращает простое целое число.

Пример кода
function product(list) {
  let product = 1;

  for (const n of list) {
    product = product * n;
  }

  return product;
}

product(downToOne(5)) // -> 120

Дополнительные материалы

Генераторы

Другой способ написания функции downToOne — использование генератора. Чтобы создать объект типа Generator, нужно использовать объявление function *. Генераторы — это функции, выполнение которых может быть прервано, а затем продолжено с тем же контекстом (привязками переменных), сохраняющимся при всех вызовах.

Например, функция downToOne может быть переписана следующим образом:

function * downToOne(n) {
  for (let i = n; i > 0; --i) {
    yield i;
  }
}

[...downToOne(5)] // -> [ 5, 4, 3, 2, 1 ]

Генераторы возвращают итерируемый объект. Когда вызывается метод next() итератор, она выполняется до первого выражения yield, которое указывает значение, которое должно быть возвращено из итератора или с помощью yield*, которое дегегирует выполнение другому генератору. Когда в генераторе вызывается выражение return, он будет помечать генератор как выполненный и возвращать значение из выражения return. Дальнейшие вызовы next() не будут возвращать никаких новых значений.

Пример кода

// Пример использования
function * idMaker() {
  var index = 0;
  while (index < 2) {
    yield index;
    index = index + 1;
  }
}

var gen = idMaker();

gen.next().value; // -> 0
gen.next().value; // -> 1
gen.next().value; // -> undefined

Выражение yield* позволяет генератору вызывать другую функцию-генератор во время итерации.

// Пример использования yield *
function * genB(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function * genA(i) {
  yield i;
  yield* genB(i);
  yield i + 10;
}

var gen = genA(10);

gen.next().value; // -> 10
gen.next().value; // -> 11
gen.next().value; // -> 12
gen.next().value; // -> 13
gen.next().value; // -> 20
// Пример возврата из генератора
function* yieldAndReturn() {
  yield "Y";
  return "R";
  yield "unreachable";
}

var gen = yieldAndReturn()
gen.next(); // -> { value: "Y", done: false }
gen.next(); // -> { value: "R", done: true }
gen.next(); // -> { value: undefined, done: true }

Дополнительные материалы

Статические методы

Краткое объяснение

Ключевое слово static используется в классах для объявления статических методов. Статические методы — это функции в классе, которые принадлежат объекту класса и недоступны никаким экземплярам этого класса.

Пример кода

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet";
  }
}

// Обратите внимание, что нам не пришлось создавать экземпляр класса Repo.
console.log(Repo.getName()); // Repo name is modern-js-cheatsheet

let r = new Repo();
console.log(r.getName()); // Не пойманный TypeError: repo.getName не является функцией.

Подробное объяснение

Статические методы можно вызвать в другом статическом методе, используя ключевое слово this, однако это не работает для нестатических методов. Нестатические методы не могут напрямую обращаться к статическим методам, используя ключевое слово this.

Вызов статических методов из статического метода.

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

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet";
  }

  static modifyName(){
    return `${this.getName()}-added-this`;
  }
}

console.log(Repo.modifyName()); // Repo name is modern-js-cheatsheet-added-this
Вызов статических методов из нестатических методов

Нестатические методы могут вызывать статические двумя способами:

  1. Используя имя класса.

Чтобы получить доступ к статическому методы из нестатического, используем имя класса и вызываем статический метод как обычное свойство, например, ClassName.StaticMethodName:

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet"
  }

  useName(){
    return `${Repo.getName()} and it contains some really important stuff`;
  }
}

// Нужно создать экземпляр класса для использования нестатических методов.
let r = new Repo();
console.log(r.useName()); // Repo name is modern-js-cheatsheet and it contains some really important stuff
  1. Используя конструктор.

Статические методы можно вызвать как свойства объекта-конструктора класса.

class Repo {
  static getName() {
    return "Repo name is modern-js-cheatsheet"
  }

useName(){
// Вызывает статический метод как обычное свойство конструктора.
  return `${this.constructor.getName()} and it contains some really important stuff`;
  }
}

// Нужно создать экземпляр класса для использования нестатических функций.
let r = new Repo();
console.log(r.useName()); // Repo name is modern-js-cheatsheet and it contains some really important stuff

Дополнительные материалы

Глоссарий

Область видимости

Контекст, в котором переменная и выражения являются «видимыми» или могут быть получены. Если переменная или выражение находятся «вне текущей области видимости», значит, их нельзя использовать.

Источник: MDN

Изменение переменных

Говорят, что переменная изменилась, когда её значение изменилось относительно начального.

var myArray = [];
myArray.push("firstEl") // Значение myArray изменено.

Переменная называется неизменяемой, если она не может быть изменена.

Более подробно смотрите в статье на MDN.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published