Движок и инструменты
Движок JS (V8, JIT, скрытые классы), транспиляция (Babel), сборка и tree-shaking, модульные системы (CommonJS против ESM), среды выполнения (Node/Deno) и полифилы.
12 вопросов
JuniorТеорияОчень частоВ чём разница между CommonJS и ES-модулями?
В чём разница между CommonJS и ES-модулями?
CommonJS (изначальная система Node) использует require() для импорта и module.exports для экспорта, загружая модули синхронно во время выполнения. ES-модули (стандарт) используют import/export, разбираются статически до выполнения и поддерживают асинхронную загрузку. Привязки ESM — живые доступные только для чтения представления, а require в CommonJS возвращает копию-снимок экспортированного значения.
Типичные ошибки
- ✗Думать, что
import/exportиrequire/module.exportsвзаимозаменяемы слово-в-слово - ✗Считать, что
requireв CommonJS возвращает живую привязку, как ESM, а не снимок значения - ✗Полагать, что ES-модули загружаются синхронно, как
requireв CommonJS
Уточняющие вопросы
- →Почему ES-модули поддаются tree-shaking, а типичный модуль CommonJS — нет?
- →Как природа живых привязок экспортов ESM меняет поведение при циклических импортах?
MiddleТеорияЧастоЧто делает сборщик и как связаны с ним tree-shaking, минификация и разделение кода?
Что делает сборщик и как связаны с ним tree-shaking, минификация и разделение кода?
Сборщик (например, Webpack, Rollup, esbuild) проходит граф модулей от точки входа и объединяет множество модулей в меньшее число файлов. Tree-shaking выбрасывает неиспользуемые экспорты — ему нужны статические ES-модули, чтобы знать, что достижимо. Минификация уменьшает вывод, убирая пробелы и переименовывая локальные переменные. Разделение кода — обратное объединению: оно режет сборку на части, загружаемые по требованию.
Типичные ошибки
- ✗Ожидать, что tree-shaking сработает на динамических модулях CommonJS, а не на статических ES-модулях
- ✗Путать минификацию (сжимает текст) с tree-shaking (убирает неиспользуемые экспорты)
- ✗Думать, что разделение кода всегда уменьшает суммарный размер, а не откладывает часть на загрузку по требованию
Уточняющие вопросы
- →Почему
require()с вычисляемым путём полностью ломает tree-shaking? - →Как разделение кода ускоряет первую загрузку, даже если суммарный объём байтов растёт?
MiddleТеорияЧастоЧто делает транспилятор Babel и чем транспиляция отличается от полифилов?
Что делает транспилятор Babel и чем транспиляция отличается от полифилов?
Babel — транспилятор: он переписывает современный синтаксис JavaScript (стрелочные функции, class, опциональную цепочку) в эквивалентный старый синтаксис, понятный целевым движкам, по настроенному списку целей (например, browserslist). Транспиляция преобразует синтаксис; полифилы добавляют отсутствующие API времени выполнения вроде Promise. Babel занимается синтаксисом и подключает полифилы (через core-js) лишь для отсутствующих у целей API.
Типичные ошибки
- ✗Смешивать транспиляцию (переписывает синтаксис) с полифилами (добавляют отсутствующие API)
- ✗Думать, что Babel выдаёт машинный код, а не JavaScript со старым синтаксисом
- ✗Полагать, что Babel транспилирует под каждый браузер, а не только под настроенные цели
Уточняющие вопросы
- →Почему Babel всё же нужен
core-js, чтобыArray.prototype.flatработал на старых движках? - →Как задание более высокой цели в browserslist уменьшает объём транспилированного вывода?
SeniorТеорияЧастоОпишите современный конвейер сборки JS от исходника до поставляемой сборки, включая место source map.
Опишите современный конвейер сборки JS от исходника до поставляемой сборки, включая место source map.
Исходник сначала транспилируется (например, Babel), понижая современный синтаксис до целей. Затем сборщик обходит статический граф ESM, объединяя модули и вырезая неиспользуемые экспорты через tree-shaking. Минификация сжимает результат; полифилы внедряются для отсутствующих у целей API (часто через core-js, в идеале только нужные). Каждый этап выдаёт source map, сцепляемые так, что итоговая карта указывает минифицированные позиции обратно на исходник для отладки.
Типичные ошибки
- ✗Путать порядок этапов — например, минифицировать до транспиляции и сборки
- ✗Поставлять всю библиотеку полифилов вместо лишь отсутствующих у целей API
- ✗Забывать, что source map нужно сцеплять между этапами, чтобы указывать на настоящий исходник
Уточняющие вопросы
- →Почему транспиляция обычно должна идти до того, как tree-shaking сможет анализировать граф модулей?
- →Как сцепленные source map переживают несколько преобразований, сохраняя точность трассировки стека?
JuniorТеорияИногдаЧто такое движок JavaScript и каковы основные этапы выполнения им кода?
Что такое движок JavaScript и каковы основные этапы выполнения им кода?
Движок JS (например, V8 в Chrome и Node) — это программа, исполняющая JavaScript. Он разбирает исходник в AST, компилирует в байт-код и сразу начинает интерпретировать; затем JIT-компилятор во время выполнения перекомпилирует горячий код в оптимизированный машинный. Само выполнение JavaScript однопоточно — один стек вызовов исполняет одну задачу за раз.
Типичные ошибки
- ✗Называть JS чисто интерпретируемым, игнорируя, что современные движки JIT-компилируют горячий код в машинный
- ✗Думать, что движок исполняет JavaScript на нескольких потоках параллельно, а не на одном стеке вызовов
- ✗Путать движок со средой выполнения, которая даёт API вроде таймеров, DOM или файловой системы
Уточняющие вопросы
- →Какие условия заставляют JIT-компилятор решить, что функция достаточно горячая для оптимизации?
- →Если JavaScript однопоточный, как среда выполнения всё же обрабатывает асинхронный ввод-вывод?
JuniorТеорияИногдаЧто такое полифил, чем он отличается от shim и как сюда вписывается определение возможностей?
Что такое полифил, чем он отличается от shim и как сюда вписывается определение возможностей?
Полифил — это код, реализующий отсутствующий встроенный API (например, Promise, Array.prototype.includes), чтобы старые среды вели себя так, будто он встроенный. Shim — более широкий термин: любой код, перехватывающий или патчащий API ради единообразного поведения, не обязательно отсутствующего стандартного. Определение возможностей (if (!window.Promise)) во время выполнения решает, грузить ли полифил, вместо распознавания браузера.
Типичные ошибки
- ✗Путать полифил (добавляет отсутствующий API) с транспиляцией (переписывает неподдерживаемый синтаксис)
- ✗Определять возможности по строке user-agent вместо прямой проверки самого API
- ✗Считать shim и полифил точными синонимами, а не shim более широкой категорией
Уточняющие вопросы
- →Почему полифил может добавить
Array.prototype.includes, но не может добавить синтаксисasync/await? - →Как определение возможностей избегает хрупкости распознавания по user-agent?
JuniorТеорияИногдаЧем различаются среды выполнения браузера, Node.js и Deno по предоставляемым API?
Чем различаются среды выполнения браузера, Node.js и Deno по предоставляемым API?
Все три встраивают движок JS, но дают разные хост-API. Браузеры добавляют DOM, fetch и localStorage, но без файловой системы. Node.js добавляет файловую систему, сеть и process через модули CommonJS (с поддержкой ESM). Deno (среда от создателя Node) по умолчанию использует веб-стандартные API и ESM, нативно поддерживает TypeScript и изолирует доступ к файлам и сети за явными флагами разрешений.
Типичные ошибки
- ✗Полагать, что DOM есть в Node.js или что файловая система есть в браузере
- ✗Думать, что общий движок означает идентичность всех хост-API в средах выполнения
- ✗Считать, что Deno включает доступ к файловой системе по умолчанию, а не за флагами разрешений
Уточняющие вопросы
- →Почему код с
document.querySelectorбросаетReferenceErrorв Node.js? - →Какую проблему решает модель разрешений Deno, которую Node.js оставляет разработчику?
MiddleТеорияИногдаКак Node.js обеспечивает совместимость CommonJS и ES-модулей и где здесь динамический import()?
Как Node.js обеспечивает совместимость CommonJS и ES-модулей и где здесь динамический import()?
Node определяет тип файла по расширению .mjs/.cjs или полю "type" пакета, затем разрешает спецификаторы через поиск в node_modules и карты exports. ESM может import модуль CommonJS (его module.exports становится экспортом по умолчанию), но CommonJS не может статически require ESM-модуль, ведь ESM грузится асинхронно. Запасной выход — динамический import(), который возвращает Promise и работает из любой системы во время выполнения.
Типичные ошибки
- ✗Пытаться
require()ES-модуль синхронно вместо динамическогоimport() - ✗Думать, что динамический
import()возвращает модуль напрямую, а неPromise - ✗Игнорировать, как поле
"type"пакета и расширение файла определяют разрешение модуля
Уточняющие вопросы
- →Почему
require()ESM-модуля на верхнем уровне не работает из-за асинхронной загрузки ESM? - →Как карта
exportsвpackage.jsonменяет, в какой файл разрешается голый спецификатор?
MiddleТеорияИногдаЧто такое source map и как он помогает отлаживать минифицированный или транспилированный код?
Что такое source map и как он помогает отлаживать минифицированный или транспилированный код?
Source map — это JSON-файл, сопоставляющий позиции в сгенерированном (минифицированном или транспилированном) выводе с исходником — файл, строку и столбец. Браузер грузит его при открытом DevTools (по комментарию //# sourceMappingURL), поэтому точки останова, трассировки стека и ошибки показывают исходный код вместо искажённой сборки. Это лишь подспорье для отладки и не меняет, как работает поставляемый код.
Типичные ошибки
- ✗Думать, что движок исполняет source map вместо сгенерированной сборки
- ✗Считать, что минифицированная сборка не запустится без присутствия source map
- ✗Полагать, что source map меняет поведение во время выполнения, а не лишь помогает отладке
Уточняющие вопросы
- →Почему source map стоит отдавать лишь авторизованным разработчикам, а не всем пользователям?
- →Как браузер узнаёт, откуда взять source map, если комментарий удалён?
MiddleТеорияРедкоКак скрытые классы и инлайн-кэширование V8 ускоряют доступ к свойствам и что вызывает деоптимизацию?
Как скрытые классы и инлайн-кэширование V8 ускоряют доступ к свойствам и что вызывает деоптимизацию?
V8 присваивает каждому объекту скрытый класс (форму), описывающий раскладку свойств, поэтому объекты, созданные одинаково, разделяют одну форму, а свойства лежат по фиксированным смещениям. Инлайн-кэширование затем запоминает увиденную форму в точке вызова, чтобы в следующий раз взять свойство напрямую. Изменение формы объекта — добавление свойств в другом порядке, удаление одного или передача разных форм — сбрасывает кэш и вызывает деоптимизацию к более медленному обобщённому коду.
Типичные ошибки
- ✗Добавлять свойства объектам в разном порядке, заставляя V8 создавать расходящиеся скрытые классы
- ✗Думать, что инлайн-кэширование хранит значение свойства, а не его форму и смещение
- ✗Считать, что оптимизированный код никогда не деоптимизируется после компиляции V8
Уточняющие вопросы
- →Почему
delete obj.propвредит производительности сильнее, чем присваивание свойствуundefined? - →Почему мономорфная точка вызова обгоняет полиморфную в инлайн-кэшировании?
SeniorТеорияРедкоКак сборка мусора, утечки памяти и цикл событий соотносятся с движком и средой выполнения?
Как сборка мусора, утечки памяти и цикл событий соотносятся с движком и средой выполнения?
Движок владеет кучей и запускает трассирующий сборщик мусора, освобождающий объекты, недостижимые от корней; утечка — это непреднамеренная достижимость: висящие замыкания, отсоединённые узлы DOM, растущие глобальные переменные или живые таймеры. Цикл событий же принадлежит среде выполнения (браузеру или Node), а не движку: он подаёт задачи и микрозадачи на единственный стек вызовов движка. Деоптимизация отдельна — её вызывают смены формы или типа, а не сборка мусора.
Типичные ошибки
- ✗Помещать цикл событий внутрь движка, тогда как он принадлежит среде выполнения
- ✗Думать, что трассирующий GC делает утечки невозможными, игнорируя непреднамеренную достижимость
- ✗Приписывать деоптимизацию сборке мусора, а не смене формы или типа
Уточняющие вопросы
- →Почему забытый
setIntervalдержит свои захваченные переменные живыми вопреки трассирующему GC? - →Как микрозадачи и макрозадачи чередуются на стеке вызовов движка через цикл среды выполнения?
SeniorТеорияРедкоПочему tree-shaking требует статических ES-модулей и какую роль играют флаги побочных эффектов?
Почему tree-shaking требует статических ES-модулей и какую роль играют флаги побочных эффектов?
Tree-shaking — это устранение мёртвого кода по графу импортов: сборщик выбрасывает экспорт, лишь доказав, что к нему нет пути. import/export ES-модулей статичны — анализируемы до запуска — поэтому граф известен; require в CommonJS может брать вычисляемый путь и переприсваивать module.exports во время выполнения, ломая анализ. Флаг sideEffects в package.json сообщает сборщику, что модуль чист, поэтому даже импортированные, но неиспользуемые модули можно удалить целиком.
Типичные ошибки
- ✗Считать, что tree-shaking анализирует CommonJS так же точно, как статические ES-модули
- ✗Думать, что флаг
sideEffectsсохраняет модули, а не позволяет удалять чистые - ✗Полагать, что устранению мёртвого кода нужно запускать код, а не статически доказывать достижимость
Уточняющие вопросы
- →Почему пометка модуля с побочными эффектами может случайно сохранить код, который вы хотели вырезать?
- →Как реэкспорт через barrel-файл может сломать tree-shaking, несмотря на использование ESM?