JavaScript: Полный курс от основ к продвинутому

Этот курс охватывает JavaScript от базовых концепций до продвинутых тем. Мы добавили дополнительные объяснения, особенно в разделах среднего и сложного уровня, включая детальный разбор конструкций, методов, примеров использования, потенциальных ошибок и лучших практик. Для средних тем (например, функции, асинхронность) мы углубимся в механизмы работы, а для сложных (ООП, паттерны) — в внутреннюю логику, альтернативы и реальные сценарии применения. Каждый раздел включает больше примеров кода с комментариями.

По запросу добавлены новые разделы по TypeScript (Часть 11), поскольку TypeScript является надстройкой над JavaScript, добавляющей статическую типизацию. Это позволит перейти от динамического JS к типизированному коду, улучшая безопасность и масштабируемость проектов. Разделы по TypeScript построены аналогично структуре курса по JS, с акцентом на различия и преимущества.

Часть 1: Фундамент (Как строится код)

1.1. Строительные блоки: Переменные - ваши первые коробки для данных

Когда вы начинаете программировать, представьте, что у вас есть пустой рабочий стол и много разных вещей (данных), которые нужно куда-то складывать. Переменная — это как подписанная коробка, в которую вы кладёте данные. Вы даёте коробке имя (например, "возрастПользователя") и кладёте туда значение (например, число 25).

Зачем это нужно? Без переменных пришлось бы каждый раз писать одни и те же значения заново. Представьте, что в вашей программе есть важное число 3.14159 (число Пи). Вы используете его 20 раз в разных местах. А потом понимаете, что нужно более точное значение — 3.1415926535. Придётся менять во всех 20 местах! Но если вы положили это число в коробку с именем pi, то меняете значение только в одном месте — там, где объявляете переменную.

Три вида "коробок" в JavaScript: var, let, const

В JavaScript исторически было три способа создать такую коробку. Давайте разберём каждый на простой аналогии:

// 1. var — СТАРАЯ КОРОБКА С ПРОБЛЕМАМИ (почти не используется в современном коде)
// Представьте картонную коробку, которая может случайно открываться в разных комнатах.
var стараяПеременная = "значение";
// Проблема: если создать такую коробку в маленькой комнате (внутри {} скобок),
// её всё равно можно взять из соседней комнаты. Это приводит к ошибкам.

// ПРИМЕР ПРОБЛЕМЫ:
for (var i = 0; i < 3; i++) {
    // Внутри этой "комнаты" создаётся коробка i
}
console.log(i); // ⚠️ ОШИБКА ЛОГИКИ, но код работает! i = 3
// Коробка i "утекла" за пределы своей комнаты. Так быть не должно!

// 2. let — НОРМАЛЬНАЯ КОРОБКА ДЛЯ ИЗМЕНЯЕМЫХ ВЕЩЕЙ
// Как пластиковый контейнер с крышкой. То, что внутри, можно менять.
let счётчик = 0;
счётчик = 1;           // ✅ Можно положить новое значение
счётчик = счётчик + 5; // ✅ Можно взять текущее значение, что-то сделать с ним и положить обратно
счётчик += 3;          // ✅ Короткая запись: счётчик = счётчик + 3

// ГДЕ ЖИВЁТ let-переменная (область видимости):
{
    let видитТолькоЗдесь = "секрет";
    console.log(видитТолькоЗдесь); // ✅ Работает
}
// console.log(видитТолькоЗдесь); // ❌ ОШИБКА! За пределами {} эта переменная не видна

// 3. const — ЗАПЕЧАТАННАЯ КОРОБКА (константа)
// Как банка с вареньем, которую нельзя заменить на другую банку.
// Но само варенье внутри можно есть (если это объект или массив).
const математическаяКонстанта = 3.14159;
// математическаяКонстанта = 3.14; // ❌ КАТАСТРОФА! Нельзя заменить банку!

// ВАЖНЫЙ НЮАНС: что значит "нельзя изменить" для разных типов данных?
const списокПокупок = ["хлеб", "молоко"];
// списокПокупок = ["чай", "кофе"]; // ❌ Нельзя заменить весь массив на новый

// НО МОЖНО делать это:
списокПокупок.push("яйца");      // ✅ Добавить в конец: ["хлеб", "молоко", "яйца"]
списокПокупок[0] = "батон";      // ✅ Заменить первый элемент: ["батон", "молоко", "яйца"]
списокПокупок.pop();             // ✅ Удалить последний элемент: ["батон", "молоко"]

const пользователь = { имя: "Иван", возраст: 25 };
пользователь.возраст = 26;        // ✅ Изменить свойство
пользователь.город = "Москва";    // ✅ Добавить новое свойство
// пользователь = { имя: "Петр" }; // ❌ Нельзя заменить весь объект

// ПРАВИЛО ДЛЯ НАЧИНАЮЩИХ:
// 1. Всегда начинайте с const
// 2. Меняйте на let только если видите ошибку "Assignment to constant variable"
// 3. Никогда не используйте var (забудьте про него)

🎯 Практика на курсе: учимся на деле!

Теория — это хорошо, но программирование учатся на практике. Поэтому в нашем курсе есть:

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

1.2. Типы данных: что можно положить в "коробку"-переменную

JavaScript различает, ЧТО именно вы кладёте в переменную. Это как на кухне: муку хранят в банке, ножи — в ящике, а кастрюли — в шкафу. Каждому типу данных — своё "место" и свои правила.

Простые типы (примитивы) — неделимые значения

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

Тип Пример Объяснение для новичка Важные особенности Как проверить
string
(строка)
"Привет"
'Мир'
`Шаблон ${x}`
Любой текст. Всегда в кавычках! Одинарные ' ', двойные " " или обратные ` ` (для шаблонов). Нельзя изменить часть строки, только создать новую. "кот"[0] = "т" — не сработает! typeof "текст" → "string"
number
(число)
42
-3.14
1e6 (1 миллион)
Infinity
Все числа: целые, дробные, отрицательные. Нет отдельного типа для целых чисел. Есть проблема точности: 0.1 + 0.2 = 0.30000000000000004. Для денег используйте копейки (вместо 10.5 руб → 1050 коп). typeof 42 → "number"
boolean
(логический)
true
false
Только два значения: ДА (true) или НЕТ (false). Для принятия решений в программе. Любое значение можно преобразовать в boolean. 0, "", null, undefined, NaNfalse, всё остальное → true. typeof true → "boolean"
undefined undefined Значение по умолчанию. Коробка создана, но в неё ещё ничего не положили. let x; — x будет undefined. Обычно означает "значение отсутствует". typeof undefined → "undefined"
null null Специальное значение "ничего", "пусто". Мы сами кладём null в переменную. Историческая ошибка: typeof null → "object". Это баг языка, который исправить нельзя. Проверяйте так: x === null
bigint 12345678901234567890n Огромные целые числа. Добавьте n в конце. Для расчётов, где обычные числа теряют точность. Нельзя смешивать с number без преобразования: 5n + 2 — ошибка! typeof 10n → "bigint"
symbol Symbol("id")
Symbol()
Уникальный идентификатор. Как серийный номер, который никогда не повторяется. Используется для скрытых свойств объектов. Пока просто знайте, что такой тип есть. typeof Symbol() → "symbol"

Сложные типы (объекты) — коллекции данных

Это как ящик с инструментами или корзина с фруктами. Содержат внутри себя другие значения.

// 1. ОБЪЕКТ (object) — набор пар "ключ: значение"
const человек = {
    имя: "Анна",           // ключ "имя", значение "Анна" (строка)
    возраст: 30,           // ключ "возраст", значение 30 (число)
    студент: true,         // ключ "студент", значение true (boolean)
    адрес: {               // значение тоже может быть объектом!
        город: "Москва",
        улица: "Ленина"
    }
};
// Как получить значение:
console.log(человек.имя);        // "Анна" (через точку)
console.log(человек["возраст"]); // 30 (через квадратные скобки)

