Как поведение изменится при замене var на let, и какие ещё “неочевидные” особенности семантики JavaScript (прототипное наследование, приводение типов, this) могут приводить к багам
Область видимости: var — функциональная/глобальная; let — блочная. Ошибки с доступом к переменным вне блока исчезают/появляются по-другому.Подъём (hoisting) и TDZ: Оба "поднимаются", но var инициализируется undefined, а let — остаётся в "temporal dead zone" до объявления; обращение до объявления даёт ReferenceError.Переобъявление: var позволяет повторно объявлять в одной области; let — нет (SyntaxError/Runtime).Свойство глобального объекта: В обычном (не-module) скрипте var x = 1 создаёт window.x, а let x = 1 — нет.Замыкания в циклах: В for/for..of с let каждая итерация получает свою привязку переменной, с var все итерации делят одну переменную — классическая ошибка с setTimeout.Пример (типичная ошибка):С var: for (var i=0; i<3; i++) setTimeout(()=>console.log(i),0) // выводит 3,3,3С let: for (let i=0; i<3; i++) setTimeout(()=>console.log(i),0) // 0,1,2Рекомендации: по умолчанию const/let, избегать var.
2) Прототипное наследование — неочевидные ловушки
Общие изменяемые свойства прототипа: Если поместить в прототип изменяемое значение (массив/объект), все экземпляры разделяют его — неожиданные мутации.Перезапись прототипа и потеря конструктора: Присвоение MyClass.prototype = {…} может сломать constructor и instanceof.Наследование от примитивов/одноуровневые копии: Object.assign и shallow copy копируют ссылки, не клонируют глубоко.for...in перечисляет свойства в цепочке прототипов — нужно hasOwnProperty для фильтрации.Изменение Object.prototype ломает всё; избегать.Объекты-словарь: обычный объект наследует методы (например hasOwnProperty) — для словарей лучше Object.create(null) или Map.
3) Приведение типов — основные сюрпризы
== делает приведение типа, === — строгое сравнение. Предпочитать ===.Непредсказуемые истинности/ложности: Boolean('0') === true, Boolean([]) === true, Boolean({}) === true, Boolean('') === false.Числовые приведения: '' -> 0, ' \n' -> 0, null -> 0 в числовом контексте, undefined -> NaN.Оператор + — и сложение, и конкатенация: "1"+2 === "12", 1+{} -> "1[object Object]".NaN не равен самому себе: NaN !== NaN — использовать Number.isNaN.typeof null === 'object' — историческое несоответствие.Знаки -0 и 0 ведут себя особо при некоторых операциях; Object.is(-0, 0) различает.parseInt без radix может дать неожиданности (старый поведение с лидирующими 0) — всегда указывайте основание.
4) this — основные источники багов
Привязка зависит от места вызова (call site): Функция вызвана как метод: obj.fn() — this = obj.Просто вызов: fn() — this = undefined (в strict) или global (в non-strict).Конструктор: new Fn() — this новый объект.fn.call(ctx) / fn.apply(ctx) — явная привязка.Стрелочные функции: Не имеют собственной привязки this — используют лексическое this внешнего окружения. Удобно для колбеков, но может привести к ошибкам в методах классов (стрелочные методы в прототипе — нет).Потеря контекста при передаче метода как колбека: const f = obj.method; f(); — this перестаёт быть obj. Частая причина багов в обработчиках событий и промисах.Решения: использовать bind, стрелочные функции, хранить const self = this, или использовать классы с привязкой методов.super и наследование классов: super() должен быть вызван в конструкторе наследника до обращения к this.
5) Другие неочевидные особенности, приводящие к багам
Асинхронность и порядок выполнения: макротаски vs микротаски (setTimeout vs Promise.then) — ошибки с порядком обновления состояния.Геттеры/сеттеры и побочные эффекты при чтении свойства.Прокси (Proxy) могут непредсказуемо перехватывать операции.Плавающие вычисления и потеря точности (числа с плавающей точкой).JSON.stringify/parse — потеря функций и символьных ключей.Иммутабельность: многие встроенные методы мутируют объект (Array.prototype.sort, push) — неожиданное состояние.
Короткие рекомендации для предотвращения:
Использовать let/const вместо var.Всегда === / !==.Явно указывать radix в parseInt.Не расширять Object.prototype.Для словарей — Map или Object.create(null).Привязывать методы (bind) или использовать стрелочные функции осознанно.Тесты и линтер (ESLint) — ловят многие из упомянутых проблем.
Если нужно — могу привести минимальные рабочие примеры кода для каждой проблемы.
Коротко и по делу.
1) Что изменится при замене var на let
Область видимости:var — функциональная/глобальная; let — блочная. Ошибки с доступом к переменным вне блока исчезают/появляются по-другому.Подъём (hoisting) и TDZ:
Оба "поднимаются", но var инициализируется undefined, а let — остаётся в "temporal dead zone" до объявления; обращение до объявления даёт ReferenceError.Переобъявление:
var позволяет повторно объявлять в одной области; let — нет (SyntaxError/Runtime).Свойство глобального объекта:
В обычном (не-module) скрипте var x = 1 создаёт window.x, а let x = 1 — нет.Замыкания в циклах:
В for/for..of с let каждая итерация получает свою привязку переменной, с var все итерации делят одну переменную — классическая ошибка с setTimeout.Пример (типичная ошибка):С var: for (var i=0; i<3; i++) setTimeout(()=>console.log(i),0) // выводит 3,3,3С let: for (let i=0; i<3; i++) setTimeout(()=>console.log(i),0) // 0,1,2Рекомендации: по умолчанию const/let, избегать var.
2) Прототипное наследование — неочевидные ловушки
Общие изменяемые свойства прототипа:Если поместить в прототип изменяемое значение (массив/объект), все экземпляры разделяют его — неожиданные мутации.Перезапись прототипа и потеря конструктора:
Присвоение MyClass.prototype = {…} может сломать constructor и instanceof.Наследование от примитивов/одноуровневые копии:
Object.assign и shallow copy копируют ссылки, не клонируют глубоко.for...in перечисляет свойства в цепочке прототипов — нужно hasOwnProperty для фильтрации.Изменение Object.prototype ломает всё; избегать.Объекты-словарь: обычный объект наследует методы (например hasOwnProperty) — для словарей лучше Object.create(null) или Map.
3) Приведение типов — основные сюрпризы
== делает приведение типа, === — строгое сравнение. Предпочитать ===.Непредсказуемые истинности/ложности:Boolean('0') === true, Boolean([]) === true, Boolean({}) === true, Boolean('') === false.Числовые приведения:
'' -> 0, ' \n' -> 0, null -> 0 в числовом контексте, undefined -> NaN.Оператор + — и сложение, и конкатенация: "1"+2 === "12", 1+{} -> "1[object Object]".NaN не равен самому себе: NaN !== NaN — использовать Number.isNaN.typeof null === 'object' — историческое несоответствие.Знаки -0 и 0 ведут себя особо при некоторых операциях; Object.is(-0, 0) различает.parseInt без radix может дать неожиданности (старый поведение с лидирующими 0) — всегда указывайте основание.
4) this — основные источники багов
Привязка зависит от места вызова (call site):Функция вызвана как метод: obj.fn() — this = obj.Просто вызов: fn() — this = undefined (в strict) или global (в non-strict).Конструктор: new Fn() — this новый объект.fn.call(ctx) / fn.apply(ctx) — явная привязка.Стрелочные функции:
Не имеют собственной привязки this — используют лексическое this внешнего окружения. Удобно для колбеков, но может привести к ошибкам в методах классов (стрелочные методы в прототипе — нет).Потеря контекста при передаче метода как колбека:
const f = obj.method; f(); — this перестаёт быть obj. Частая причина багов в обработчиках событий и промисах.Решения: использовать bind, стрелочные функции, хранить const self = this, или использовать классы с привязкой методов.super и наследование классов: super() должен быть вызван в конструкторе наследника до обращения к this.
5) Другие неочевидные особенности, приводящие к багам
Асинхронность и порядок выполнения: макротаски vs микротаски (setTimeout vs Promise.then) — ошибки с порядком обновления состояния.Геттеры/сеттеры и побочные эффекты при чтении свойства.Прокси (Proxy) могут непредсказуемо перехватывать операции.Плавающие вычисления и потеря точности (числа с плавающей точкой).JSON.stringify/parse — потеря функций и символьных ключей.Иммутабельность: многие встроенные методы мутируют объект (Array.prototype.sort, push) — неожиданное состояние.Короткие рекомендации для предотвращения:
Использовать let/const вместо var.Всегда === / !==.Явно указывать radix в parseInt.Не расширять Object.prototype.Для словарей — Map или Object.create(null).Привязывать методы (bind) или использовать стрелочные функции осознанно.Тесты и линтер (ESLint) — ловят многие из упомянутых проблем.Если нужно — могу привести минимальные рабочие примеры кода для каждой проблемы.