var имеет область видимости функции, поднимается (hoisting) со значением undefined и допускает повторное объявление. let и const блочные и до строки объявления находятся во временной мёртвой зоне, поэтому раннее чтение бросает ReferenceError; const к тому же нельзя переприсвоить.
Блочная область: let внутри if или for остаётся в этом блоке; var протекает на всю функцию.
Hoisting: поднимаются все три объявления, но только у var есть пригодное значение (undefined) до его строки.
**const не означает неизменяемость**: const user = {} всё равно позволяет user.name = 'Ada'; запрещено только user = ....
Циклы: let создаёт новую привязку на каждой итерации, поэтому замыкания в цикле for (let ...) видят каждое своё значение.
На практике: const по умолчанию, let когда нужно переприсваивание, var в новом коде никогда.
ReactJunior
JavaScript для React
Замыкания, области видимости, this, прототипы, сравнение, event loop, промисы и async/await, модули, иммутабельность, методы массивов
Закрыто вопросов
0 / 21
- 0Повторить
- 15Часто спрашивают
Вопросы
=== сравнивает без приведения типов: у значений должны совпадать и тип, и значение. == сначала приводит операнды к общему типу, поэтому '1' == 1 и 0 == '' дают true. Используйте ===; единственное частое исключение x == null, которое совпадает и с null, и с undefined.
Объекты оба оператора сравнивают по ссылке: {} === {} даёт false, и два массива с одинаковым содержимым не равны.
**NaN**: NaN === NaN даёт false; проверяйте через Number.isNaN(x).
**Object.is** работает как ===, но Object.is(NaN, NaN) даёт true, а Object.is(0, -0) даёт false; именно им React сравнивает стейт и зависимости.
**Сюрпризы ==**: [] == false и '0' == false дают true, а null == 0 даёт false.
Примитивы (string, number, bigint, boolean, undefined, null, symbol) представляют собой неизменяемые значения и копируются при присваивании. Объекты, массивы и функции ссылочные: переменная хранит ссылку, поэтому копия переменной указывает на тот же объект.
Копия или общий объект: let b = a с числом копирует значение; с объектом оба имени указывают на один объект, и изменение через одно видно через другое.
Аргументы функции всегда передаются по значению, но у объекта это значение и есть ссылка, поэтому функция может изменить объект вызывающего кода.
Сравнение: примитивы сравниваются по значению, объекты по идентичности.
Почему это важно для React: React сравнивает стейт и пропсы через Object.is; у мутированного объекта ссылка та же, поэтому React не видит изменения и может пропустить ререндер.
**typeof null возвращает 'object'**: историческая ошибка, оставленная ради совместимости.
undefined означает, что значение никогда не присваивалось: неинициализированная переменная, отсутствующее свойство или аргумент, функция без return. null присваивают намеренно, чтобы сказать «здесь ничего нет». null == undefined равно true, а null === undefined равно false.
Кто что возвращает: undefined даёт сам язык (obj.missing, [].find(...) без совпадения); null намеренно возвращают API и ваш код (document.getElementById без совпадения, useState<User | null>(null)).
Значения по умолчанию: параметры по умолчанию и значения по умолчанию при деструктуризации подставляются только вместо undefined, поэтому greet(null) сохраняет null; ?? и ?. считают отсутствующими оба.
JSON: JSON.stringify({ a: undefined, b: null }) даёт {"b":null}, поэтому чтобы очистить поле через API, отправляют null.
Проверка: value == null ловит оба значения; typeof null равно 'object', так что через typeof null не распознать.
Замыкание (closure) представляет собой функцию вместе с переменными той области видимости, где она была создана. Функция сохраняет доступ к этим переменным даже после того, как внешняя функция завершилась.
Приватное состояние: фабрика счётчиков прячет count, и до него можно добраться только через возвращённые функции.
Колбэки: любой обработчик события и колбэк setTimeout замыкается на переменные вокруг себя.
В React: обработчики и эффекты каждого рендера замыкаются на пропсы и стейт этого рендера, поэтому обработчик видит значения того рендера, который его создал.
Устаревшее замыкание (stale closure): интервал, запущенный один раз в эффекте, продолжает читать count первого рендера; это исправляют updater-формой setCount(c => c + 1) или правильными зависимостями.
Память: замыкание удерживает захваченные переменные, поэтому долгоживущий слушатель может держать в памяти большие объекты.
Он трижды выведет 3. var создаёт одну переменную i на всю функцию, все три колбэка замыкаются на неё, а к моменту срабатывания таймеров цикл уже закончился и оставил в i значение 3.
Таймеры срабатывают позже: setTimeout(..., 0) ставит задачу в очередь, и она выполнится только после текущего скрипта.
**Исправление через let**: for (let i = 0; ...) создаёт новую привязку на каждой итерации, поэтому каждый колбэк видит своё значение, и вывод будет 0, 1, 2.
Исправления до ES2015: обернуть тело в IIFE или передать значение аргументом: setTimeout(console.log, 0, i).
Та же причина в React: колбэк читает переменную, которая изменилась после того, как колбэк был создан.
Код выведет hi, undefined и hello, затем бросит TypeError: wave is not a function. Объявление функции поднимается (hoisting) вместе с телом; var hello = function... поднимает только переменную со значением undefined до своей строки, и стрелочная функция в var ведёт себя так же.
Что движок делает сначала: перед выполнением области видимости он создаёт в ней все привязки. Объявления функций сразу получают функцию, привязки var начинаются с undefined, а let, const и class остаются неинициализированными (временная мёртвая зона).
**typeof hello равно 'undefined'**: переменная существует, но функции в ней пока нет, поэтому вызов hello() в этом месте бросил бы такой же TypeError, как wave().
**С let или const** ранний вызов бросает ReferenceError: Cannot access 'wave' before initialization; так же ведёт себя new Foo() выше class Foo.
Hoisting работает в пределах функции: var label в любом месте функции перекрывает внешний label с первой её строки, поэтому console.log(label) выше объявления выводит undefined.
Мёртвая зона зависит от времени, а не от позиции: функция может использовать const, объявленный ниже, если её вызывают после выполнения этой строки; поэтому компоненты могут вызывать хелперы, объявленные ниже в модуле.
Деструктуризация распаковывает объект или массив в переменные; spread ... разворачивает массив или объект в другой; rest ... собирает остаток; ?. даёт undefined вместо ошибки, если слева null или undefined; ?? подставляет запасное значение только для null и undefined.
**?? против ||**: count || 10 заменяет ещё и 0 и ''; count ?? 10 их сохраняет, поэтому для чисел и строк по умолчанию берите ??.
Значения по умолчанию при деструктуризации срабатывают только для undefined: const { size = 'md' } = props.
Rest в пропсах: function Button({ variant, ...rest }) забирает нужное и пробрасывает остальное в <button {...rest}>.
**?. с вызовами и индексами**: onClose?.(), list?.[0].
Spread поверхностный: вложенные объекты остаются общими.
map, filter, find, some, every, reduce и slice возвращают новые значения и не трогают массив, что и нужно стейту в React. push, pop, splice, sort, reverse и fill меняют массив на месте; в ES2023 появились копирующие toSorted, toReversed, toSpliced, with.
**map: рендер списков и преобразование элементов; результат всегда той же длины.
filter**: удаление элемента из стейта: items.filter(i => i.id !== id).
**find / findIndex**: первое совпадение или undefined / -1; findLast ищет с конца.
**reduce: сворачивает массив в одно значение, например сумму или объект-справочник; обычный цикл часто понятнее.
Сортировка стейта**: [...items].sort(cmp) или items.toSorted(cmp); items.sort() мутирует массив из стейта, и React может не сделать ререндер.
**includes против indexOf**: includes находит и NaN.
Поверхностная копия создаёт новый объект верхнего уровня, но вложенные объекты делит с оригиналом; глубокая копия дублирует все уровни. Spread ({...obj}, [...arr]) и Object.assign делают поверхностную копию; structuredClone(obj) делает глубокую.
Ловушка поверхностной копии: после const b = { ...a } запись b.address.city = 'Paris' меняет и a.address.city.
**structuredClone** (все современные браузеры, Node 17+): справляется с вложенными объектами, массивами, Map, Set, Date и циклами; бросает ошибку на функциях и DOM-узлах и теряет прототипы классов.
**JSON.parse(JSON.stringify(x))**: старый приём; он теряет undefined и функции, превращает Date в строку, а Map и Set в пустые объекты.
В React глубокая копия нужна редко: копируют только изменяемый путь ({ ...user, address: { ...user.address, city } }), поэтому неизменённые ветки сохраняют идентичность, и мемоизированные дети пропускают работу.
Immer (встроен в Redux Toolkit) позволяет писать мутирующий код и сам делает такую копию пути.
У обычной функции this зависит от вызова: obj.method() даёт obj, call, apply и bind задают его явно, new даёт новый объект, а простой вызов в strict mode даёт undefined. У стрелочной функции нет своего this: она берёт его из области видимости, где была объявлена.
**Потеря this**: const f = obj.method; f() или передача obj.method колбэком отрывает метод от объекта, и this становится undefined.
Исправления: obj.method.bind(obj), обёртка-стрелка () => obj.method() или стрелочное поле класса handle = () => {...}.
Стрелки плохо подходят для методов: стрелка в литерале объекта берёт this из внешней области, а не из объекта; кроме того, стрелки нельзя вызывать с new, и у них нет arguments.
История React: в классовых компонентах писали this.handleClick = this.handleClick.bind(this); у функциональных компонентов this нет вовсе.
Модули и классы работают в strict mode, поэтому старое правило «при простом вызове this равен window» действует только в нестрогих скриптах.
У каждого объекта есть скрытая ссылка на другой объект, его прототип. Если свойства нет у самого объекта, JavaScript ищет его по этой цепочке прототипов, пока не дойдёт до null. class представляет собой более удобный синтаксис над тем же механизмом: его методы лежат в ClassName.prototype.
Поиск, а не копирование: все экземпляры делят один метод в прототипе, и метод, добавленный туда, сразу виден уже созданным экземплярам.
**Object.getPrototypeOf(obj)** читает ссылку (__proto__ остался как устаревший аксессор); Object.create(proto) создаёт объект с заданным прототипом.
**extends** связывает Child.prototype с Parent.prototype; super вызывает конструктор или метод родителя.
Собственное или унаследованное: Object.hasOwn(obj, 'x') проверяет сам объект; 'x' in obj проверяет всю цепочку.
**Что добавляет class**: приватные поля #count, static-члены и конструктор, который бросает ошибку при вызове без new.
Именованных экспортов может быть сколько угодно, их импортируют по точному имени в фигурных скобках; экспорт по умолчанию один, и его импортируют под любым именем. ES-модули статические и разрешаются до запуска кода; CommonJS (require) представляет собой старый формат Node с синхронной загрузкой.
Именованные экспорты сохраняют одинаковые имена по всей кодовой базе и помогают редактору с автоимпортом и переименованием; многие команды используют их везде, кроме мест, где инструмент требует default (React.lazy, страницы Next.js).
Статическая структура даёт tree shaking: сборщик видит, какие экспорты используются, и выбрасывает остальные.
**Динамический import()** возвращает промис и создаёт отдельный чанк; на нём построен React.lazy(() => import('./Chart')).
Живые привязки (live bindings): импортированный let отражает последующие переприсваивания в экспортирующем модуле; значение, полученное через require, остаётся снимком.
Модули строгие и изолированные: переменные верхнего уровня не становятся глобальными, и разрешён await на верхнем уровне.
Promise представляет собой объект для значения, которое появится позже: он начинается в pending и завершается один раз, fulfilled со значением или rejected с ошибкой. async/await служит синтаксисом над промисами: async-функция всегда возвращает промис, а await ждёт его завершения.
Цепочки: .then возвращает новый промис, поэтому шаги идут по порядку; .catch ловит отказ любого предыдущего шага; .finally выполняется в любом случае.
**await не блокирует поток: приостанавливается только функция, остальная программа продолжает работать, а продолжение выполняется как микрозадача.
Ошибки**: отклонённый промис под await бросает исключение, поэтому оборачивайте его в try/catch; без него отклоняется промис самой async-функции.
Последовательно или параллельно: два await подряд выполняются друг за другом; чтобы запустить вместе, начните оба и сделайте await Promise.all([...]).
**fetch резолвится и при HTTP-ошибках**: проверяйте response.ok; отклоняется он только при сетевом сбое.
Все четыре принимают несколько промисов и возвращают один. all даёт все результаты или отклоняется на первой ошибке; allSettled ждёт все и сообщает исход каждого; race повторяет первый завершившийся промис; any даёт первый успех и отклоняется, только если упали все.
**Promise.all: независимые запросы, которые нужны все; одна ошибка отклоняет весь результат, хотя остальные запросы продолжают выполняться.
Promise.allSettled**: дашборд, где упавший виджет не должен прятать остальные; каждый элемент имеет вид { status: 'fulfilled', value } или { status: 'rejected', reason }.
**Promise.race**: таймаут вокруг запроса; проигравший промис не отменяется, поэтому чтобы действительно остановить fetch, нужен AbortController.
**Promise.any**: самое быстрое из нескольких зеркал; если упали все, он отклоняется с AggregateError.
Ни один из них не запускает работу: работа начинается при создании промисов, а эти методы только ждут.
Оборачивайте await в try/catch там, где с ошибкой можно что-то сделать, а в остальных местах пропускайте её выше: отказ поднимается через каждую функцию, которая ждёт промис, как брошенное исключение. Необработанный отказ вызывает unhandledrejection в браузере и останавливает процесс Node.
**Ловятся только промисы под await**: try { save(); } без await или return save() внутри try выходят из блока раньше, чем промис отклонится, и catch не срабатывает. Пишите await save() или return await save().
**Вызовам «запустил и забыл» нужен свой .catch: у аналитики, предзагрузки или async-обработчика клика нет вызывающего кода, который получил бы ошибку, и error boundary её тоже не увидит.
Промисы, запущенные заранее и ожидаемые по очереди**: в const a = loadA(); const b = loadB(); await a; await b; быстрый отказ b остаётся без обработчика, пока a ещё ждёт, и считается необработанным. Promise.all вешает обработчики на все промисы сразу.
Ловите там, где можете действовать: показать сообщение, повторить запрос или подставить запасной вариант; в остальных местах пробрасывайте дальше, обернув в { cause: error }, если добавляете контекст. Пустой catch {} прячет баги.
Ошибкой может быть что угодно: строгий TypeScript типизирует её как unknown, поэтому проверяйте error instanceof Error; прерванный fetch отклоняется с AbortError, который обычно игнорируют.
**Глобальный обработчик unhandledrejection** нужен для отчётов, как в системах мониторинга ошибок, а не для обработки. Начиная с Node 15 необработанный отказ завершает процесс с кодом 1.
JavaScript выполняет код по одному фрагменту на стеке вызовов. Когда стек пуст, event loop выполняет все микрозадачи (колбэки промисов, продолжения после await, queueMicrotask), затем даёт браузеру отрисовать кадр, затем берёт следующую макрозадачу (таймер, событие, сообщение).
Асинхронные API работают вне стека: setTimeout, fetch и события обрабатывает браузер и ставит колбэк в очередь, когда всё готово.
Сначала микрозадачи: очередь микрозадач опустошается полностью после каждой задачи, поэтому .then всегда выполнится раньше setTimeout(..., 0), поставленного в тот же момент.
Голодание (starvation): микрозадачи, которые всё время добавляют новые микрозадачи, блокируют отрисовку и ввод так же, как долгий цикл.
Долгие задачи замораживают страницу: синхронный цикл на 200 мс задерживает клики и кадры; разбейте работу, вынесите её в Web Worker или в React пометьте обновление как transition.
**Колбэки requestAnimationFrame** выполняются прямо перед следующей отрисовкой.
Он выведет 1, 4, 6, 3, 5, 2. Сначала выполняется синхронный код, в том числе начало async-функции до await; затем микрозадачи в порядке постановки в очередь (.then, потом продолжение после await); колбэк setTimeout относится к макрозадачам и выполняется последним.
**1 и 6: обычные синхронные выводы.
4**: async-функция выполняется синхронно до первого await.
**3 раньше 5**: колбэк .then попал в очередь раньше продолжения после await.
**2 последним**: даже таймер на 0 мс ждёт, пока опустеют стек и вся очередь микрозадач.
forEach игнорирует промисы, которые возвращает его колбэк, поэтому saveAll их не ждёт: сразу выводит done, а неудачное сохранение превращается в необработанный отказ (unhandled rejection) вместо ошибки, которую мог бы поймать вызывающий код.
**forEach синхронный**: он вызывает каждый async-колбэк, каждый возвращает промис в ожидании, и forEach их выбрасывает.
Параллельное исправление: await Promise.all(users.map((user) => saveUser(user))) запускает все сохранения и ждёт их все.
Последовательное исправление: for (const user of users) await saveUser(user);, когда важен порядок или сервер не выдерживает много запросов сразу.
Ошибки: при любом исправлении упавшее сохранение отклоняет saveAll, и try/catch вызывающего кода его увидит.
Та же ловушка: map с async-колбэком возвращает массив промисов, которые всё равно нужно дождаться через await.
debounce откладывает вызов, пока серия событий не затихнет на N мс, и выполняется только последний вызов, например поиск, когда пользователь перестал печатать. throttle вызывает функцию не чаще раза в N мс во время серии; он подходит для скролла и ресайза, где обновления нужны постоянно.
Как работает debounce: каждый вызов сбрасывает ожидающий таймер и запускает новый; id таймера хранится в замыкании.
**Сохраняйте последние аргументы и this: передавайте их в отложенный вызов.
В React**: создавайте debounced-функцию один раз (useMemo или ref), а не на каждом рендере, иначе у каждого рендера свой таймер и debounce не работает; отменяйте её при размонтировании.
Часто нужно значение, а не вызов: хук useDebouncedValue или useDeferredValue, чтобы ввод оставался отзывчивым без фиксированной задержки.
Библиотеки: lodash.debounce добавляет leading, trailing, maxWait и cancel.
Утечка означает память, которая остаётся достижимой, когда уже не нужна, и сборщик мусора не может её освободить. Типичные причины в SPA: слушатели, таймеры и подписки, пережившие свой компонент, кэши, которые только растут, и оторванные DOM-узлы, на которые ссылается долгоживущий объект.
Как решает GC: движок помечает всё, что достижимо от корней (глобальные объекты, стек), и удаляет остальное; цикл ссылок сам по себе утечкой не является.
Забытая очистка: window.addEventListener, setInterval, WebSocket или подписка на стор, добавленные в useEffect без функции очистки, держат в памяти замыкание уже размонтированного компонента.
Неограниченные кэши: Map на уровне модуля с ключами по id растёт бесконечно; ограничьте его размер, используйте LRU или WeakMap с ключами-объектами.
Оторванный DOM (detached): ref или переменная, которая хранит удалённый узел, удерживает и узел, и всё его поддерево.
Как найти: в панели Memory в Chrome DevTools сделайте heap snapshot, несколько раз повторите действие (открыть и закрыть страницу), сделайте ещё один снимок и сравните; ищите растущее число своих объектов и элементы с пометкой «Detached». Performance monitor показывает JS heap и число DOM-узлов во времени.
Не любой рост означает утечку: TanStack Query и роутеры намеренно держат кэши и вычищают их позже (gcTime).