// 2. МАССИВ (array) — упорядоченный список
const оценки = [5, 4, 3, 5, 4]; // Нумерация с НУЛЯ!
console.log(оценки[0]); // 5 (первый элемент)
console.log(оценки[1]); // 4 (второй элемент)

// 3. ФУНКЦИЯ (function) — блок кода, который можно выполнять
function приветствие(имя) {
    return `Привет, ${имя}!`;
}
console.log(typeof приветствие); // "function" (особый вид объекта)

ОЧЕНЬ ВАЖНО: Как const работает с разными типами

Это самый сложный момент для новичков. Давайте разберём на аналогиях:

// СИТУАЦИЯ 1: const с ПРИМИТИВАМИ (string, number, boolean...)
const мойПароль = "секрет123";
// мойПароль = "новыйПароль"; // ❌ НЕЛЬЗЯ! Это как попытаться заменить надпись на запертой двери.

// СИТУАЦИЯ 2: const с МАССИВОМ
const холодильник = ["молоко", "яйца"];
// холодильник = ["колбаса", "сыр"]; // ❌ НЕЛЬЗЯ заменить весь холодильник!

// НО МОЖНО:
холодильник.push("масло");      // ✅ Добавить продукт
холодильник[0] = "кефир";       // ✅ Заменить молоко на кефир
холодильник.pop();              // ✅ Убрать последний продукт

// АНАЛОГИЯ: const с массивом — это закреплённая полка в холодильнике.
// Вы не можете заменить всю полку, но продукты на ней переставлять можно!

// СИТУАЦИЯ 3: const с ОБЪЕКТОМ
const настройки = { цвет: "синий", звук: true };
// настройки = { цвет: "красный" }; // ❌ НЕЛЬЗЯ заменить весь объект!

// НО МОЖНО:
настройки.цвет = "красный";     // ✅ Изменить свойство
настройки.яркость = 80;         // ✅ Добавить новое свойство
delete настройки.звук;          // ✅ Удалить свойство

// АНАЛОГИЯ: const с объектом — это закреплённая папка с документами.
// Вы не можете выбросить всю папку, но листы внутри менять можно!

Запоминалка: const защищает саму "коробку" (ссылку в памяти), а не то, что внутри (содержимое объектов/массивов).

1.3. Операции: что можно делать с данными

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

1. Математические операции (для чисел)

let a = 10 + 5;   // Сложение: 15
let b = 10 - 5;   // Вычитание: 5
let c = 10 * 2;   // Умножение: 20
let d = 10 / 2;   // Деление: 5
let e = 11 % 3;   // Остаток от деления: 2 (11 ÷ 3 = 3 и 2 в остатке)
let f = 2 ** 4;   // Возведение в степень: 16 (2 × 2 × 2 × 2)

// КОРОТКИЕ ЗАПИСИ (часто используются):
let число = 10;
число += 5;  // то же, что число = число + 5
число -= 3;  // то же, что число = число - 3
число *= 2;  // то же, что число = число × 2
число /= 4;  // то же, что число = число ÷ 4
число %= 3;  // то же, что число = число % 3

// ИНКРЕМЕНТ и ДЕКРЕМЕНТ (увеличить/уменьшить на 1):
let счетчик = 0;
счетчик++;    // Увеличить на 1 (постфиксная форма)
++счетчик;    // Увеличить на 1 (префиксная форма)
счетчик--;    // Уменьшить на 1

// В чём разница между ++x и x++?
let x = 5;
let y = x++;  // 1) y получает СТАРОЕ значение x (5), 2) x увеличивается до 6
console.log(x, y); // 6, 5

let m = 5;
let n = ++m;  // 1) m увеличивается до 6, 2) n получает НОВОЕ значение m (6)
console.log(m, n); // 6, 6

2. Операции со строками

// КОНКАТЕНАЦИЯ (сложение строк):
let имя = "Иван";
let фамилия = "Петров";
let полноеИмя = имя + " " + фамилия; // "Иван Петров"

// ШАБЛОННЫЕ СТРОКИ (современный способ):
let возраст = 25;
let сообщение = `Меня зовут ${имя} и мне ${возраст} лет`;
// Результат: "Меня зовут Иван и мне 25 лет"

// Полезные методы строк:
let текст = "  JavaScript это интересно  ";
console.log(текст.length);           // 30 (длина строки включая пробелы)
console.log(текст.toUpperCase());    // "  JAVASCRIPT ЭТО ИНТЕРЕСНО  "
console.log(текст.toLowerCase());    // "  javascript это интересно  "
console.log(текст.trim());           // "JavaScript это интересно" (убирает пробелы по краям)
console.log(текст.indexOf("интересно")); // 15 (позиция начала слова)
console.log(текст.includes("JS"));   // false (содержит ли строка "JS"?)
console.log(текст.slice(2, 12));     // "JavaScript" (вырезать часть строки)

3. Сравнение (всегда возвращает true или false)

САМАЯ ВАЖНАЯ ЧАСТЬ ДЛЯ НАЧИНАЮЩИХ! Здесь много подводных камней.

// СТРОГОЕ СРАВНЕНИЕ (ИСПОЛЬЗУЙТЕ ВСЕГДА ЭТО!):
console.log(5 === 5);    // true (число 5 равно числу 5)
console.log(5 === "5");  // false (число 5 НЕ равно строке "5")
console.log(5 !== 3);    // true (5 не равно 3)
console.log(5 !== "5");  // true (число 5 не равно строке "5")

// НЕСТРОГОЕ СРАВНЕНИЕ (ИЗБЕГАЙТЕ! может давать странные результаты):
console.log(5 == "5");   // true (JavaScript пытается "угадать", преобразует типы)
console.log(0 == false); // true (0 и false считаются равными!)
console.log("" == false); // true (пустая строка и false считаются равными!)
console.log(null == undefined); // true (ещё одна странность)

// БОЛЬШЕ/МЕНЬШЕ:
console.log(10 > 5);     // true
console.log(10 >= 10);   // true (больше или равно)
console.log(3 < 5);      // true
console.log(3 <= 3);     // true (меньше или равно)

// СРАВНЕНИЕ СТРОК (по алфавиту):
console.log("яблоко" > "банан");   // true (буква "я" дальше в алфавите)
console.log("Анна" < "анна");      // true (заглавные буквы идут перед строчными)
console.log("10" > "2");           // false! (это строки, сравнивается посимвольно: "1" < "2")

4. Логические операции (для условий)

// && (И) — true, если ОБА выражения true
console.log(true && true);    // true
console.log(true && false);   // false
console.log(10 > 5 && 3 < 4); // true (оба сравнения true)

// || (ИЛИ) — true, если ХОТЯ БЫ ОДНО выражение true
console.log(true || false);   // true
console.log(false || false);  // false
console.log(10 < 5 || 3 < 4); // true (второе выражение true)

// ! (НЕ) — инвертирует значение
console.log(!true);   // false
console.log(!false);  // true
console.log(!(5 > 3)); // false (5 > 3 это true, инвертируем → false)

// ПРАКТИЧЕСКОЕ ПРИМЕНЕНИЕ:
let возраст = 25;
let естьПаспорт = true;

let можетГолосовать = возраст >= 18 && естьПаспорт; // true
let можетВодить = возраст >= 18 || естьВодительскиеПрава; // зависит от прав

// КОРОТКОЕ ЗАМЫКАНИЕ (важная особенность):
let x = 0;
let y = 5;
// Если первая часть && false, вторая не вычисляется
console.log(x > 1 && y++); // false, y остаётся 5 (y++ не выполнился)

