Изучаем JavaScript через задачи: от переменных до классов

JavaScript — язык, с которого начинается путь фронтенд-разработчика. Но «знать синтаксис» и «уметь решать задачи» — это разные вещи. В этом руководстве мы пройдём основы языка через практические задачи: от переменных и типов до классов и наследования.

Каждый раздел — теория + задача с решением. В конце — ссылки на продвинутые темы.


Переменные

const, let, var

const PI = 3.14; // нельзя переприсвоить
let count = 0; // можно менять
count = 1; // ok
var oldWay = "avoid"; // функциональная область видимости

Правило: всегда используйте const. Меняйте на let только когда значение точно будет меняться. Забудьте про var.

Hoisting и Temporal Dead Zone

JavaScript «поднимает» объявления переменных в начало области видимости. Но var и let/const ведут себя по-разному:

// var — поднимается и инициализируется как undefined
console.log(x); // undefined (без ошибки!)
var x = 5;

// let/const — поднимаются, но НЕ инициализируются (TDZ)
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;

Temporal Dead Zone (TDZ) — период между началом блока и объявлением let/const. В этот период обращение к переменной вызывает ошибку. Это защищает от использования переменной до её объявления.

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

function example() {
  const x = 10;

  if (true) {
    const y = 20;
    console.log(x + y); // 30 — видим x и y
  }

  console.log(x); // 10 — ok
  // console.log(y); // ReferenceError — y не видна
}

const и let создают блочную область видимости — переменная видна только в пределах { ... }.
var создаёт функциональную область видимости — видна во всей функции.

function demo() {
  if (true) {
    var a = 1; // видна во всей функции
    let b = 2; // видна только в if
  }
  console.log(a); // 1
  // console.log(b); // ReferenceError
}

Задача 1: Swap переменных

Поменяйте значения двух переменных местами без создания третьей.

let a = 5;
let b = 10;

// Решение с деструктуризацией
[a, b] = [b, a];

console.log(a); // 10
console.log(b); // 5

Типы данных

Примитивы

В JavaScript 8 примитивов:

ТипПримерОписание
string"hello"строки
number42, 3.14, NaN, Infinityчисла
bigint42nбольшие целые числа
booleantrue, falseлогические
undefinedundefinedне инициализировано
nullnullпустое значение
symbolSymbol("id")уникальный идентификатор
typeof "hello"; // "string"
typeof 42; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" (исторический баг JS, не исправлен)
typeof Symbol(); // "symbol"
typeof 42n; // "bigint"

Почему typeof null === "object"? Это баг первого движка JavaScript. При реализации typeof проверяли внутренний тип объекта, и null имел тот же тип что и объекты. Исправлять не стали — сломает обратную совместимость.

Примитивы vs объекты

Примитивы хранятся по значению, объекты — по ссылке:

let a = 5;
let b = a;
b = 10;
console.log(a); // 5 — a не изменилась

let obj1 = { x: 1 };
let obj2 = obj1; //obj2 ссылается на тот же объект
obj2.x = 99;
console.log(obj1.x); // 99 — obj1 тоже изменилась!

Это значит:

  • Копирование примитива создаёт копию значения
  • Копирование объекта создаёт ссылку на тот же объект

Проверка типа

// Рекомендуемые способы
typeof value === "string"; // для примитивов
Array.isArray(value); // для массивов
Number.isNaN(value); // для NaN (не isNaN!)

// Избегайте
value instanceof String; // не работает для примитивов
typeof value === "object"; // верно и для null, и для объектов

Задача 2: Определить тип переменной

Напишите функцию getType, которая возвращает тип переменной. Для массивов возвращайте "array", для null"null".

function getType(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  return typeof value;
}

getType("hello"); // "string"
getType([1, 2]); // "array"
getType(null); // "null"
getType({}); // "object"

Строки

Основные методы

Строки в JavaScript неизменяемы — вы не можете изменить отдельный символ:

const s = "Hello, World!";

