Привет, Хабр! После прошлого поста делюсь новым разбором задач с собеседований. Сегодня разберём три ключевые темы: поднятие (hoisting), работу с объектами и реализацию связного списка. Погнали!Для кого эти задачи и что проверяют?
Эти вопросы часто встречаются на собеседованиях для Middle JavaScript-разработчиков. Через них проверяют:
➕ Понимание «подводных камней» языка (hoisting, TDZ, ссылочные типы);
➕ Умение работать с низкоуровневыми структурами данных;
➕ Способность предвидеть edge-кейсы.
▍ Часть 1: Области видимости и hoisting: почему это до сих пор спрашивают?
Каждый день разработчик объявляет множество переменных, и кажется, что здесь нет ничего сложного. Но JavaScript хранит в себе множество нюансов, которые превращают простое объявление let/const или var в ловушку для разработчиков.
Что проверяют работодатели:
Понимание временной мёртвой зоны (TDZ): Знаете ли вы, почему обращение к переменной до её объявления иногда вызывает ошибку, а иногда — нет?
Чувство области видимости: Можете ли вы предсказать, где переменная «живёт» — внутри блока, функции или глобально?
Осознанный выбор инструментов: Зачем в 2025 году спрашивать про устаревший var? Ответ прост: чтобы проверить, понимаете ли вы эволюцию языка и причины появления let/const.
Главный подвох задач на hoisting — иллюзия доступности. Переменные var создают «призраков»: они существуют в коде до объявления, но их значение — undefined. С let/const всё строже: попытка использовать их до инициализации ломает код, что часто становится сюрпризом для тех, кто привык к другим языкам.
Практическая ценность:
Эти нюансы критичны не только для собеседований. Например, ошибки TDZ возникают при работе с асинхронными операциями, когда переменная объявляется позже вызова. Понимание областей видимости помогает избежать багов в больших проектах, где одни и те же имена переменных могут использоваться в разных модулях.
Функция 1: TDZ в действии
function doSome() {
if (true) {
console.log(x, y); // 🚨 Ошибка!
}
let x = 2;
var y = 3;
console.log(x, y); // Этот код не выполнится
}
Что происходит:
let x:
Объявлена после блока if, но попытка чтения происходит до объявления.
Из-за TDZ обращение к x до инициализации вызовет ошибку: ReferenceError: Cannot access 'x' before initialization.
var y:
Поднимается в начало функции со значением undefined, поэтому доступна (но выведет undefined без ошибки).
Итог: Код упадёт на первом console.log, так как x находится в TDZ.
Функция 2: Блоки и повторные объявления
function doSome() {
if (true) {
let x = 2;
var y = 3;
console.log(x, y); // ✅ 2 3
}
console.log(x, y); // 🚨 Ошибка!
let x = 2; // Переобъявление x
var y = 3; // Перезапись y
}
Разбор:
Внутри if:
let x видна только внутри блока.
var y поднимается в начало функции, присваивается 3.
Вывод: 2 3.
Вне if:
console.log(x): x из блока if уже не существует. Попытка обратиться к новой x, объявленной через let ниже, снова приводит к TDZ → ReferenceError.
console.log(y): y уже равен 3 благодаря hoisting.
Итог: Первый вывод корректен, второй вызовет ошибку из-за TDZ для x.
▍ Часть 2: Объекты в JavaScript: почему копирование — это не всегда просто?
Работа с объектами кажется интуитивно понятной, пока вы не столкнётесь с их «ссылочной» природой. Задачи на объекты проверяют не только знание синтаксиса, но и понимание того, как данные хранятся в памяти.
Что проверяют работодатели:
Работу с ссылочными типами: Понимаете ли вы, что присваивание объекта создаёт ссылку, а не копию?
Особенности оператора ... (spread): Знаете ли вы, что он делает поверхностное копирование, оставляя вложенные объекты связанными?
Специфику ключей: Почему использование объектов как ключей часто приводит к неочевидным результатам?
Главный подвох задач на объекты — иллюзия независимости. Например, два объекта {a: 1}
и {a: 1}
не равны друг другу, а изменение свойства в «скопированном» объекте может затронуть исходный. Это ловушка для тех, кто не осознаёт разницу между поверхностным и глубоким копированием.
Практическая ценность:
Ошибки с ссылками часто возникают при работе с состоянием в React/Vue, где неверное копирование приводит к лишним ререндерам или багам.
Понимание ключей-объектов помогает при работе с Map и WeakMap, где идентичность сохраняется.
Совет: Всегда уточняйте, требуется ли глубокая копия. Используйте structuredClone() или иммутабельные подходы, чтобы избежать неожиданных мутаций.
Даны два объекта:
const a = {
set: { foo: { bar: 10 } }, // 🧐 Ключи set и delete — разрешены?
delete: { foo: { bar: 20 } },
};
const b = {
set: { foo: { bar: 10 } },
delete: { foo: { bar: 20 } },
};
Вопрос: Можно ли использовать set и delete как ключи?
Ответ: Да! Эти слова зарезервированы для операторов, но в объектах их можно использовать как ключи.
Эксперименты с объектами:
1. Слияние через spread:
const sum = { ...a, ...b };
Поверхностное копирование: sum.set и sum.delete берутся из b (последний в spread имеет приоритет).
Вложенные объекты остаются ссылками на оригиналы из b.
2. Изменение вложенного свойства:
sum.set.foo.bar = 11;
Изменит b.set.foo.bar и sum.set.foo.bar, так как они ссылаются на один объект.
3. Полная перезапись:
sum.set = { foo: { bar: 13 } };
sum.set больше не связан с b.set, следовательно меняется только sum.set, b.set остается прежним.
4. Ключи-объекты:
sum[a] = 14; // sum["[object Object]"] = 14
sum[b] = 15; // sum["[object Object]"] = 15 (перезапись)
Ключи-объекты преобразуются в строку [object Object], поэтому значения перезаписываются.
▍ Часть 3: Связные списки: зачем их реализовывать вручную?
Связные списки — базовая структура данных, которая редко используется напрямую в веб-разработке. Однако задачи на их реализацию раскрывают навык работы с низкоуровневыми концепциями.
Что проверяют работодатели:
Умение оперировать ссылками: Можете ли вы управлять связями между узлами без ошибок?
Обработку edge-кейсов: Помните ли вы про пустые списки, обновление head и tail?
Понимание Big O: Сможете ли вы объяснить, когда список эффективнее массива
Главный подвох задач на списки — незаметные ошибки в логике связей. Например, если забыть обновить tail при добавлении элемента, список превратится в «битый» набор узлов. Это проверяет внимательность к деталям.
Практическая ценность:
Связные списки лежат в основе LRU-кэшей, очередей задач и систем типа «Отменить/Повторить».
Понимание их устройства помогает оптимизировать вложенные структуры данных, где вставка/удаление должны быть быстрыми.
Совет: Даже если не пишете списки ежедневно, разберитесь в их устройстве. Это основа для изучения более сложных структур — деревьев, графов и хэш-таблиц.
Задача: Создать класс LinkedList с методами:
push(value) — добавление в конец.
toArray() — преобразование в массив.
Решение:
class LinkedList {
constructor() {
this.head = null; // Первый элемент
this.tail = null; // Последний элемент
}
push(value) {
const newNode = { value, next: null };
if (!this.head || !this.tail) { // Если список пуст
this.head = newNode;
this.tail = newNode;
return this; // Для цепочки вызовов
}
// Добавление в конец
this.tail.next = newNode;
this.tail = newNode;
return this; // Для цепочки вызовов
}
toArray() {
const arr = [];
let current = this.head;
while (current) { // Идём от head к tail
arr.push(current.value);
current = current.next;
}
return arr;
}
}
Пример использования:
const list = new LinkedList();
list.push(1).push(2).push(3);
console.log(list.toArray()); // [1, 2, 3]
Как это работает:
Каждый узел содержит value и ссылку next.
push обновляет tail, toArray проходит от head до tail, собирая значения.
P.S. Если хотите глубже погрузиться в тему, изучите двусвязные списки, я привел лишь базовый пример, который попался мне. Удачи на собеседованиях!