// Если первая часть || true, вторая не вычисляется
console.log(x === 0 || y++); // true, y остаётся 5

5. Тернарный оператор (сокращённый if-else)

// Обычный if-else:
let доступ;
if (возраст >= 18) {
    доступ = "взрослый";
} else {
    доступ = "ребёнок";
}

// Тернарный оператор (то же самое в одну строку):
let доступ = (возраст >= 18) ? "взрослый" : "ребёнок";
// Синтаксис: условие ? значениеЕслиTrue : значениеЕслиFalse

// Более сложный пример:
let цена = 100;
let итоговаяЦена = (цена > 50) ? цена * 0.9 : цена; // 10% скидка, если цена > 50
// Если цена > 50, то итоговаяЦена = цена * 0.9, иначе итоговаяЦена = цена

6. Специальные случаи и подводные камни

// NaN (Not a Number) — результат некорректной математической операции
console.log(0 / 0);      // NaN
console.log("текст" * 2); // NaN
console.log(Math.sqrt(-1)); // NaN

// Важно: NaN не равно даже самому себе!
console.log(NaN === NaN); // false (!)
// Проверяйте так:
console.log(isNaN(NaN));    // true
console.log(Number.isNaN(NaN)); // true (более надёжно)

// Бесконечность:
console.log(1 / 0);      // Infinity
console.log(-1 / 0);     // -Infinity
console.log(Infinity > 1000000); // true

// Приведение типов (когда JavaScript сам меняет типы):
console.log("5" + 3);    // "53" (число 3 превращается в строку "3")
console.log("5" - 3);    // 2 (строка "5" превращается в число 5)
console.log("5" * "2");  // 10 (обе строки превращаются в числа)
console.log(true + 1);   // 2 (true превращается в 1)
console.log(false + 1);  // 1 (false превращается в 0)

📝 Задание для самопроверки (попробуйте в редакторе курса!)

Что выведет этот код? Проверьте свои догадки в нашем встроенном редакторе.

let a = "10";
let b = 5;
console.log(a + b);       // 1. ?
console.log(a - b);       // 2. ?
console.log(a > b);       // 3. ?

let x = 0;
let y = "0";
console.log(x == y);      // 4. ?
console.log(x === y);     // 5. ?

let list = [1, 2, 3];
list.push(4);
list[0] = 10;
console.log(list);        // 6. ?

const obj = { prop: "значение" };
obj.prop = "новое";
// obj = { другой: "объект" }; // 7. Что будет, если раскомментировать?

Ответы: 1) "105", 2) 5, 3) true, 4) true, 5) false, 6) [10, 2, 3, 4], 7) Ошибка "Assignment to constant variable"

Главный совет: Не пытайтесь запомнить всё сразу! Программирование — это навык, который развивается практикой. Используйте тренажёр задач после каждого раздела, экспериментируйте в редакторе, делайте ошибки и исправляйте их. Через несколько практических занятий эти концепции станут естественными.

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

2.1. Условия (Ветвление)

Условия позволяют коду принимать решения на основе данных. if-else — базовая конструкция, switch — для множественных случаев.

// if-else: проверяет условие (truthy/falsy). Можно цепочкой.
let weather = "дождь";

if (weather === "дождь") {
    console.log("Возьми зонт!");
} else if (weather === "солнце") {
    console.log("Надень очки!");
} else {
    console.log("Одевайся обычно");
}

// Пример с несколькими условиями:
if (age >= 18 && hasID) {
    console.log("Вход разрешен");
} else {
    console.log("Вход запрещен");
}

// switch-case: для сравнения с несколькими значениями. Использует строгое ===. Без break — fall-through (провалится в следующий case).
let day = "понедельник";

switch(day) {
    case "понедельник":
        console.log("Начало недели");
        break; // Обязателен, иначе выполнит все ниже!
    case "вторник":
    case "среда":
    case "четверг":
        console.log("Будни"); // Группировка случаев без break между ними
        break;
    case "пятница":
        console.log("Пятница!");
        break;
    default:
        console.log("Выходной"); // Для всех остальных
}

Расширенное объяснение: switch эффективен для enum-подобных значений (строки, числа). Альтернатива — объект с ключами: const actions = { "дождь": () => "Зонт", "солнце": () => "Очки" }; actions[weather]() || "Обычно";. Это более гибко для динамических случаев. Избегайте глубоких вложенных if — используйте early return для читаемости.

2.2. Циклы (Повторение)

Циклы повторяют код. Выберите тип в зависимости от сценария: известное количество итераций (for), условие (while), итерация по коллекциям (for...of/in).

// for: классический, для известного количества. Синтаксис: инициализация; условие; инкремент.
for(let i = 0; i < 5; i++) {
    console.log("Шаг", i); // i локальна благодаря let
}
// Вывод: Шаг 0 ... Шаг 4

// Расширенный: вложенные циклы для матриц
for(let row = 0; row < 3; row++) {
    for(let col = 0; col < 3; col++) {
        console.log(`Клетка ${row},${col}`);
    }
}

// while: пока условие true. Полезен, когда итерации зависят от внешних факторов.
let attempts = 0;
while(attempts < 3) {
    console.log("Попытка номер", attempts + 1);
    attempts++;
}

// do-while: гарантирует хотя бы одно выполнение (постпроверка).
let userInput;
do {
    userInput = prompt("Введите пароль:"); // Спросит минимум раз
} while(userInput !== "12345");

// for...of: итерация по iterable (массивы, строки, Set, Map). Получает значение.
let fruits = ["яблоко", "банан", "апельсин"];
for(let fruit of fruits) {
    console.log(fruit);
}

// for...in: итерация по перечисляемым свойствам объектов (ключи). Не для массивов (порядок не гарантирован).
let person = {name: "Анна", age: 25};
for(let key in person) {
    console.log(key, ":", person[key]);
}

Расширенное: Для прерывания используйте break (выход из цикла), continue (пропуск итерации). Избегайте бесконечных циклов (добавьте счетчики). Для производительности кэшируйте длину: for(let i=0, len=array.length; i<len; i++). В ES6+ prefer for...of для читаемости.

Часть 3: Структуры данных

3.1. Массивы (Списки значений)

Массивы — упорядоченные коллекции. Они динамические (размер меняется), индексированы с 0. Массивы — это объекты, но с особым поведением.

// Создание: литерал [] или new Array().
let emptyArray = [];
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, "текст", true, null]; // Гетерогенные типы разрешены

// Доступ: по индексу. Отрицательные индексы не поддерживаются (используйте at() в ES2022).
let first = numbers[0];  // 1
let last = numbers[numbers.length - 1]; // 5
console.log(numbers.at(-1)); // 5 (ES2022, отрицательный индекс от конца)

// Мутация: массивы мутабельны.
numbers[0] = 10; // [10, 2, 3, 4, 5]
numbers.push(6); // [10, 2, 3, 4, 5, 6] - добавляет в конец, возвращает новую длину
let popped = numbers.pop(); // 6, массив: [10, 2, 3, 4, 5]
numbers.unshift(0); // [0, 10, 2, 3, 4, 5] - добавляет в начало (медленно для больших массивов)
numbers.shift();    // [10, 2, 3, 4, 5] - удаляет первый

Методы массивов (средний уровень): Многие методы возвращают новый массив (иммутабельные), другие мутируют оригинал. Это функциональный стиль (map, filter) vs императивный.

let fruits = ["Яблоко", "Банан", "Апельсин"];

// map: трансформирует каждый элемент, возвращает новый массив.
let upperFruits = fruits.map(fruit => fruit.toUpperCase()); // ["ЯБЛОКО", "БАНАН", "АПЕЛЬСИН"]
// Внутри: callback(элемент, индекс, массив)

// filter: возвращает новый массив с элементами, прошедшими тест.
let longFruits = fruits.filter(fruit => fruit.length > 5); // ["Яблоко", "Апельсин"]