s.length; // 13
s[0]; // "H"
s.includes("World"); // true
s.indexOf("World"); // 7
s.slice(0, 5); // "Hello"
s.toUpperCase(); // "HELLO, WORLD!"
s.toLowerCase(); // "hello, world!"
s.split(", "); // ["Hello", "World!"]
s.replace("World", "JS"); // "Hello, JS!"
s.trim(); // без пробелов по краям
s.startsWith("Hello"); // true
s.endsWith("!"); // true
s.repeat(2); // "Hello, World!Hello, World!"

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

Шаблонные строки (template literals) — современный способ работы со строками:

const name = "Alice";
const age = 25;

// Интерполяция — подстановка переменных через ${}
const msg1 = `${name} is ${age} years old`;

// Вычисления внутри ${}
const msg2 = `${name} will be ${age + 5} in 5 years`;

// Многострочность
const msg3 = `
  Dear ${name},
  You are ${age} years old.
  Welcome to our platform!
`;

Конкатенация vs шаблонные строки

// Старый способ (избегайте)
const msg1 = "Hello, " + name + "! You are " + age + " years old";

// Современный способ (используйте)
const msg2 = `Hello, ${name}! You are ${age} years old`;

Задача 3: Палиндром

Проверьте, является ли строка палиндромой (читается одинаково в обе стороны, без учёта регистра и пробелов).

function isPalindrome(s) {
  const cleaned = s.toLowerCase().replace(/[^a-z0-9]/g, "");
  return cleaned === cleaned.split("").reverse().join("");
}

isPalindrome("A man, a plan, a canal: Panama"); // true
isPalindrome("race a car"); // false

Числа

Особенности

0.1 + 0.2; // 0.30000000000000004 (проблема точности)
Number.isInteger(1); // true
Number.isInteger(1.5); // false
Math.floor(1.7); // 1 (округление вниз)
Math.ceil(1.2); // 2 (округление вверх)
Math.round(1.5); // 2 (округление до ближайшего)

NaN и Infinity

// NaN — результат некорректной операции
0 / 0; // NaN
Number("abc"); // NaN
typeof NaN; // "number" (да, typeof врёт)
NaN === NaN; // false (уникальное свойство!)

// Как проверять NaN
Number.isNaN(NaN); // true — правильный способ
isNaN("abc"); // true (преобразует строку в число)
Number.isNaN("abc"); // false (не преобразует)

// Infinity
1 / 0; // Infinity
-1 / 0; // -Infinity

Преобразование

Number("42"); // 42
Number("abc"); // NaN
parseInt("42px"); // 42 (останавливается на не-цифре)
parseInt("abc42"); // NaN (начинается с не-цифры)
parseFloat("3.14abc"); // 3.14
+"42"; // 42 (унарный плюс)
+""; // 0

Форматирование

(3.14159).toFixed(2); // "3.14" (строка!)
(1234567).toLocaleString(); // "1 234 567" (зависит от локали)
(255).toString(16); // "ff" (в шестнадцатеричном)

Задача 4: Факториал

Напишите функцию, вычисляющую факториал числа.

function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

factorial(5); // 120
factorial(0); // 1

Условия и операторы

Сравнение

// Всегда используйте === (строгое сравнение)
0 == false; // true (с приведением типов)
0 === false; // false
null == undefined; // true
null === undefined; // false

Truthy и Falsy значения

JavaScript автоматически преобразует значения в boolean. Falsy значения:

false; // логическое false
0; // ноль
(""); // пустая строка
null; // пустое значение
undefined; // не определено
NaN; // не число

Всё остальное — Truthy, включая:

[]; // пустой массив — truthy!
{
} // пустой объект — truthy!
("0"); // строка с нулём — truthy!
new Boolean(false); // объект boolean — truthy!
// Осторожно!
if ([]); // true — пустой массив truthy
if ({}); // true — пустой объект truthy
if ("0"); // true — непустая строка truthy

Тернарный оператор

const age = 20;
const status = age >= 18 ? "adult" : "minor";

// Вложенность (избегайте — читаемость падает)
const category = age < 13 ? "child" : age < 18 ? "teen" : "adult";

Optional chaining и Nullish coalescing

const user = { address: { city: "Moscow" } };

user?.address?.zip; // undefined (без ошибки)
user?.phone?.number; // undefined (без ошибки, даже если phone нет)

