Синтаксис и семантика — Примеры

Пример 1 — Ноль, принятый за отсутствие

В объекте настроек хранится число повторных попыток. Код ниже печатает одно и то же сообщение и тогда, когда счётчик равен нулю, и тогда, когда его нет вовсе. Назовите правило, на которое здесь полагаются, а затем сделайте намерение явным.

const settings = { retries: 0, label: "" };

if (settings.retries) {
  console.log("будем повторять");
} else {
  console.log("попыток нет");   // выполняется, даже когда retries — настоящий 0
}

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

Решение

if (settings.retries > 0) {
  console.log("будем повторять");
}

if (Object.hasOwn(settings, "retries")) {
  console.log("настройка задана");
}

console.log(settings.label === "");     // равенство без приведения
console.log([] === false);              // false

Ложных значений шесть — false, 0, "", null, undefined и NaN, — поэтому проверка на истинность отвечает на вопрос «ложно ли это значение», а не «отсутствует ли оно»: законный 0 от ключа, которого нет, так не отличить. Перед сравнением == приводит типы — отсюда и равенство пустого массива false. Запись > выражает числовое намерение, Object.hasOwn — намерение о наличии, а === приведение прекращает. Правила существовали всегда; однако исходный код просто не говорил, на какой вопрос отвечает.

Пример 2 — Цикл, все колбэки которого видят последнее значение

В цикле собираются три функции, и каждая должна вывести собственный индекс. Все три выводят одно и то же число. Объясните, какую привязку они захватили, и исправьте цикл, не изменив его поведения.

const handlers = [];

for (var i = 0; i < 3; i++) {
  handlers.push(() => console.log(i));
}

handlers.forEach((handler) => handler());   // 3 3 3

Решение

const handlers = [];

for (let i = 0; i < 3; i++) {
  handlers.push(() => console.log(i));      // 0 1 2
}

handlers.forEach((handler) => handler());

Замыкание захватывает саму переменную, а не копию её значения, а var поднимается к объемлющей функции — поэтому все три стрелочные функции замыкаются на одну и ту же i, которая успевает дойти до 3, прежде чем запустится хоть одна из них. let блочный и на каждой итерации создаёт свежую привязку, так что каждый колбэк получает свою. То же правило объясняет, почему счётчик на замыкании хранит приватное состояние между вызовами: функция помнит место, где её написали, а не место, откуда её позвали.