// find: возвращает первый элемент, прошедший тест (или undefined).
let found = fruits.find(fruit => fruit.startsWith("Б")); // "Банан" (startsWith - метод строки)

// forEach: выполняет callback для каждого, ничего не возвращает (для side-effects).
fruits.forEach((fruit, index) => console.log(`${index}: ${fruit}`));

// reduce: аккумулирует значение (сумма, max и т.д.). Callback(аккумулятор, элемент, индекс, массив), initialValue.
let numbers2 = [1, 2, 3, 4];
let sum = numbers2.reduce((total, num) => total + num, 0); // 10
let max = numbers2.reduce((max, num) => Math.max(max, num), -Infinity); // 4

// slice: копирует часть, не мутирует. slice(start, end) - end не включительно.
let part = fruits.slice(1, 3); // ["Банан", "Апельсин"]
let copy = fruits.slice(); // Полная копия

// splice: мутирует, удаляет/вставляет. splice(start, deleteCount, ...items)
fruits.splice(1, 1, "Груша"); // Удаляет 1 элемент с индекса 1, вставляет "Груша". fruits: ["Яблоко", "Груша", "Апельсин"]

Расширенное: some/every — проверка условий (возвращают boolean). indexOf/lastIndexOf — поиск позиции. flat/flatMap — для вложенных массивов. Ошибки: Не путайте мутирующие методы (push, splice) с иммутабельными (map, filter) — это важно в React/Redux. Для глубоких копий используйте JSON.parse(JSON.stringify(arr)) или structuredClone (ES2022).

3.2. Объекты (Структурированные данные)

Объекты — неупорядоченные коллекции ключ-значение (хэш-мапы). Ключи — строки или Symbol. Объекты мутабельны, передаются по ссылке.

// Создание: литерал {} или new Object().
let user = {
    firstName: "Иван", // Свойство (key: value)
    lastName: "Петров",
    age: 30;
    isAdmin: false,
    
    // Метод: функция как значение.
    getFullName: function() { // Классическая запись
        return this.firstName + " " + this.lastName; // this - ссылка на объект
    },
    
    greet() { // Короткая запись (ES6), эквивалентно function greet()
        return `Привет, я ${this.firstName}!`;
    }
};

// Доступ: точка (для известных ключей) или скобки (для динамических).
console.log(user.firstName);     // "Иван"
console.log(user["lastName"]);   // "Петров" (полезно для переменных: let key = "age"; user[key])

// Мутация:
user.email = "ivan@example.com"; // Добавление
user["phone"] = "+79123456789";
user.age = 31; // Изменение

// Удаление:
delete user.isAdmin; // Удаляет свойство

// Проверка:
if ("age" in user) { // true
    console.log("Возраст указан");
}
console.log(user.hasOwnProperty("age")); // true (только собственные свойства, не прототипные)

Расширенное: Object.keys(user) — массив ключей, Object.values — значений, Object.entries — пар [key, value]. Object.assign(target, ...sources) — слияние объектов (поверхностное). Для глубокого слияния используйте библиотеки как lodash. this в методах: Контекст зависит от вызова (user.greet() — this=user). Потеря this в callbacks — решайте arrow functions или bind.

// Пример потери this:
let greetFunc = user.greet;
greetFunc(); // undefined (this=window или undefined в strict mode)

// Решение:
let boundGreet = user.greet.bind(user); // Фиксирует this
boundGreet(); // Работает

Лучшая практика: Используйте короткую запись методов. Для immutable объектов — Object.freeze(user) (запрещает изменения).

Часть 4: Функции - основа JavaScript

4.1. Базовые функции

Есть три способа объявления: declaration (hoisting), expression, arrow (ES6, нет своего this/arguments).

// 1. Declaration: поднимается, можно вызвать до объявления.
function sayHello(name) { // Параметры в ()
    return `Привет, ${name}!`; // return - выход с значением
}
sayHello("Мир"); // "Привет, Мир!" (аргументы)

// 2. Expression: присваивается переменной, нет hoisting.
const sayGoodbye = function(name) {
    return `Пока, ${name}!`;
};

// 3. Arrow: компактная, нет своего this (берет из окружения), не может быть конструктором.
const add = (a, b) => a + b; // Неявный return для одной строки
const square = x => x * x; // Для одного параметра без ()
const greet = name => { // Блок {} требует явного return
    const message = `Привет, ${name}`;
    return message;
};

Параметры: По умолчанию, rest, деструктуризация (средний уровень).

// Default parameters (ES6):
function greet(name = "Гость") { // Если аргумент не передан
    return `Привет, ${name}`;
}
greet(); // "Привет, Гость"

// Rest: собирает аргументы в массив.
function sumAll(...numbers) { // ... - rest operator
    return numbers.reduce((total, num) => total + num, 0);
}
sumAll(1, 2, 3, 4); // 10

// Деструктуризация: извлекает свойства из объектов/массивов.
function printUser({name, age = 18}) { // Default в деструктуре
    console.log(`${name}, ${age} лет`);
}
printUser({name: "Анна", age: 25}); // "Анна, 25 лет"

Arguments объект: Псевдомассив всех аргументов (устарел, prefer rest).

Ошибки: Передача лишних аргументов игнорируется, недостающие — undefined. Используйте strict mode ("use strict") для строгих правил.

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

// 1. Global: вне функций, видна везде (избегайте загрязнения).
let globalVar = "Я глобальная";
console.log(window.globalVar); // В браузере global = window

function test() {
    console.log(globalVar); // Доступна
}

// 2. Function: внутри функций.
function outer() {
    let outerVar = "Я в outer";
    
    function inner() {
        let innerVar = "Я в inner";
        console.log(outerVar); // Лексическая видимость (внешний scope)
        console.log(globalVar);
    }
    
    inner();
    // console.log(innerVar); // ReferenceError (не видна)
}

// 3. Block: для let/const внутри {} (if, for, {}).
{
    let blockVar = "Я в блоке";
    var oldVar = "Я var"; // var - функциональный scope, игнорирует блоки
}
// console.log(blockVar); // ReferenceError
console.log(oldVar); // "Я var"

Scope chain: Поиск переменной идет вверх по цепочке scopes до global. Strict mode: Запрещает неявные глобальные переменные.

4.3. Замыкания

function createCounter() {
    let count = 0; // Приватная, сохраняется в замыкании
    
    return function() { // Внутренняя функция замыкает count
        count++; // Доступ к внешнему scope
        return count;
    };
}

const counter1 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2

const counter2 = createCounter(); // Независимое замыкание
console.log(counter2()); // 1

Практика (средний уровень): Модульный паттерн для приватности.

function createBankAccount(initialBalance) {
    let balance = initialBalance; // Приватная переменная
    
    return { // Объект с методами, замыкающими balance
        deposit(amount) {
            if (amount > 0) { // Валидация
                balance += amount;
            }
            return balance;
        },
        withdraw(amount) {
            if (amount <= balance) {
                balance -= amount;
                return balance;
            }
            return "Недостаточно средств";
        },
        getBalance() {
            return balance; // Только чтение
        }
    };
}

let account = createBankAccount(1000);
account.deposit(500); // 1500
// balance недоступна напрямую — инкапсуляция

Расширенное: Замыкания потребляют память (держат scope живым). В циклах с var — проблема (все замыкания видят последнее значение), решайте let или IIFE. Применение: currying, event handlers.

// Currying с замыканием
function multiply(a) {
    return function(b) {
        return a * b;
    };
}
const double = multiply(2);
double(5); // 10

Часть 5: Работа с DOM (Браузер)

5.1. Поиск элементов

Методы document для поиска. querySelector — мощный, как CSS.

// getElementById: быстрый, по уникальному id.
let header = document.getElementById("header"); // null если не найден