null ?? "default"; // "default"
0 ?? "default"; // 0 (отличие от || — ноль не заменяется)
"" ?? "default"; // "" (пустая строка тоже не заменяется)

Switch

function getDayName(day) {
  switch (day) {
    case 0:
      return "Sunday";
    case 1:
      return "Monday";
    case 2:
      return "Tuesday";
    case 3:
      return "Wednesday";
    case 4:
      return "Thursday";
    case 5:
      return "Friday";
    case 6:
      return "Saturday";
    default:
      return "Invalid day";
  }
}

Когда использовать: когда сравниваете одно значение с несколькими. Для диапазонов и сложных условий — if/else.

Задача 5: FizzBuzz

Выведите числа от 1 до 100. Если число делится на 3 — "Fizz", на 5 — "Buzz", на оба — "FizzBuzz".

function fizzBuzz(n) {
  const result = [];

  for (let i = 1; i <= n; i++) {
    if (i % 15 === 0) result.push("FizzBuzz");
    else if (i % 3 === 0) result.push("Fizz");
    else if (i % 5 === 0) result.push("Buzz");
    else result.push(i);
  }

  return result;
}

Массивы

Создание

const arr1 = [1, 2, 3];
const arr2 = new Array(3); // [empty × 3] — не используйте
const arr3 = Array.from("hello"); // ["h", "e", "l", "l", "o"]
const arr4 = Array(3).fill(0); // [0, 0, 0]

Мутабельные vs иммутабельные методы

Мутабельные — изменяют исходный массив:

const arr = [3, 1, 4, 1, 5, 9];

arr.push(2); // добавить в конец → [3, 1, 4, 1, 5, 9, 2]
arr.pop(); // удалить с конца → [3, 1, 4, 1, 5, 9]
arr.unshift(0); // добавить в начало → [0, 3, 1, 4, 1, 5, 9]
arr.shift(); // удалить с начала → [3, 1, 4, 1, 5, 9]
arr.splice(2, 1, 10); // удалить 1 элемент с позиции 2, вставить 10
arr.sort((a, b) => a - b); // сортировка
arr.reverse(); // разворот

Иммутабельные — возвращают новый массив:

const arr = [3, 1, 4, 1, 5, 9];

arr.slice(1, 4); // [1, 4, 1] — копия
arr.filter((x) => x > 3); // [4, 5, 9] — новый массив
arr.map((x) => x * 2); // [6, 2, 8, 2, 10, 18] — новый массив
arr.concat([7, 8]); // [3, 1, 4, 1, 5, 9, 7, 8] — новый массив
arr.flat(); // [3, 1, 4, 1, 5, 9] — развернуть вложенность
arr.flatMap((x) => [x, x * 2]); // [3, 6, 1, 2, 4, 8, 1, 2, 5, 10, 9, 18]

Методы поиска и преобразования

const arr = [3, 1, 4, 1, 5, 9];

// Поиск
arr.indexOf(1); // 1 (первый индекс)
arr.includes(5); // true
arr.find((x) => x > 4); // 5 (первый подходящий)
arr.findIndex((x) => x > 4); // 4 (его индекс)
arr.some((x) => x > 8); // true (хотя бы один)
arr.every((x) => x > 0); // true (все)

// Преобразование
arr.map((x) => x * 2); // [6, 2, 8, 2, 10, 18]
arr.filter((x) => x > 3); // [4, 5, 9]
arr.reduce((sum, x) => sum + x, 0); // 23

for...of vs forEach

const arr = [1, 2, 3, 4, 5];

// for...of — можно break/continue
for (const item of arr) {
  if (item === 3) break; // остановка
  console.log(item); // 1, 2
}

// forEach — нельзя break/continue
arr.forEach((item) => {
  // if (item === 3) break; // SyntaxError!
  console.log(item);
});

// Рекомендация: используйте for...of когда нужен break
// Для преобразований — map/filter/reduce

Задача 6: Плоский массив (Flatten)

Реализуйте функцию flatten, которая разворачивает вложенные массивы.

function flatten(arr) {
  return arr.reduce((acc, item) => {
    return acc.concat(Array.isArray(item) ? flatten(item) : item);
  }, []);
}

