Оператор объединения с null
Предложение об операторе объединения с null (??
) добавляет новый оператор краткого замыкания, предназначенный для обработки значений по умолчанию.
Вы, возможно, уже знакомы с другими операторами краткого замыкания &&
и ||
. Оба этих оператора работают с "истинными" и "ложными" значениями. Представьте следующий пример: lhs && rhs
. Если lhs
(читаем как "левая сторона") является ложным значением, выражение оценивается как lhs
. В противном случае оно оценивается как rhs
(читаем как "правая сторона"). Для примера lhs || rhs
действует обратное правило. Если lhs
является истинным значением, выражение оценивается как lhs
. В противном случае оно оценивается как rhs
.
Но что именно означает "истинное" и "ложное" значение? В терминах спецификации это эквивалентно абстрактной операции ToBoolean
. Для нас, обычных разработчиков JavaScript, всё является истинным значением, кроме ложных значений: undefined
, null
, false
, 0
, NaN
, и пустой строки ''
. (Технически, значение, связанное с document.all
, также является ложным, но мы обсудим это позже.)
Так в чем проблема с &&
и ||
? И почему нам нужен новый оператор объединения с null? Проблема в том, что определение истинных и ложных значений не подходит для всех сценариев, что приводит к ошибкам. Представьте следующий пример:
function Component(props) {
const enable = props.enabled || true;
// …
}
В этом примере мы будем рассматривать свойство enabled
как необязательное логическое свойство, которое управляет включением функциональности компонента. Это означает, что мы можем явно установить enabled
как true
или false
. Но, поскольку это необязательное свойство, мы можем не устанавливать его вообще, что эквивалентно заданию enabled = undefined
. Если его значение равно undefined
, мы хотим считать, что компонент имеет значение enabled = true
(значение по умолчанию).
Вероятно, на данный момент вы уже заметили ошибку в примере кода. Если мы явно устанавливаем enabled = true
, то переменная enable
равна true
. Если мы неявно устанавливаем enabled = undefined
, то переменная enable
также равна true
. А если мы явно устанавливаем enabled = false
, то переменная enable
всё равно равна true
! Мы хотели задать значение по умолчанию true
, но вместо этого выбрали его принудительно. Решение этого случая заключается в четком указании ожидаемых значений:
function Component(props) {
const enable = props.enabled !== false;
// …
}
Такая ошибка может возникнуть с любым ложным значением. Это могло бы также быть необязательной строкой (где пустая строка ''
считается допустимым вводом) или необязательным числом (где 0
считается допустимым вводом). Это настолько распространённая проблема, что теперь мы вводим оператор объединения с null для обработки подобного назначения значений по умолчанию:
function Component(props) {
const enable = props.enabled ?? true;
// …
}
Оператор объединения с null (??
) работает очень похоже на оператор ||
, за исключением того, что мы не используем "истинные значения" при оценке оператора. Вместо этого мы используем определение "значение nullish", что означает "значение строго равно null
или undefined
". Представьте выражение lhs ?? rhs
: если lhs
не является значением nullish, оно оценивается как lhs
. В противном случае оно оценивается как rhs
.
Явно это означает, что значения false
, 0
, NaN
, и пустая строка ''
— это ложные значения, которые не являются nullish. Когда такие значения, которые ложные, но не nullish, находятся на левой стороне выражения lhs ?? rhs
, оно оценивается в них, вместо правой стороны. Ошибки исчезают!
false ?? true; // => false
0 ?? 1; // => 0
'' ?? 'default'; // => ''
null ?? []; // => []
undefined ?? []; // => []
А как насчёт назначения по умолчанию при использовании деструктуризации?
Вы могли заметить, что последний пример кода также можно исправить с помощью назначения по умолчанию внутри объекта при деструктуризации:
function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}
Это несколько громоздко, но всё ещё полностью корректный JavaScript. Однако он использует немного другую семантику. Назначение по умолчанию внутри объекта при деструктуризации проверяет, равно ли свойство строго undefined
, и в случае равенства принимает значение по умолчанию.
Но эти проверки строгого равенства только с undefined
не всегда желательны, и объект для выполнения деструктуризации не всегда доступен. Например, может быть необходимость задать значение по умолчанию для возвращаемых значений функции (нет объекта для деструктуризации). Или функция может возвращать null
(что распространено для DOM API). В таких случаях стоит использовать оператор объединения с null:
// Лаконичное объединение с null
const link = document.querySelector('link') ?? document.createElement('link');
// Стандартная деструктуризация с boilerplate
const {
link = document.createElement('link'),
} = {
link: document.querySelector('link') || undefined
};
Кроме того, некоторые новые функции, такие как опциональная цепочка, не работают идеально с деструктуризацией. Поскольку для деструктуризации требуется объект, необходимо предусмотреть защиту в случае, если опциональная цепочка вернёт undefined
вместо объекта. С использованием слияния с nullish у нас нет такой проблемы:
// Опциональная цепочка и слияние с nullish вместе
const link = obj.deep?.container.link ?? document.createElement('link');
// Стандартная деструктуризация с опциональной цепочкой
const {
link = document.createElement('link'),
} = (obj.deep?.container || {});
Смешивание и сочетание операторов
Проектирование языка — это трудно, и мы не всегда можем создать новые операторы без определённой неоднозначности в намерениях разработчика. Если вы когда-либо смешивали операторы &&
и ||
, вы, вероятно, сами сталкивались с этой неоднозначностью. Представьте выражение lhs && middle || rhs
. В JavaScript это фактически парсится так же, как выражение (lhs && middle) || rhs
. Теперь подумайте о выражении lhs || middle && rhs
. Это будет парситься как lhs || (middle && rhs)
.
Вы, вероятно, заметили, что оператор &&
имеет более высокий приоритет для левой и правой сторон, чем оператор ||
, что означает, что подразумеваемые скобки охватывают &&
, а не ||
. При проектировании оператора ??
нам пришлось решить, какой будет его приоритет. Он мог быть:
- ниже, чем у обоих
&&
и||
- ниже, чем у
&&
, но выше, чем у||
- выше, чем у обоих
&&
и||
Для каждой из этих дефиниций приоритетов нам пришлось протестировать четыре возможных случая:
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
В каждом тестовом выражении нам нужно было решить, где будут находиться неявные скобки. И если они не обрамляли выражение так, как задумал разработчик, мы бы получили плохо написанный код. К сожалению, какой бы уровень приоритета мы ни выбрали, одно из тестовых выражений могло нарушать намерения разработчика.
В конце концов, мы решили требовать явных скобок при смешивании ??
и (&&
или ||
) (заметьте, как я был точен в группировке скобок! мета-юмор!). Если вы смешиваете, вы должны обернуть одну из групп операторов в скобки или получите синтаксическую ошибку.
// Явные группы скобок обязательны для смешивания
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);
(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);
(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
Таким образом, синтаксический анализатор языка всегда будет соответствовать намерениям разработчика. И любой, кто позже читает этот код, тоже сразу поймёт его. Здорово!
Расскажите мне про document.all
document.all
— это специальное значение, которое вы никогда, никогда, никогда не должны использовать. Но если вы всё же его используете, лучше знать, как оно взаимодействует с «истинными значениями» и «nullish».
document.all
— это объект, похожий на массив, а это значит, что у него есть индексированные свойства, как у массива, и длина. Объекты обычно истиненны — но, что удивительно, document.all
притворяется ложным значением! На самом деле, он слабо равен как null
, так и undefined
(что обычно означает, что у него вообще не может быть свойств).
При использовании document.all
с &&
или ||
он притворяется ложным. Однако он не строго равен null
или undefined
, так что он не nullish. Таким образом, при использовании document.all
с ??
он ведёт себя как любой другой объект.
document.all || true; // => true
document.all ?? true; // => HTMLAllCollection[]