// getElementsByClassName: коллекция (HTMLCollection, живая — обновляется автоматически).
let items = document.getElementsByClassName("item"); // Массивоподобный, for...of работает

// getElementsByTagName: по тегу.
let paragraphs = document.getElementsByTagName("p");

// querySelector: первый по CSS-селектору.
let element = document.querySelector(".container .item"); // .class, #id, tag, [attr]

// querySelectorAll: все, возвращает NodeList (статический, forEach поддерживает).
let allElements = document.querySelectorAll(".item");

Расширенное: Поиск от элемента: element.querySelector(".child"). Живые vs статические коллекции: getElements* — живые (изменения DOM отражаются), querySelectorAll — снимок.

5.2. Изменение содержимого

Манипуляция текстом, стилями, классами.

let div = document.getElementById("myDiv");

// Текст: textContent - чистый текст (безопасный от XSS), innerHTML - с HTML (опасный, если пользовательский ввод).
div.textContent = "Новый текст";
div.innerHTML = "<strong>Жирный</strong> текст"; // Парсит HTML

// Стили: inline styles.
div.style.color = "red"; // camelCase для свойств вроде background-color
div.style.backgroundColor = "#f0f0f0";
div.style.display = "none"; // Скрыть (удаляет из потока)
div.style.visibility = "hidden"; // Скрыть, но занимает место

// Классы: classList - объект для манипуляции.
div.classList.add("active", "visible"); // Добавляет несколько
div.classList.remove("old");
div.classList.toggle("hidden"); // Добавляет/удаляет
div.classList.contains("active"); // true/false
div.classList.replace("old", "new"); // Замена

Расширенное: Для производительности prefer classList над style (CSS правила лучше). Атрибуты: div.setAttribute("data-id", "123"), div.getAttribute("data-id").

5.3. События

События — реакции на действия пользователя/DOM. addEventListener — стандарт.

let button = document.querySelector("button");

// onclick: старый, перезаписывает (не используйте).
button.onclick = function() {
    console.log("Клик!");
};

// addEventListener: добавляет несколько, options (once, passive).
button.addEventListener("click", function(event) { // event - объект с деталями
    console.log("Клик!", event.target, event.type); // target - элемент, type - "click"
});

// Основные события:
element.addEventListener("click", handler);      // Клик (мышь)
element.addEventListener("dblclick", handler);   // Двойной клик
element.addEventListener("mouseover", handler);  // Наведение
element.addEventListener("mouseout", handler);   // Уход
element.addEventListener("focus", handler);      // Фокус (для input)
element.addEventListener("blur", handler);       // Потеря фокуса
element.addEventListener("keydown", handler);    // Клавиша вниз (event.key, event.code)
element.addEventListener("submit", handler);     // Форма (для form)

// Удаление: removeEventListener (нужен тот же handler).
let handler = () => console.log("Click");
button.addEventListener("click", handler);
button.removeEventListener("click", handler);

// Пропагация: bubbling (всплытие от target вверх), capturing (захват сверху вниз).
// Отмена: event.preventDefault() - отменяет default (submit, click на link)
link.addEventListener("click", function(event) {
    event.preventDefault(); // Не перейти по ссылке
    event.stopPropagation(); // Остановить всплытие
    event.stopImmediatePropagation(); // Остановить все handlers на этом элементе
});

Расширенное (средний уровень): Delegation — слушатель на родителе, проверка event.target.matches(".selector"). Полезно для динамических элементов. Custom events: new CustomEvent("myEvent", {detail: data}), element.dispatchEvent.

5.4. Создание элементов

Динамическое создание/удаление.

// createElement: создает узел.
let newDiv = document.createElement("div"); // <div></div>
newDiv.textContent = "Я новый элемент!";

// Добавление:
document.body.appendChild(newDiv); // В конец body
parentElement.insertBefore(newDiv, referenceElement); // Перед reference
parentElement.append(newDiv, anotherDiv); // ES2016, несколько
parentElement.prepend(newDiv); // В начало

// Удаление:
let element = document.getElementById("toRemove");
element.remove(); // Простой
// Старый: element.parentNode.removeChild(element);

// Клонирование:
let clone = element.cloneNode(true); // true - глубокое (с детьми), false - поверхностное

Расширенное: Для массовых изменений используйте DocumentFragment (виртуальный контейнер, минимизирует reflows).

let frag = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
    let p = document.createElement("p");
    p.textContent = `Параграф ${i}`;
    frag.appendChild(p);
}
document.body.appendChild(frag); // Одна вставка

Часть 6: Асинхронность

6.1. Callbacks (Старый способ)

Callbacks — функции, передаваемые как аргументы, вызываются по завершении.

// Пример: setTimeout - асинхронный таймер.
function getData(callback) {
    setTimeout(() => { // Имитация задержки (network, IO)
        console.log("Данные получены");
        callback("data");
    }, 1000);
}

getData(function(data) {
    console.log("Обработали:", data);
    getMoreData(function(moreData) { // Callback hell - вложенность, сложно читать/ошибки
        console.log("Еще:", moreData);
    });
});

Проблемы: Pyramid of doom, сложная обработка ошибок (нужен callback для error). Альтернативы: Promises.

6.2. Promises (Обещания)

Promise — объект для асинхронных операций. Состояния: pending, fulfilled (resolve), rejected (reject).

// Создание:
let promise = new Promise((resolve, reject) => { // Executor выполняется сразу
    setTimeout(() => {
        let success = true;
        if (success) {
            resolve("Данные загружены!"); // fulfilled
        } else {
            reject(new Error("Ошибка загрузки")); // rejected
        }
    }, 2000);
});

// Цепочка: then (для resolve), catch (для reject), finally (всегда).
promise
    .then(result => { // result из resolve
        console.log("Успех:", result);
        return result + " обработаны"; // Передает в следующий then
    })
    .then(processed => {
        console.log("Обработано:", processed);
        throw new Error("Имитация ошибки"); // Перейдет в catch
    })
    .catch(error => { // Ловит ошибки из then/reject
        console.error("Ошибка:", error.message);
    })
    .finally(() => {
        console.log("Завершено"); // Очистка
    });

Расширенное (средний уровень): Promise.all([p1, p2]) — ждет все, reject при первой ошибке. Promise.race — первый завершенный. Promise.allSettled — все, с статусами. Promise.any — первый fulfilled. Ошибки: Всегда добавляйте catch, иначе unhandled rejection.

6.3. Async/Await (Современный способ)

Синтаксический сахар над Promises. Делает код линейным.

async function loadData() { // async - возвращает Promise
    try { // Для ошибок
        console.log("Начинаем...");
        
        let data = await fetchData(); // await - ждет resolve, бросает reject в catch
        console.log("Данные:", data);
        
        let processed = await processData(data);
        console.log("Обработано:", processed);
        
        return processed; // resolve
    } catch (error) {
        console.error("Ошибка:", error);
        throw error; // reject
    }
}

// Использование: как Promise
loadData()
    .then(result => console.log("Финал:", result))
    .catch(err => console.log("Ошибка в вызове:", err));

Расширенное: В циклах: for await (of asyncIterable). Параллель: let [d1, d2] = await Promise.all([p1, p2]). Top-level await в модулях. Ошибки: Всегда try-catch, await только в async.

6.4. Fetch API (Работа с HTTP)

Built-in для запросов. Возвращает Promise с Response.

// GET: простой запрос.
async function fetchUsers() {
    try {
        let response = await fetch('https://api.example.com/users'); // GET по умолчанию
        
        if (!response.ok) { // Проверка статус 200-299
            throw new Error(`HTTP ошибка: ${response.status}`); // 404, 500 и т.д.
        }
        
        let users = await response.json(); // Парсит body как JSON (Promise)
        // Альтернативы: .text(), .blob(), .formData()
        console.log(users);
        return users;
    } catch (error) {
        console.error("Ошибка:", error);
        return []; // Fallback
    }
}