flatten([1, [2, [3, [4]], 5]]); // [1, 2, 3, 4, 5]

Задача 7: Группировка массива

Сгруппируйте элементы по значению функции.

function groupBy(arr, fn) {
  return arr.reduce((groups, item) => {
    const key = typeof fn === "function" ? fn(item) : item[fn];
    groups[key] = groups[key] || [];
    groups[key].push(item);
    return groups;
  }, {});
}

groupBy([1, 2, 3, 4, 5], (x) => (x % 2 === 0 ? "even" : "odd"));
// { odd: [1, 3, 5], even: [2, 4] }

Объекты

Создание и доступ

const user = {
  name: "Alice",
  age: 25,
  greet() {
    return `Hi, I'm ${this.name}`;
  },
};

user.name; // "Alice" (точечная нотация)
user["age"]; // 25 (квадратные скобки — динамические ключи)
user.email = "alice@mail.com"; // добавить свойство
delete user.age; // удалить свойство

Ссылочная модель

Объекты хранятся по ссылке, поэтому два пустых объекта не равны:

const a = { x: 1 };
const b = { x: 1 };
a === b; // false — разные объекты в памяти

const c = a;
c === a; // true — та же ссылка
c.x = 99;
a.x; // 99 — a тоже изменилась!

Перебор объектов

const user = { name: "Alice", age: 25, city: "Moscow" };

// Object.keys — массив ключей
Object.keys(user); // ["name", "age", "city"]

// Object.values — массив значений
Object.values(user); // ["Alice", 25, "Moscow"]

// Object.entries — массив пар [ключ, значение]
Object.entries(user); // [["name", "Alice"], ["age", 25], ["city", "Moscow"]]

// for...in — перебор ключей (включая унаследованные)
for (const key in user) {
  console.log(key, user[key]);
}

// for...of по Object.entries — перебор пар
for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}

Деструктуризация

const { name, age, email = "N/A" } = user;
// name = "Alice", age = 25, email = "N/A" (значение по умолчанию)

// Переименование
const { name: userName } = user;
// userName = "Alice"

// Вложенная деструктуризация
const {
  address: { city },
} = { address: { city: "Moscow" } };
// city = "Moscow"

Spread и Rest

// Spread — копирование/слияние
const copy = { ...user, age: 30 }; // копия с изменённым age
const merged = { ...obj1, ...obj2 }; // слияние двух объектов

// Rest — сбор оставшихся свойств
const { name: _, ...rest } = user; // rest = { age: 25, city: "Moscow" }

Задача 8: Глубокое клонирование

Реализуйте глубокое клонирование объекта (без structuredClone).

function deepClone(obj) {
  if (obj === null || typeof obj !== "object") return obj;

  if (Array.isArray(obj)) {
    return obj.map((item) => deepClone(item));
  }

  const cloned = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepClone(obj[key]);
    }
  }
  return cloned;
}

const original = { a: 1, b: { c: 2, d: [3, 4] } };
const cloned = deepClone(original);
cloned.b.c = 99;
console.log(original.b.c); // 2 — оригинал не изменился

Задача 9: Сериализация объекта

Преобразуйте объект в строку и обратно (без JSON.stringify/parse).

function serialize(obj) {
  if (typeof obj === "string") return `"${obj}"`;
  if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
  if (obj === null) return "null";
  if (Array.isArray(obj)) {
    return `[${obj.map(serialize).join(",")}]`;
  }
  const props = Object.entries(obj)
    .map(([k, v]) => `"${k}":${serialize(v)}`)
    .join(",");
  return `{${props}}`;
}

serialize({ name: "Alice", scores: [1, 2, 3] });
// '{"name":"Alice","scores":[1,2,3]}'

Функции

Типы функций

// Объявление функции (function declaration)
function add(a, b) {
  return a + b;
}
// Поднимается (hoisting) — можно вызвать до объявления

// Выражение функции (function expression)
const multiply = function (a, b) {
  return a * b;
};
// Не поднимается — нельзя вызвать до объявления

// Стрелочная функция (arrow function)
const divide = (a, b) => a / b;
// Нет своего this, нет arguments

