Если вам сложно разобраться с каким-то понятием, рекомендую искать ответы на вопросы на следующих ресурсах:
- MDN (сеть разработчиков Mozilla).
- Вы не знаете JS (серия книг).
- ES6 Features with examples.
- Блог WesBos (ES6).
- Javascript Basics for Beginners — бесплатный курс от Udacity.
- Reddit (JavaScript).
- Google для поиска специализированных блогов и ресурсов.
- StackOverflow.
- Памятка по современному JavaScript
- Введение
- Содержание
- Понятия
- Объявление переменных:
var
,const
,let
- Стрелочные функции
- Значение аргументов функции по умолчанию
- Деструктуризация объектов и массивов
- Методы массивов -
map
/filter
/reduce
- Оператор расширения
...
- Сокращенная запись свойств объектов
- Промисы
- Шаблонные строки
- Тегированные шаблонные строки
- Импорт / экспорт
this
в JavaScript- Класс
- Ключевые слова Extends и super
- Async Await
- Истина / Ложь
- Анаморфизмы и катаморфизмы
- Генераторы
- Статические методы
- Объявление переменных:
- Глоссарий
В JavaScript есть три ключевых слова, отвечающих за объявление переменных, и у каждого из них свои особенности. Эти слова − var
, let
и const
.
Переменным, объявленным с помощью ключевого слова const
, нельзя позже присвоить новое значение, в то время как переменным, объявленным с помощью let
или var
, можно.
Я рекомендую всегда объявлять переменные ключевым словом const
, а let
использовать только в том случае, если позже эту переменную понадобится изменить или переопределить.
Область видимости | Можно переопределять | Можно изменять | Временная мертвая зона | |
---|---|---|---|---|
`const` | Блок | Нет | Да | Да |
`let` | Блок | Да | Да | Да |
`var` | Функция | Да | Да | Нет |
const person = "Коля";
person = "Ваня" // Вызовет ошибку, переменной person нельзя присвоить новое значение.
let person = "Коля";
person = "Ваня";
console.log(person) // -> "Ваня", присвоение нового значения разрешено в случае с let.
Область видимости переменной определяет, где эта переменная доступна в коде.
Областью видимости переменных, объявленных с помощью 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;
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
, ведут себя так же, как переменные, объявленные через 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
ведёт себя в 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 Arrow Functions Introduction — WesBos.
- Стрелочные функции в JavaScript — MDN.
- Javascript ES6 — Arrow Functions and Lexical
this
.
Начиная с обновления 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
— это методы массивов, пришедшие из парадигмы функционального программирования.
Перечислю их:
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];
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
.
const evenNumbers = numbers.filter(function(n) {
return n % 2 === 0; // Истинно, если n чётное; ложно, если n нечётное.
});
console.log(evenNumbers); // -> [0, 2, 4, 6]
Мы применяем filter
к массиву numbers
. Метод filter
взаимодействует с каждым элементом массива и передаёт его в нашу функцию. Функция возвращает булево значение, определяющее, будет ли элемент сохранён в массиве. Затем filter
возвращает массив отфильтрованных значений.
Цель метода 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
- TC39 — Object Rest/Spread Properties for ECMAScript.
- Spread Operator Introduction — WesBos.
- JavaScript & The spread operator.
- 6 Great Uses of the Spread Operator.
При записи переменной в свойство объекта, если у переменной то же имя, что и у свойства, можно сделать следующее:
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 Promises for Dummies — Jecelyn Yeen.
- JavaScript Promise API — David Walsh.
- Using promises — MDN.
- Master the JavaScript Interview: What is a Promise? — Eric Elliott.
- JavaScript Promises: an Introduction — Jake Archibald.
- Документация по промисам — 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
- ES6 Modules in bulletpoints
- Экспорт — MDN.
- Импорт — MDN.
- Understanding ES6 Modules.
- Destructuring special case — import statements.
- Misunderstanding ES6 Modules — Kent C. Dodds.
- Modules in 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
Для понимания прототипов:
- Understanding Prototypes in JS — Yehuda Katz
- A plain English guide to JS prototypes — Sebastian Porto
- Наследование и цепочка прототипов — MDN.
Для понимания классов:
Ключевое слово 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
похожи на комбинацию генераторов и промисов. (Ссылка: 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)); // -> "Пользователь не найден!"
- Async/Await — JavaScript.Info.
- ES7 Async/Await.
- 6 Reasons Why JavaScript’s Async/Await Blows Promises Away.
- JavaScript awaits.
- Using Async Await in Express with Node 8.
- Функция Async.
- Await.
- Using async / await in express with node 8.
В 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
Нестатические методы могут вызывать статические двумя способами:
- Используя имя класса.
Чтобы получить доступ к статическому методы из нестатического, используем имя класса и вызываем статический метод как обычное свойство, например, 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
- Используя конструктор.
Статические методы можно вызвать как свойства объекта-конструктора класса.
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.