// POST: с опциями.
async function createUser(userData) {
    let response = await fetch('https://api.example.com/users', {
        method: 'POST', // GET, POST, PUT, DELETE
        headers: { // Заголовки
            'Content-Type': 'application/json',
            'Authorization': 'Bearer token' // Для аутентификации
        },
        body: JSON.stringify(userData), // Stringify для JSON
        credentials: 'include', // Cookies
        mode: 'cors' // CORS
    });
    
    if (!response.ok) {
        throw new Error(await response.text());
    }
    return await response.json();
}

Расширенное (средний уровень): AbortController для отмены: let controller = new AbortController(); fetch(url, {signal: controller.signal}); controller.abort(). Headers — Map-подобный. Для form: new FormData(formElement).

Часть 7: ООП в JavaScript

ООП в JS прототипное, не классовое (под капотом прототипы). ES6 классы — сахар.

7.1. Конструкторы (Классический подход)

Функции для создания объектов с общими методами.

function Person(name, age) { // Конструктор (new вызывает)
    this.name = name;
    this.age = age;
    
    this.greet = function() { // Метод на экземпляре (копируется каждый раз - неэффективно)
        console.log(`Привет, я ${this.name}`);
    };
}

let person1 = new Person("Анна", 25); // new создает {}, устанавливает this, прототип
person1.greet();

// Проблема: методы дублируются. Решение: прототип.

Расширенное: Без new — this=global, ошибки. Проверяйте: if (!(this instanceof Person)) return new Person(...).

7.2. Прототипы

Прототип — объект для общих свойств. Цепочка прототипов (inheritance).

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

// Прототип: общие методы
Person.prototype.greet = function() {
    console.log(`Привет, я ${this.name}`);
};

Person.prototype.getBirthYear = function() {
    return new Date().getFullYear() - this.age;
};

let p = new Person("Иван", 30);
p.greet(); // Находит в прототипе

// Наследование:
function Student(name, age, major) {
    Person.call(this, name, age); // Вызов супер-конструктора
    this.major = major;
}

Student.prototype = Object.create(Person.prototype); // Связь прототипов
Student.prototype.constructor = Student; // Восстановление

Student.prototype.study = function() {
    console.log(`${this.name} изучает ${this.major}`);
};

let s = new Student("Мария", 20, "Математика");
s.greet(); // Из Person.prototype
s.study();
console.log(s instanceof Student); // true
console.log(s instanceof Person); // true (цепочка)

Расширенное (сложный уровень): __proto__ - ссылка на прототип (не используйте напрямую). Object.getPrototypeOf(obj). Для mixin: Object.assign(Student.prototype, mixinObj). Прототипы экономят память.

7.3. Классы ES6 (Современный синтаксис)

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

// Базовый класс
class Person {
    // Конструктор
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // Метод
    greet() {
        console.log(`Привет, я ${this.name}`);
    }
    
    // Геттер (вызывается как свойство)
    get birthYear() {
        return new Date().getFullYear() - this.age;
    }
    
    // Сеттер
    set nickname(value) {
        this._nickname = value;
    }
    
    // Статический метод (принадлежит классу, а не объекту)
    static compareAge(a, b) {
        return a.age - b.age;
    }
}

// Наследование
class Student extends Person {
    constructor(name, age, major) {
        super(name, age); // Вызов родительского конструктора
        this.major = major;
    }
    
    // Переопределение метода (полиморфизм)
    greet() {
        super.greet(); // Вызов родительского метода
        console.log(`Я изучаю ${this.major}`);
    }
    
    // Свой метод
    study() {
        console.log(`${this.name} готовится к экзамену`);
    }
}

// Использование базового класса
let person = new Person("Алексей", 35);
person.greet(); // "Привет, я Алексей"
console.log(person.birthYear); // Текущий год - 35
person.nickname = "Alex"; // Установка через setter
console.log(person._nickname); // "Alex"

// Использование наследуемого класса
let student = new Student("Иван", 20, "Информатика");
student.greet(); // "Привет, я Иван" + "Я изучаю Информатика"
student.study(); // "Иван готовится к экзамену"
console.log(Person.compareAge(person, student)); // 15 (35 - 20)

// Дополнительный пример: Класс с приватными полями (ES2022+)
class BankAccount {
    #balance; // Приватное поле (недоступно извне)
    
    constructor(initialBalance) {
        this.#balance = initialBalance;
    }
    
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
        }
        return this.#balance;
    }
    
    get balance() { // Геттер для чтения приватного
        return this.#balance;
    }
    
    static createSavings(initial) { // Статический фабричный метод
        return new BankAccount(initial);
    }
}

let account = BankAccount.createSavings(1000); // Использование статического метода
account.deposit(500);
console.log(account.balance); // 1500 (через геттер)
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class

// Дополнительный пример: Полиморфизм с несколькими классами
class Animal {
    constructor(name) {
        this.name = name;
    }
    
    makeSound() {
        console.log("Звук животного");
    }
}

class Dog extends Animal {
    makeSound() {
        console.log("Гав!");
    }
}

class Cat extends Animal {
    makeSound() {
        console.log("Мяу!");
    }
}

// Массив с полиморфизмом
const animals = [new Dog("Бобик"), new Cat("Мурка")];
animals.forEach(animal => animal.makeSound()); // "Гав!" "Мяу!" (вызов переопределенных методов)

// Дополнительный пример: Композиция (вместо глубокого наследования)
class Engine {
    start() {
        console.log("Двигатель запущен");
    }
}

class Car {
    constructor() {
        this.engine = new Engine(); // Композиция: имеет Engine
    }
    
    drive() {
        this.engine.start();
        console.log("Машина едет");
    }
}

let myCar = new Car();
myCar.drive(); // "Двигатель запущен" "Машина едет"

