Синтаксис и семантика

Что это такое

Речь идёт о самом языке JavaScript: какие в нём бывают значения, как имена связываются с ними, как функции захватывают своё окружение и по каким правилам ведут себя объекты. К браузеру всё это отношения не имеет: DOM, промисы и модули надстраиваются поверх этих правил и относятся уже к стандартной библиотеке и окружению.

Почему это важно

Когда говорят «JavaScript — странный язык», за этим чаще всего стоит другое: «JavaScript следует правилам, о которых мне не рассказали». 0.1 + 0.2 — не признак сломанного языка, this, меняющий смысл, — не баг, а переменная, равная undefined в начале функции, попала туда из-за hoisting. Знание правил превращает набор мемов в модель, из которой можно рассуждать. Именно она даёт возможность читать незнакомый код, предсказывать результат до запуска и объяснять коллеге, почему его проверка проходит там, где он ждал провала.

Ключевые идеи

У значений есть типы, и приведение происходит незаметно

В JavaScript есть примитивы (string, number, boolean, null, undefined, symbol, bigint) и объекты. == сначала приводит типы, а потом сравнивает, поэтому и получаются результаты вроде [] == false; === не приводит ничего, и брать по умолчанию стоит именно его. Ложных значений ровно шесть — false, 0, "", null, undefined, NaN — и на них держится множество неявных ветвлений.

console.log(1 == "1");   // true  — приведено
console.log(1 === "1");  // false — не приведено
console.log([] == false); // true — обе стороны приведены к 0

let и const блочные, var — нет

var поднимается к объемлющей функции, и переприсвоить его можно из любого места внутри неё. const запрещает переприсваивать привязку, однако мутировать объект, на который она указывает, не мешает. Берите const по умолчанию, let — когда переприсваивание действительно нужно, а var считайте признаком того, что перед вами старый код.

const user = { name: "Ada" };
user.name = "Grace";   // допустимо — объект мутирует
// user = {};          // TypeError — привязку переприсвоить нельзя

Функции — это значения, а замыкания захватывают переменные

Функцию можно передать, вернуть и сохранить, как любое другое значение; замыкание же запоминает переменные того места, где функция была определена, а не того, откуда её вызвали. Именно поэтому счётчик сохраняет своё значение между вызовами, и поэтому же переменная цикла может выглядеть «общей» для всех колбэков, построенных внутри него.

function counter() {
  let n = 0;
  return () => ++n;
}

const next = counter();
console.log(next(), next());  // 1 2

this зависит от того, как вызвана функция

У обычной функции this задаётся местом вызова, у стрелочной он наследуется из места, где функция написана. Поэтому метод, переданный как колбэк, теряет свой this, и поэтому же внутри колбэков предпочитают стрелочные функции. bind, call и apply задают его явно.

Объекты и прототипы

У каждого объекта есть прототип, к которому объект обращается, когда нужного свойства нет на нём самом. Классы — синтаксис для того же механизма, а методы class лежат на прототипе, а не на каждом экземпляре. Отсюда понятно, почему for...in обходит унаследованные свойства, а Object.keys — нет.

Практика

Предскажите до запуска

Напишите пять выражений, смешивающих ==, ===, null, undefined, 0 и "". Предскажите каждый результат, затем проверьте выражения в консоли и объясните каждое удивление одним предложением.

Готово, когда: у вас есть хотя бы один неверный прогноз, и теперь вы можете назвать правило приведения, которое его сломало.

Постройте замыкание, которое нельзя сломать

Напишите функцию, которая возвращает две функции с общим приватным счётчиком: одна увеличивает его, другая читает, и глобальная переменная при этом не нужна.

Готово, когда: вызов читателя снаружи всегда сообщает текущее значение, и сбросить его не может ничто, кроме инкремента.

Частые ошибки

  • Использование == для сравнения — сравнение молча приводит типы, и несвязанные значения оказываются равны; исправить: берите ===, если только не проверяете null или undefined вместе намеренно.
  • Убеждение, что const делает значение неизменяемым — свойства объекта под зафиксированной привязкой всё равно меняются; исправить: используйте Object.freeze там, где неизменяемость важна, или перестаньте мутировать общие значения.
  • Потеря this при передаче метода как колбэка — метод получает не того получателя и падает; исправить: используйте стрелочную функцию или один раз привяжите метод через bind.
  • Объявление переменных через var внутри цикла — все колбэки замыкаются на одну и ту же привязку; исправить: используйте let, который создаёт новую привязку на каждой итерации.
  • Проверка свойства через if (obj.value) — законный 0 или "" здесь выглядит как отсутствие; исправить: сравнивайте с undefined или используйте Object.hasOwn.

Что читать дальше

  • Язык гипертекстовой разметки (HTML) (02-frontend/03-html) — документ, над которым эти значения в итоге и будут работать.
  • DOM и события (02-frontend/06-javascript/02-the-dom-and-events) — как язык применяется к странице.
  • TypeScript (02-frontend/19-typescript) — многие из описанных сюрпризов система типов ловит ещё до запуска кода.