// IIFE (Immediately Invoked Function Expression)
const result = (function () {
  return 42;
})();
// Вызывается сразу после объявления

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

const obj = {
  name: "Alice",

  // Обычная функция — this = obj
  greetRegular() {
    return `Hello, ${this.name}`;
  },

  // Стрелочная функция — this = внешний контекст (не obj!)
  greetArrow: () => {
    return `Hello, ${this.name}`; // this = undefined (или window)
  },
};

obj.greetRegular(); // "Hello, Alice"
obj.greetArrow(); // "Hello, undefined"

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

this — 4 правила определения контекста

// 1. Глобальный вызов
function show() {
  console.log(this);
}
show(); // window (или undefined в strict mode)

// 2. Метод объекта
const obj = {
  show() {
    console.log(this);
  },
};
obj.show(); // obj

// 3. Конструктор
function User(name) {
  this.name = name;
}
new User("Alice"); // новый объект

// 4. call/apply/bind
function greet() {
  console.log(this.name);
}
greet.call({ name: "Bob" }); // "Bob"

Замыкание (кратко)

Функция «запоминает» переменные из внешней области видимости:

function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    getCount: () => count,
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount(); // 2

Подробнее — в статье Замыкания в JavaScript.

Параметры по умолчанию

function greet(name = "Guest") {
  return `Hello, ${name}`;
}

greet(); // "Hello, Guest"
greet("Bob"); // "Hello, Bob"

rest-параметры

function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}

sum(1, 2, 3, 4); // 10

Задача 10: Каррирование

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

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...args2) => curried(...args, ...args2);
  };
}

const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1)(2, 3); // 6

Обработка ошибок

try/catch/finally

try {
  const data = JSON.parse("invalid json");
} catch (error) {
  // Выполняется при ошибке
  console.error(error.message); // "Unexpected token i in JSON at position 0"
  console.error(error.name); // "SyntaxError"
} finally {
  // Выполняется всегда (с ошибкой или без)
  console.log("Всегда выполняется");
}

Типы ошибок

// ReferenceError — обращение к несуществующей переменной
console.log(x); // ReferenceError: x is not defined

// TypeError — неправильный тип
null.foo; // TypeError: Cannot read properties of null
(5).foo(); // TypeError: (5).foo is not a function

// SyntaxError — синтаксическая ошибка
eval("if (true) {"); // SyntaxError

// RangeError — значение за пределами допустимого
(1).toFixed(100); // RangeError

Создание и генерация ошибок

function divide(a, b) {
  if (b === 0) throw new Error("Division by zero");
  return a / b;
}

// Генерация с кастомным именем
function validateAge(age) {
  if (age < 0) {
    const err = new Error("Age cannot be negative");
    err.name = "ValidationError";
    throw err;
  }
}

try {
  divide(10, 0);
} catch (e) {
  console.error(e.message); // "Division by zero"
}

Когда использовать

  • try/catch — для операций, которые могут завершиться ошибкой (парсинг, запросы, файлы)
  • throw — когда функция не может выполнить свою задачу
  • Возвращайте ошибки, а не бросайте — если хотите, чтобы вызывающий код мог обработать

Задача 11: Безопасный JSON.parse

Оберните JSON.parse в функцию, которая возвращает null вместо ошибки.

function safeJsonParse(str) {
  try {
    return JSON.parse(str);
  } catch {
    return null;
  }
}

safeJsonParse('{"a": 1}'); // { a: 1 }
safeJsonParse("invalid"); // null

Асинхронный JavaScript

Promise

Promise — объект, представляющий результат асинхронной операции. Три состояния:

  • pending — ожидание
  • fulfilled — успешно
  • rejected — ошибка
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("done"), 1000);
});

promise
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

async/await

async/await — современный способ работы с промисами. Код выглядит синхронным, но работает асинхронно.

async function fetchData() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    return data;
  } catch (err) {
    console.error(err);
  }
}

Правило: await можно использовать только внутри async функции (или в модулях).

async/await vs .then()

// С .then()
fetchUser(id)
  .then((user) => fetchPosts(user.id))
  .then((posts) => console.log(posts))
  .catch((err) => console.error(err));