Расширенное (сложный уровень): Приватные поля (#private, ES2022) обеспечивают настоящую инкапсуляцию. Super в методах для вызова родительских. Классы не поднимаются (no hoisting). Для полиморфизма переопределяйте методы. Предпочитайте композицию над наследованием для гибкости (принцип "has-a" vs "is-a"). Ошибки: Забыть super() в наследнике — ReferenceError. Используйте instanceof для проверки типа.

Часть 8: Продвинутые концепции

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

Извлечение значений из объектов/массивов. ES6.

// Массивы: по позиции.
let numbers = [1, 2, 3, 4, 5];
let [first, second, , fourth, ...rest] = numbers; // Пропуск ,, rest - остаток
// first=1, second=2, fourth=4, rest=[5]

// Обмен:
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1

// Объекты: по ключам.
let user = { name: "Анна", age: 25, city: "Москва" };
let { name, age, city = "Неизвестно" } = user; // Default
let { name: userName } = user; // Переименование

// Вложенная:
let { address: { street } } = { address: { street: "Main" } }; // street="Main"

Расширенное: В параметрах функций, с defaults: function({name="Гость"}={}) {}. Для модулей: import {a as b} from...

8.2. Spread и Rest операторы

... для разворачивания/сбора.

// Spread: в массивах/объектах/вызовах.
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let combined = [...arr1, ...arr2, 7]; // [1,2,3,4,5,6,7]

let obj1 = { x: 1, y: 2 };
let obj2 = { y: 3, z: 4 };
let merged = { ...obj1, ...obj2 }; // {x:1, y:3, z:4} (перезаписывает)

Math.max(...arr1); // 3 (spread в аргументы)

// Rest: в параметрах/деструктуре.
function sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

Расширенное: Копирование: let copy = {...obj} (поверхностное). В React: для props.

8.3. Модули

ES6 модули для организации кода. Экспорт/импорт.

math.js:

export const PI = 3.14159; // Named export

export function sum(a, b) {
    return a + b;
}

export default class Calculator { // Default export (один на модуль)
    multiply(a, b) {
        return a * b;
    }
}

app.js:

import Calculator, { PI, sum as add } from './math.js'; // Default без {}, named в {}, as - alias

console.log(PI);
console.log(add(2, 3));

let calc = new Calculator();
console.log(calc.multiply(4, 5));

Расширенное (сложный уровень): Dynamic import: import("./mod.js").then(mod => ...). Tree shaking в bundlers (Webpack). CommonJS (require/exports) для Node.

8.4. Генераторы

Функции, возвращающие итераторы. yield — пауза/возврат.

function* numberGenerator() { // * - generator
    yield 1; // Пауза, возвращает {value:1, done:false}
    yield 2;
    yield 3;
    return 4; // {value:4, done:true}
}

let gen = numberGenerator();
console.log(gen.next()); // {value:1, done:false}
console.log(gen.next().value); // 2

// for...of: автоматически next()
for (let num of numberGenerator()) {
    console.log(num); // 1 2 3 (return игнорируется)
}

// Двусторонняя: next(arg) - передает в yield
function* twoWay() {
    let input = yield "Give me input";
    console.log(input);
}
let g = twoWay();
g.next(); // {value:"Give me input", done:false}
g.next("Input"); // Выводит "Input"

Практика (сложный уровень): Ленивые последовательности.

function* fibonacci(limit) {
    let [prev, curr] = [0, 1];
    while (limit-- > 0) {
        yield curr; // Вычисляет по требованию
        [prev, curr] = [curr, prev + curr];
    }
}

for (let num of fibonacci(10)) {
    console.log(num); // 1 1 2 3 5 8 13 21 34 55
}

Расширенное: Async generators: async function*, yield await. Для streams, infinite sequences.

Часть 9: Практические паттерны

Паттерны — решения типичных проблем. Средний/сложный уровень.

9.1. Модульный паттерн

IIFE для приватности (до ES6 модулей).

const UserModule = (function() { // IIFE - immediate invoke
    // Приватные
    let users = [];
    let apiUrl = 'https://api.example.com';
    
    function validateUser(user) { // Приватный метод
        return user.name && user.email;
    }
    
    // Публичные
    return {
        addUser(user) {
            if (validateUser(user)) {
                users.push(user);
                return true;
            }
            return false;
        },
        
        getUsers() {
            return [...users]; // Копия для безопасности
        },
        
        async fetchUsers() {
            try {
                let response = await fetch(`${apiUrl}/users`);
                if (!response.ok) throw new Error("Fetch error");
                users = await response.json();
                return users;
            } catch (error) {
                console.error("Ошибка:", error);
                return [];
            }
        }
    };
})();

UserModule.addUser({name: "Анна", email: "anna@test.com"});
console.log(UserModule.getUsers());
UserModule.fetchUsers();

Расширенное: Augmenting modules — добавление методов позже. Альтернатива: ES6 classes с #private.

9.2. Паттерн Observer (Наблюдатель)

Для pub/sub (events). Базис для EventEmitter в Node.

class EventEmitter {
    constructor() {
        this.events = {}; // {event: [listeners]}
    }
    
    on(event, listener) { // Subscribe
        if (!this.events[event]) this.events[event] = [];
        this.events[event].push(listener);
        return () => this.off(event, listener); // Возврат unsubscribe
    }
    
    off(event, listener) { // Unsubscribe
        if (this.events[event]) {
            this.events[event] = this.events[event].filter(l => l !== listener);
        }
    }
    
    emit(event, ...data) { // Publish
        if (this.events[event]) {
            this.events[event].forEach(listener => listener(...data));
        }
    }
    
    once(event, listener) { // Один раз
        const wrapper = (...args) => {
            listener(...args);
            this.off(event, wrapper);
        };
        this.on(event, wrapper);
    }
}

const emitter = new EventEmitter();

const unsubscribe = emitter.on('login', user => {
    console.log(`Пользователь ${user.name} вошел`);
});

emitter.once('error', err => console.error(err));

emitter.emit('login', {name: "Анна"});
unsubscribe();
emitter.emit('login', {name: "Боб"}); // Не вызовет

Расширенное (сложный уровень): Namespaces для событий (event:namespace). Wildcards (*.event). Используйте в React (useEffect как observer).

9.3. Паттерн Singleton (Одиночка)

Один экземпляр класса.

class Database {
    static instance; // Статическое поле (ES6)
    
    constructor(config) {
        if (Database.instance) {
            return Database.instance; // Возврат существующего
        }
        
        this.connection = this.connect(config);
        Database.instance = this;
    }
    
    connect(config) {
        console.log("Подключаемся...");
        return { connected: true, config };
    }
    
    query(sql) {
        console.log("Запрос:", sql);
        return { result: "данные" };
    }
}

const db1 = new Database({host: 'localhost'});
const db2 = new Database({host: 'remote'});
console.log(db1 === db2); // true
db1.query("SELECT *");

Расширенное: Lazy init (в constructor). Для многопоточности — не подходит (JS однопоточный). Альтернатива: модули как singletons.

Часть 10: Советы и лучшие практики

10.1. Кодстайл

Следуйте стандартам (Airbnb, Google). Используйте linters (ESLint).

Хорошо:

const MAX_RETRIES = 3; // Константы uppercase
const API_URL = 'https://api.example.com'; // 单引号 для строк

function calculateTotal(items) { // camelCase, descriptive
    return items.reduce((sum, item) => sum + item.price, 0); // Функциональный стиль
}

class UserService { // PascalCase
    constructor(apiClient) {
        this.apiClient = apiClient;
    }
    
    async getUser(id) {
        try {
            return await this.apiClient.fetch(`/users/${id}`);
        } catch (error) {
            console.error('Ошибка загрузки:', error);
            throw error;
        }
    }
}

Плохо:

var maxretries=3; // Нет пробелов, var
var ApiUrl='https://api.example.com';

function calc(items){let s=0;for(let i=0;i<items.length;i++){s+=items[i].price;}return s;} // Не читаемо

Практики: 2-4 пробела, ; в конце, {} для if даже одной строки. Комментарии JSDoc.

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

Всегда ловите, логируйте, информируйте.

// try-catch в async:
async function fetchData() {
    try {
        let data = await apiCall();
        return process(data);
    } catch (error) {
        console.error('Ошибка в fetchData:', error.stack); // Stack для debug
        showUserMessage('Ошибка загрузки'); // UI
        return []; // Graceful degradation
    }
}

// Custom errors:
class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = 'ValidationError';
        this.field = field;
        this.timestamp = new Date();
    }
}

function validateUser(user) {
    if (!user.email) {
        throw new ValidationError('Email обязателен', 'email');
    }
}

try {
    validateUser({});
} catch (err) {
    if (err instanceof ValidationError) {
        console.log(`Поле: ${err.field}`);
    }
}

// Global handlers:
window.addEventListener('error', event => {
    console.error('Глобальная:', event.error);
    // Sentry или analytics
});

window.addEventListener('unhandledrejection', event => {
    console.error('Promise rejection:', event.reason);
    event.preventDefault(); // Не показывать в консоли
});

Расширенное: Error boundaries в React. В Node: process.on('uncaughtException').

10.3. Производительность

Оптимизируйте циклы, DOM, события.

// Циклы: кэш длины.
for (let i = 0, len = array.length; i < len; i++) {} // ХОРОШО

// DOM: batch updates с fragment.
let fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    let div = document.createElement('div');
    div.textContent = `Элемент ${i}`;
    fragment.appendChild(div);
}
document.body.appendChild(fragment); // Один reflow

// Debounce: для resize, scroll (throttle - фиксированная частота).
function debounce(func, delay) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), delay);
    };
}

