Синтаксис и семантика
Что это такое
Речь идёт о самом языке 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) — многие из описанных сюрпризов система типов ловит ещё до запуска кода.