Десять дней 1995 года: язык и его шрамы
JavaScript спроектировали примерно за десять дней, и он не имеет права сломать старые страницы. Почти вся его странность вытекает из этих двух фактов — и куча разрозненной ерунды превращается в одно объяснение.
В 1995 году Netscape срочно понадобился скриптовый язык для браузера, и по маркетинговым соображениям он должен был отдалённо походить на Java. Брендан Айк выдал работающий прототип примерно за десять дней.
Дальше случилось необычное: веб сохранил все когда-либо опубликованные страницы. Браузер, ломающий сайт 1998 года, — сломанный браузер, поэтому язык можно было только дополнять, но не исправлять. Тридцать лет самого используемого языка программирования в мире, стоящего на десятидневном прототипе, который нельзя переписать.
Это не забавный факт из истории. Это самое полезное знание при изучении JavaScript, потому что оно превращает длинный список произвольных странностей в одно объяснение и сразу говорит, что делать: учи современный язык и знай старые ловушки достаточно, чтобы их узнавать.
Любой странный угол JavaScript — это обещание, которое веб дал и никогда не нарушал: старые страницы обязаны работать.
Значения и разделение, объясняющее всё остальное
В JavaScript два вида значений, и половина всей путаницы новичка живёт на границе между ними.
Примитивы — строки, числа, булевы, null, undefined, а также более редкие symbol и bigint — копируются по значению. Присвой одну переменную другой — получишь независимую копию; изменение одной не трогает вторую.
Объекты — а это и массивы, и функции, потому что и то и другое объекты — работают по ссылке. Переменная хранит не объект, а его адрес. Присвой её другой переменной — и оба имени указывают на одно и то же, поэтому изменение через одно видно через другое. Передай объект в функцию — и функция может изменить твой объект.
Это не причуда для заучивания, а механизм за огромным классом настоящих багов: массив, который ты считал скопированным, состояние, изменившееся там, куда ты не лазил, «почему поменялся и оригинал тоже?», стоящее половины дня. Всякая последующая статья, говорящая «не мутируй, делай копию», вытекает из этого абзаца.
Ещё одно, что стоит знать рано: null и undefined оба означают «ничего», и это разные «ничего». undefined язык выдаёт сам, когда значения не было: неинициализированная переменная, отсутствующее свойство, функция без возврата. null присваивает программист, чтобы сказать «пусто намеренно». Видишь undefined во время работы — обычно что-то не приехало; видишь null — кто-то так и задумал.
Динамическая типизация и ловушка приведения
Типы принадлежат значениям, а не переменным. Переменная может держать строку сейчас и число потом, и никто не возразит — удобно на десяти строках и опасно на десяти тысячах. Именно эту опасность убирает TypeScript, и ему дальше отведена отдельная статья.
Кусается же в первую очередь приведение типов: получив не тот тип, которого ожидала, операция преобразует вместо того, чтобы возразить. Сложение числа со строкой даёт строку. Сравнение строки с числом через == сначала преобразует одно из них. Отсюда знаменитый список абсурдных результатов, который гуляет по сети как доказательство сломанности языка, — и бо́льшая часть которого есть одно правило, последовательно применённое в ситуации, которую никто в здравом уме не напишет.
Почти всё это снимается двумя привычками:
Всегда пиши ===. Тройное равенство сравнивает без преобразования. Двойное сначала преобразует, по таблице правил, которую никто не помнит верно. Не существует ситуации, где == нужен настолько, чтобы оправдать двусмысленность.
Знай ложные значения. В условии всё считается истиной, кроме ровно семи вещей: false, 0, -0, "", null, undefined и NaN. Два следствия кусаются в реальном коде: пустой массив и пустой объект — истинны, а совершенно законный 0 ложен, отчего if (count) молча пропускает ноль и тихо ломает твою логику.
Раз уж мы здесь: NaN означает «не число», сам при этом имеет тип число, это единственное значение, не равное самому себе, и обычно он значит, что в арифметику попало нечто нечисловое. Увидеть его в интерфейсе — всегда баг двумя шагами выше.
Переменные: const по умолчанию
Способов объявить переменную три, а в твоём коде должны быть два.
let — для значения, которое изменится. const — для того, которое нет. var — никогда: он игнорирует границы блоков и ведёт себя так, как ведёт, исключительно ради совместимости.
Самое непонятое место: const не делает значение неизменяемым. Он запрещает переприсваивать имя. В const-массив по-прежнему можно добавлять элементы; у const-объекта можно менять свойства. Нельзя направить это имя на другой массив. Что прямо следует из правила про ссылки выше: константа — это адрес, а не содержимое.
По умолчанию бери const и переходи на let, когда переприсваивание действительно нужно. Маленькая привычка, делающая код читаемым: let превращается в сигнал «здесь меняется, будь внимателен».
Область видимости и то, почему функции — лучшая часть
Блок — всё, что в фигурных скобках, — это область видимости. Переменные, объявленные через let и const, существуют внутри и не существуют снаружи.
Функции — та часть языка, которая состарилась лучше всех, и их поведение стоит проговорить явно: функцию можно положить в переменную, передать в другую функцию и вернуть из функции. Это не продвинутая техника, а повседневная ткань JavaScript, и следующая статья построена на ней целиком.
Одно поведение стоит встретить сейчас, потому что позже оно выглядит магией: функция помнит область, в которой была создана, даже после того как эта область отработала. Это замыкание, и благодаря ему обработчик клика по-прежнему знает про переменную из функции, завершившейся три секунды назад.
Коротко про this
this в JavaScript — настоящий шрам проектирования: его значение определяется тем, как функцию вызвали, а не тем, где её описали, поэтому одна и та же функция может видеть разный this в зависимости от места вызова.
Современный совет короток. У стрелочных функций нет своего this — они берут окружающий, и почти всегда нужен именно он. В коде, который ты будешь писать ближайшие годы, this встречается в основном в классах и в старом коде, который ты поддерживаешь. Узнавай его, не строй на нём и не считай себя обязанным освоить четыре правила привязки прежде, чем начать приносить пользу.
Ошибки — это информация
Ошибка прерывает выполнение и идёт вверх, пока её кто-нибудь не поймает. try/catch — этот кто-нибудь.
Правило, отделяющее мучительную отладку от сносной: никогда не лови ошибку, чтобы ничего с ней не сделать. Пустой catch превращает громкий и локализуемый сбой в тихий неверный результат — самый дорогой вид. Не можешь обработать — пропусти. Можешь — обработай и скажи об этом.
И читай сообщения как следует. «Cannot read properties of undefined (reading 'name')» — не шум: там сказано, что нечто, считавшееся объектом, оказалось undefined, а чинить надо выше по течению от места взрыва. Половина умения отлаживать — понимание того, что сообщение об ошибке уже было ответом.
Как это на самом деле учится
JavaScript — та точка роадмапа, где чтение перестаёт работать. HTML и CSS дают мгновенную визуальную обратную связь; язык не даёт ничего, пока ты на нём что-нибудь не собрал.
Три вещи из сферы «Как учиться» действуют здесь с особой силой.
Извлечение, а не узнавание. Учебник, где каждое понятие объясняется прямо перед применением, даёт максимально гладкое ощущение и минимальное удержание: он тренирует узнавать код, а не писать его. Закрой вкладку и напиши пример заново с нуля. Разница между «я понял» и «я могу воспроизвести» — это и есть весь предмет.
Перемешивай. Двадцать задач на массивы подряд тренируют применение метода, но не выбор метода, а настоящий код никогда не сообщает, какой метод нужен. Намеренно чередуй типы задач и заставляй себя называть инструмент до того, как к нему потянулся.
Используй консоль как петлю обратной связи. Открыл, набрал выражение, через секунду увидел ответ. Плотнее петли осознанная практика почти не даёт — но только если ты предсказываешь результат до нажатия Enter. Предсказание превращает каждую строку в проверку твоей модели; пропуск предсказания превращает её в игровой автомат.
На практике
Сначала предскажи, потом запусти. Прежде чем выполнить что-либо в консоли, скажи, что она напечатает. Каждый сюрприз — бесплатный урок про неверную модель.
Набирай руками. Никакого копипаста, пока учишься. Трение и есть смысл: оно заставляет обработать, зачем нужен каждый символ.
Месяц пиши === не задумываясь и выучи семь ложных значений наизусть. Эти два факта предотвращают больше багов на единицу усилия, чем что угодно другое в языке.
Когда что-то undefined, иди назад. Не ставь заглушку в месте падения — найди, откуда значение должно было прийти. Заглушка прячет баг и сохраняет его.
Объясни замыкание вслух человеку или стене. Это понятие, которое почти все считают понятым, пока не начнут произносить слова.
Проверь себя
Закрой статью и ответь своими словами:
- Почему в JavaScript столько странных углов и что это говорит о способе его учить?
- Чем отличается присваивание примитивов от присваивания объектов и какой класс багов отсюда растёт?
- Когда ты видишь
undefined, а когдаnull? - Почему всегда
===и какие семь значений ложны? - Что именно запрещает
constи чего он не запрещает? - Что такое замыкание — одним предложением?
- Почему пустой
catchхуже, чем отсутствиеcatch?
Коротко
- Язык собрали за десять дней, и он не имеет права ломать старые страницы: один этот факт объясняет бо́льшую часть странностей и говорит учить современное подмножество плюс ловушки.
- Примитивы копируются по значению, объекты — по ссылке; отсюда «почему поменялся и оригинал?» и все дальнейшие правила про отказ от мутаций.
undefinedозначает, что ничего не приехало;null— что кто-то так решил.- Типы принадлежат значениям, а приведение преобразует вместо возражения: всегда
===, и помни, что0и""ложны, а[]и пустой объект истинны. constзамораживает имя, а не содержимое; бери его по умолчанию и пустьletслужит сигналом «здесь меняется».- Функции — это значения, а замыкание — функция, помнящая область, в которой родилась.
thisзависит от места вызова; стрелочные функции берут окружающий, и обычно нужен именно он.- Никогда не проглатывай ошибку и читай сообщение — в нём обычно назван и способ починки. Учись предсказанием, набором руками и перемешиванием, а не чтением.