EverProduct
Фронтенд

Этап 03 · Язык

Десять дней 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, иди назад. Не ставь заглушку в месте падения — найди, откуда значение должно было прийти. Заглушка прячет баг и сохраняет его.

Объясни замыкание вслух человеку или стене. Это понятие, которое почти все считают понятым, пока не начнут произносить слова.

Проверь себя

Закрой статью и ответь своими словами:

  1. Почему в JavaScript столько странных углов и что это говорит о способе его учить?
  2. Чем отличается присваивание примитивов от присваивания объектов и какой класс багов отсюда растёт?
  3. Когда ты видишь undefined, а когда null?
  4. Почему всегда === и какие семь значений ложны?
  5. Что именно запрещает const и чего он не запрещает?
  6. Что такое замыкание — одним предложением?
  7. Почему пустой catch хуже, чем отсутствие catch?

Коротко

  • Язык собрали за десять дней, и он не имеет права ломать старые страницы: один этот факт объясняет бо́льшую часть странностей и говорит учить современное подмножество плюс ловушки.
  • Примитивы копируются по значению, объекты — по ссылке; отсюда «почему поменялся и оригинал?» и все дальнейшие правила про отказ от мутаций.
  • undefined означает, что ничего не приехало; null — что кто-то так решил.
  • Типы принадлежат значениям, а приведение преобразует вместо возражения: всегда ===, и помни, что 0 и "" ложны, а [] и пустой объект истинны.
  • const замораживает имя, а не содержимое; бери его по умолчанию и пусть let служит сигналом «здесь меняется».
  • Функции — это значения, а замыкание — функция, помнящая область, в которой родилась.
  • this зависит от места вызова; стрелочные функции берут окружающий, и обычно нужен именно он.
  • Никогда не проглатывай ошибку и читай сообщение — в нём обычно назван и способ починки. Учись предсказанием, набором руками и перемешиванием, а не чтением.