// С async/await
async function getUserPosts(id) {
  try {
    const user = await fetchUser(id);
    const posts = await fetchPosts(user.id);
    console.log(posts);
  } catch (err) {
    console.error(err);
  }
}

// Рекомендация: async/await для сложных цепочек, .then() для простых

Параллельные запросы

// Все параллельно (Promise.all)
const [users, posts] = await Promise.all([
  fetch("/users").then((r) => r.json()),
  fetch("/posts").then((r) => r.json()),
]);

// Первый успешный (Promise.any)
const fastest = await Promise.any([
  fetch("/mirror1"),
  fetch("/mirror2"),
  fetch("/mirror3"),
]);

// Все с результатами (Promise.allSettled)
const results = await Promise.allSettled([
  fetch("/might-fail-1"),
  fetch("/might-fail-2"),
]);
// results = [{ status: "fulfilled", value: ... }, { status: "rejected", reason: ... }]

Event Loop: микрозадачи vs макрозадачи

console.log("1"); // синхронный код

setTimeout(() => console.log("2"), 0); // макрозадача

Promise.resolve().then(() => console.log("3")); // микрозадача

console.log("4"); // синхронный код

// Вывод: 1, 4, 3, 2

Порядок выполнения:

  1. Синхронный код
  2. Все микрозадачи (Promise.then, queueMicrotask)
  3. Одна макрозадача (setTimeout, setInterval)
  4. Повторяем с пункта 2

Задача 12: Задержка (Delay)

Создайте функцию delay, которая возвращает промис, резолвящийся через ms миллисекунд.

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function example() {
  console.log("Start");
  await delay(1000);
  console.log("After 1 second");
}

Задача 13: Ограничитель запросов (Rate Limiter)

Реализуйте throttle — функцию, которая вызывается не чаще чем раз в limit мс.

function throttle(fn, limit) {
  let lastCall = 0;

  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      return fn.apply(this, args);
    }
  };
}

const throttledLog = throttle(console.log, 1000);
throttledLog("a"); // выведет сразу
throttledLog("b"); // проигнорировано

Промисы в глубину

Задача 14: Задача о ленте (Promise Chain)

Запросите данные о пользовате, затем о его постах, затем о комментариях к первому посту.

async function getUserPostComments(userId) {
  const user = await fetch(`/api/users/${userId}`).then((r) => r.json());
  const posts = await fetch(`/api/users/${userId}/posts`).then((r) => r.json());
  const comments = await fetch(`/api/posts/${posts[0].id}/comments`).then((r) =>
    r.json(),
  );

  return { user, post: posts[0], comments };
}

Задача 15: Retry с экспоненциальной задержкой

Повторяйте запрос до maxRetries раз с увеличением задержки.

async function retry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === maxRetries - 1) throw err;
      await delay(1000 * Math.pow(2, i));
    }
  }
}

// Использование
const data = await retry(() => fetch("/api/unstable").then((r) => r.json()));

Классы

Основы

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

  greet() {
    return `Hi, I'm ${this.name}, ${this.age} years old`;
  }

  static create(name, age) {
    return new User(name, age);
  }
}

const user = new User("Alice", 25);
user.greet(); // "Hi, I'm Alice, 25 years old"

const user2 = User.create("Bob", 30); // статический метод

this в классах

this в классах — это экземпляр класса. Но есть нюанс со стрелочными методами:

class Timer {
  seconds = 0;

  // Проблема: this теряется при передаче метода
  startBad() {
    setInterval(function () {
      this.seconds++; // this = undefined (или window)
      console.log(this.seconds);
    }, 1000);
  }

  // Решение 1: стрелочное свойство (создаётся в constructor)
  startGood = () => {
    setInterval(() => {
      this.seconds++; // this = экземпляр Timer
      console.log(this.seconds);
    }, 1000);
  };
}

Наследование

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  speak() {
    return `${this.name} barks`;
  }
}

class Cat extends Animal {
  speak() {
    return `${this.name} purrs`;
  }
}

new Dog("Rex").speak(); // "Rex barks"
new Cat("Whiskers").speak(); // "Whiskers purrs"

