Skip to content

Latest commit

 

History

History
1634 lines (1350 loc) · 96.9 KB

File metadata and controls

1634 lines (1350 loc) · 96.9 KB

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

Памятка по современному JavaScript За картинку спасибо Ahmad Awais ⚡️

Введение

Мотивация

В этом документе собраны возможности языка JavaScript, с которыми вы наверняка столкнетесь в современных проектах и примерах кода.

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

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

Примечание: Большинство представленных здесь понятий взяты из обновления языка JavaScript (ES2015, которое часто называют ES6). Вы можете найти новые возможности из этого обновления здесь; они хорошо описаны.

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

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

Содержание

Понятия

Объявление переменных: 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.