Привет, Хабр! После прошлого поста делюсь новым разбором задач с собеседований. Сегодня разберём три ключевые темы: поднятие (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. Если хотите глубже погрузиться в тему, изучите двусвязные списки, я привел лишь базовый пример, который попался мне. Удачи на собеседованиях!
ссылка на оригинал статьи https://habr.com/ru/articles/912910/
Добавить комментарий