Приватные свойства

class BankAccount {
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount > 0) this.#balance += amount;
  }

  get balance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);
account.balance; // 1500
// account.#balance; // SyntaxError

Геттеры и сеттеры

class Temperature {
  #celsius;

  constructor(celsius) {
    this.#celsius = celsius;
  }

  get fahrenheit() {
    return this.#celsius * 1.8 + 32;
  }

  set fahrenheit(f) {
    this.#celsius = (f - 32) / 1.8;
  }
}

const t = new Temperature(100);
t.fahrenheit; // 212
t.fahrenheit = 32;
t.fahrenheit; // 32

Прототипы (кратко)

Методы классов хранятся в прототипе, а не в каждом экземпляре:

class Dog {
  constructor(name) {
    this.name = name; // хранится в экземпляре
  }

  bark() {
    return `${this.name} barks`;
  }
}

const dog1 = new Dog("Rex");
const dog2 = new Dog("Buddy");

// bark() одна на всех — в прототипе
Dog.prototype.bark === dog1.bark; // true

// name разные — в экземплярах
dog1.name; // "Rex"
dog2.name; // "Buddy"

Проверка типа

class Animal {}
class Dog extends Animal {}

const dog = new Dog();

dog instanceof Dog; // true
dog instanceof Animal; // true
dog.constructor === Dog; // true

Задача 16: Реализовать Event Emitter

class EventEmitter {
  #listeners = new Map();

  on(event, callback) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, new Set());
    }
    this.#listeners.get(event).add(callback);
    return () => this.off(event, callback);
  }

  off(event, callback) {
    this.#listeners.get(event)?.delete(callback);
  }

  emit(event, ...args) {
    this.#listeners.get(event)?.forEach((cb) => cb(...args));
  }
}

const emitter = new EventEmitter();
const unsub = emitter.on("data", (msg) => console.log(msg));
emitter.emit("data", "hello"); // "hello"
unsub(); // отписка
emitter.emit("data", "hello"); // ничего не выведет

Деструктуризация и spread/rest — продвинутое

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

const [first, , third, ...rest] = [1, 2, 3, 4, 5];
// first = 1, third = 3, rest = [4, 5]

// Обмен переменных
let x = 1,
  y = 2;
[x, y] = [y, x];

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

const { name, age, ...rest } = { name: "Alice", age: 25, city: "Moscow" };
// rest = { city: "Moscow" }

// Переименование
const { name: userName } = { name: "Alice" };
// userName = "Alice"

Spread в функциях

function log(a, b, c) {
  console.log(a, b, c);
}

const args = [1, 2, 3];
log(...args); // 1 2 3

Модули

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

// math.js
export const PI = 3.14;
export function add(a, b) {
  return a + b;
}
export default class Calculator {
  /* ... */
}

// app.js
import Calculator, { PI, add } from "./math.js";

Динамический импорт

const module = await import("./heavy-module.js");
module.doSomething();

Итого: что вы изучили

РазделКлючевые концепцииЧто запомнить
Переменныеconst, let, varconst по умолчанию, let когда меняется, var избегать
Типы данных8 примитивов, typeoftypeof null === "object" — баг, NaN !== NaN
СтрокиМетоды, шаблонные строкиСтроки неизменяемы, шаблоны через ${}
ЧислаТочность, NaN, InfinityNumber.isNaN(), не isNaN()
УсловияTruthy/falsy, сравнениеВсегда ===, пустой массив — truthy
МассивыМутабельные/иммутабельныеsplice/sort/reverse меняют, slice/map/filter нет
ОбъектыСсылки, деструктуризация{ a: 1 } !== { a: 1 }, spread для копирования
Функцииthis, стрелочные, замыканиеСтрелочные не имеют своего this
Ошибкиtry/catch, типы ошибокReferenceError, TypeError, SyntaxError
АсинхронностьPromise, async/await, event loopМикрозадачи → макрозадачи
КлассыНаследование, приватные поля#balance, extends, instanceof

Что дальше

Это основы. Дальше — более сложные темы, которые требуют практики:

Попробуйте решить задачи на Девстанции — AI проверит ваш код и покажет, как написать его лучше.