window.addEventListener('resize', debounce(() => {
    console.log('Изменено');
}, 250));

// Throttle:
function throttle(func, limit) {
    let inThrottle;
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

Расширенное: Web Workers для тяжелых вычислений. Memoization для функций. Профилируйте с Chrome DevTools.

Часть 11: TypeScript - Надстройка над JavaScript

TypeScript (TS) - это типизированный superset JavaScript, компилируемый в JS. Он добавляет статическую типизацию, интерфейсы, generics и другие фичи для больших проектов. TS помогает ловить ошибки на этапе разработки, улучшает автодополнение и рефакторинг. Установка: npm install -g typescript, компиляция: tsc file.ts. Конфигурация в tsconfig.json.

11.1. Базовые типы и переменные

В TS переменные имеют типы, объявляемые явно. Это предотвращает ошибки типов в runtime.

// Базовые типы: number, string, boolean, null, undefined, any (избегайте, отключает типизацию), unknown (безопасный any).
let age: number = 25; // Явный тип
// age = "двадцать пять"; // Ошибка компиляции: Type 'string' is not assignable to type 'number'.

let name: string = "Анна";
let isStudent: boolean = true;
let nothing: null = null;
let maybe: undefined = undefined;

let flexible: any = 42; // Можно присвоить что угодно, но избегайте
flexible = "текст"; // Нет ошибки

let safe: unknown = 42; // Нужно проверить тип перед использованием
if (typeof safe === "number") {
    let sum = safe + 10; // OK
}

// Массивы и кортежи (tuples)
let numbers: number[] = [1, 2, 3]; // Массив чисел
// numbers.push("четыре"); // Ошибка

let tuple: [string, number] = ["Анна", 25]; // Фиксированная длина и типы
// tuple[0] = 30; // Ошибка

Почему типы? В JS типы динамические, что приводит к runtime-ошибкам (например, сложение строки и числа). TS проверяет на compile-time. Лучшая практика: Используйте строгие типы, избегайте any. Для опциональных: let optional?: string;.

11.2. Интерфейсы и типы

Интерфейсы описывают структуру объектов. Union и intersection для комбинаций.

// Интерфейс: контракт для объектов
interface User {
    name: string; // Обязательное
    age: number;
    email?: string; // Опциональное
    greet(): string; // Метод
}

let user: User = {
    name: "Иван",
    age: 30,
    greet() {
        return `Привет, ${this.name}`;
    }
};
// user.age = "тридцать"; // Ошибка

// Type alias: альтернатива интерфейсам для простых типов
type ID = string | number; // Union: строка или число
let userId: ID = "abc123";
userId = 123; // OK

// Intersection: комбинация типов
type Admin = User & { role: string }; // & - intersection
let admin: Admin = {
    name: "Админ",
    age: 40,
    role: "admin",
    greet() { return "Привет"; }
};

Расширенное: Extends для наследования интерфейсов: interface Student extends User { major: string; }. Readonly: readonly name: string; для иммутабельности. Ошибки: Несоответствие интерфейсу — compile error.

11.3. Функции в TypeScript

Типы для параметров и возвращаемого значения. Overloads для перегрузок.

// Базовая функция с типами
function add(a: number, b: number): number { // :number - тип возврата
    return a + b;
}
// add("1", 2); // Ошибка: Argument of type 'string' is not assignable to parameter of type 'number'.

// Опциональные и default параметры
function greet(name: string, greeting: string = "Привет"): string {
    return `${greeting}, ${name}`;
}

// Rest параметры
function sum(...numbers: number[]): number {
    return numbers.reduce((total, num) => total + num, 0);
}

// Arrow функции
const multiply = (a: number, b: number): number => a * b;

// Overloads: несколько сигнатур
function combine(a: number, b: number): number;
function combine(a: string, b: string): string;
function combine(a: any, b: any): any {
    return a + b;
}
combine(1, 2); // 3
combine("a", "b"); // "ab"

Расширенное: Типизация callbacks: (callback: (x: number) => void). Async: async function fetchData(): Promise<User[]>. Лучшая практика: Всегда типизируйте функции для ясности.

11.4. Классы и ООП в TypeScript

Классы как в ES6, но с типами, модификаторами доступа (public, private, protected).

class Person {
    private name: string; // Private: доступ только внутри класса
    public age: number; // Public: по умолчанию
    protected nickname: string; // Protected: доступ в классе и наследниках
    
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    
    greet(): string {
        return `Привет, я ${this.name}`;
    }
}

class Student extends Person {
    major: string;
    
    constructor(name: string, age: number, major: string) {
        super(name, age);
        this.major = major;
        this.nickname = "Студент"; // Доступ к protected
    }
    
    greet(): string { // Override с тем же типом
        return super.greet() + `, изучаю ${this.major}`;
    }
}

let student = new Student("Иван", 20, "Информатика");
console.log(student.greet()); // "Привет, я Иван, изучаю Информатика"
// console.log(student.name); // Ошибка: Property 'name' is private

Расширенное: Abstract классы: abstract class Shape { abstract area(): number; }. Readonly свойства. Ошибки: Несовместимые override — compile error.

11.5. Generics

Generics для обобщенных типов (как шаблоны в других языках).

// Generic функция: T - placeholder типа
function identity<T>(arg: T): T {
    return arg;
}
let num = identity<number>(42); // 42
let str = identity("текст"); // Инференс типа: "текст"

// Generic класс
class Box<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
    getValue(): T {
        return this.value;
    }
}
let numberBox = new Box<number>(10);
let stringBox = new Box<string>("коробка");

// Generic интерфейс
interface Pair<K, V> {
    key: K;
    value: V;
}
let pair: Pair<string, number> = { key: "возраст", value: 25 };

Расширенное: Constraints: <T extends number>. Defaults: <T = string>. Практика: Для коллекций, как Array<T>.

11.6. Модули и продвинутые фичи

Модули как в ES6, но с типами. Enums, namespaces.

// Enum: перечисления
enum Color {
    Red = "Красный",
    Green = "Зеленый",
    Blue = "Синий"
}
let c: Color = Color.Red; // "Красный"

// Namespaces: группировка
namespace Utils {
    export function log(message: string): void {
        console.log(message);
    }
}
Utils.log("Сообщение");

// Декларации: для внешних библиотек (d.ts файлы)
declare module "some-lib" {
    export function func(): void;
}

Расширенное: Utility types: Partial<T>, Readonly<T>, Pick<T, K>. Лучшая практика: Используйте @types для библиотек (npm install @types/node).

Переход от JS к TS: Начните с any, постепенно добавляйте типы. TS улучшает JS-код, но требует компиляции.

Итог: Путь изучения JavaScript и TypeScript

Первый проект после основ:

// Простой TODO список
class TodoApp {
    constructor() {
        this.todos = [];
        this.init();
    }
    
    init() {
        this.form = document.getElementById('todo-form');
        this.input = document.getElementById('todo-input');
        this.list = document.getElementById('todo-list');
        
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            this.addTodo(this.input.value);
            this.input.value = '';
        });
    }
    
    addTodo(text) {
        const todo = {
            id: Date.now(),
            text: text,
            completed: false
        };
        
        this.todos.push(todo);
        this.render();
    }
    
    render() {
        this.list.innerHTML = '';
        
        this.todos.forEach(todo => {
            const li = document.createElement('li');
            li.innerHTML = `
                <input type="checkbox" ${todo.completed ? 'checked' : ''}>
                <span>${todo.text}</span>
                <button>Удалить</button>
            `;
            
            this.list.appendChild(li);
        });
    }
}

new TodoApp();

Помните: JavaScript учится на практике. Пишите код каждый день, делайте проекты, ошибайтесь и исправляйте ошибки. Удачи! 🚀