Необов'язковий ланцюжок (?.)

Оператор необов'язкового ланцюжка (?.) звертається до властивості об'єкта або викликає функцію. Якщо об'єкт, до якого відбувається звертання, або функція, що викликається за допомогою цього оператора, – undefined або null, то замість викидання помилки – вираз закорочується й обчислюється в undefined.

Спробуйте його в дії

Синтаксис

obj.val?.prop
obj.val?.[expr]
obj.func?.(args)

Опис

Оператор ?. – подібний до оператора ланцюжка ., окрім того, що замість спричинення помилки, коли посилання є порожнім (null чи undefined), то вираз закорочується з поверненим значенням undefined. Бувши застосованим до виклику функції, оператор повертає undefined, якщо дана функція не існує.

Це призводить до коротших і простіших виразів при звертанні до ланцюжків властивостей, коли існує ймовірність того, що якесь посилання відсутнє. Також це може бути корисним при дослідженні вмісту об'єкта, коли немає гарантій того, які властивості в ньому є обов'язковими.

Припустімо, об'єкт obj має вкладену структуру. Без необов'язкового ланцюжка звертання до глибоко вкладеної підвластивості вимагає валідації проміжних посилань:

const nestedProp = obj.first && obj.first.second;

Значення obj.first перевірено на нерівність nullundefined) перед звертанням до значення obj.first.second. Такий код запобігає помилці, що трапилась би, якби відбулось звертання напряму до obj.first.second, без перевірки obj.first.

Це ідіоматичний патерн у JavaScript, але такий запис стає громіздким, коли ланцюжок – довгий, і це не є безпечним підходом. Наприклад, якщо obj.firstхибне значення, що не є null чи undefined, наприклад, 0, то це все одно призведе до закорочення і змусить nestedProp стати 0, що може не бути бажаним результатом.

Проте з оператором необов'язкового ланцюжка (?.) немає потреби явно перевіряти й закорочувати на основі стану obj.first перед спробою звернутися до obj.first.second:

const nestedProp = obj.first?.second;

При використанні оператора ?. замість простого . JavaScript знає, що перед спробою доступитися до obj.first.second треба неявно пересвідчитися, що obj.first не є ані null, ані undefined. Якщо obj.first є null чи undefined, вираз автоматично закорочується, повертаючи undefined.

Це еквівалентно до наступного коду, окрім того, що тимчасова змінна фактично не створюється:

const temp = obj.first;
const nestedProp =
  temp === null || temp === undefined ? undefined : temp.second;

Необов'язковий ланцюжок не може бути застосований до неоголошеного кореневого об'єкта, але може бути застосований до кореневого об'єкта, чиє значення – undefined.

undeclaredVar?.prop; // ReferenceError: undeclaredVar is not defined

Необов'язковий ланцюжок для виклику функцій

Необов'язковий ланцюжок можна використовувати при спробі викликати метод, що може не існувати. Це може бути корисним, наприклад, при використанні API, метод якого може бути недоступним, або через вік реалізації, або через функціональність, що недоступна на пристрої користувача

Використання з викликами функцій необов'язкового ланцюжка змушує вираз автоматично повертати undefined замість викидання винятку, якщо метод не знайдений:

const result = someInterface.customMethod?.();

Проте якщо властивість з таким іменем є, але вона не є функцією, то ?. все одно призведе до винесення винятку TypeError (someInterface.customMethod is not a function).

[!NOTE] Якщо сам someInterface є null чи undefined, все ж буде винесений виняток TypeError (someInterface is null). Якщо очікується, що сам someInterface може бути null чи undefined, слід використовувати ?. також на іншій позиції: someInterface?.customMethod?.().

eval?.() – найстисліший спосіб ввійти у режим непрямого обчислення.

Необов'язковий ланцюжок з виразами

Також оператор необов'язкового ланцюжка можна використовувати вкупі з записом квадратних дужок, котрий дає змогу передати як ім'я властивості – вираз:

const nestedProp = obj?.["prop" + "Name"];

Це особливо корисно для масивів, адже до індексів масивів можна звертатися лише з квадратними дужками.

function printMagicIndex(arr) {
  console.log(arr?.[42]);
}
printMagicIndex([0, 1, 2, 3, 4, 5]); // undefined
printMagicIndex(); // undefined; якби не ?., тут викинуло б помилку "Cannot read properties of undefined (reading '42')"

Недійсний необов'язковий ланцюжок

Не можна намагатися присвоїти результат виразові необов'язкового ланцюжка:

const object = {};
object?.property = 1; // SyntaxError: Invalid left-hand side in assignment

Теги шаблонних літералів не можуть з'являтися в необов'язковому ланцюжку (дивіться SyntaxError: tagged template cannot be used with optional chain):

String?.raw`Привіт, світе!`;
String.raw?.`Привіт, світе!`; // SyntaxError: Invalid tagged template on optional chain

Конструктор виразів new не може бути частиною необов'язкового ланцюжка (дивіться SyntaxError: new keyword cannot be used with an optional chain):

new Intl?.DateTimeFormat(); // SyntaxError: Invalid optional chain from new expression
new Map?.();

Закорочення

Якщо при використанні необов'язкового ланцюжка з виразами лівий операнд є null чи undefined, то вираз не буде обчислюватися. Наприклад:

const potentiallyNullObj = null;
let x = 0;
const prop = potentiallyNullObj?.[x++];

console.log(x); // 0, оскільки x не було збільшено

Подальші звертання до властивостей також не будуть обчислені.

const potentiallyNullObj = null;
const prop = potentiallyNullObj?.a.b;
// Не викидає, тому що обчислення вже зупинилося на
// першій необов'язковій ланці

Це еквівалентно щодо:

const potentiallyNullObj = null;
const prop =
  potentiallyNullObj === null || potentiallyNullObj === undefined
    ? undefined
    : potentiallyNullObj.a.b;

А проте, така закорочувальна поведінка спрацьовує лише для одного протяжного "ланцюжка" звертань до властивостей. Якщо згрупувати частину ланцюжка, то подальші звертання до властивостей все ж будуть обчислені.

const potentiallyNullObj = null;
const prop = (potentiallyNullObj?.a).b;
// TypeError: Cannot read properties of undefined (reading 'b')

Це еквівалентно щодо:

const potentiallyNullObj = null;
const temp = potentiallyNullObj?.a;
const prop = temp.b;

Окрім того, що змінна temp не створюється.

Приклади

Найпростіший приклад

Цей приклад шукає значення властивості name елемента відображення CSS, де такого елемента немає. Таким чином, результатом є undefined.

const myMap = new Map();
myMap.set("JS", { name: "Денис", desc: "Я обслуговую всіляке" });

const nameBar = myMap.get("CSS")?.name;

Виклик необов'язкових функцій зворотного виклику чи обробників подій

Якщо використовуються функції зворотного виклику, або якщо методи отримуються з об'єкта шляхом присвоєння з деструктуруванням, можуть бути відсутні значення, котрі не можна викликати як функції, не перевіривши їх існування. За допомогою ?. цієї додаткової перевірки можна уникнути:

// Код, написаний без застосування необов'язкового ланцюжка
function doSomething(onContent, onError) {
  try {
    // Якісь операції з даними
  } catch (err) {
    if (onError) {
      // Перевірка того, що onError справді існує
      onError(err.message);
    }
  }
}
// Використання необов'язкового ланцюжка з викликами функцій
function doSomething(onContent, onError) {
  try {
    // Якісь операції з даними
  } catch (err) {
    onError?.(err.message); // Жодного винятку, якщо onError – undefined
  }
}

Нагромадження операторів необов'язкового ланцюжка

При роботі зі вкладеними структурами можливо використовувати необов'язковий ланцюжок декілька разів:

const customer = {
  name: "Карл",
  details: {
    age: 82,
    location: "Райські води", // Точна адреса – невідома
  },
};
const customerCity = customer.details?.address?.city;

// Це також працюватиме з викликом функції з необов'язковим ланцюжком
const customerName = customer.name?.getName?.(); // Метод не існує, customerName – undefined

Поєднання з оператором null-злиття

Оператор null-злиття може використовуватися після необов'язкового ланцюжка, аби надати усталене значення, коли нічого не знайдено:

function printCustomerCity(customer) {
  const customerCity = customer?.city ?? "Невідоме місто";
  console.log(customerCity);
}
printCustomerCity({
  name: "Натан",
  city: "Львів",
}); // "Львів"
printCustomerCity({
  name: "Карл",
  details: { age: 82 },
}); // "Невідоме місто"

Специфікації

Сумісність із браузерами

desktop mobile server
Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet Deno Node.js
Optional chaining operator (?.)
Chrome Full support 80
Edge Full support 80
Firefox Full support 74
Internet Explorer No support Ні
Opera Full support 67
Safari Full support 13.1
WebView Android Full support 80
Chrome Android Full support 80
Firefox for Android Full support 79
Opera Android Full support 57
Safari on iOS Full support 13.4
Samsung Internet Full support 13.0
Deno Full support 1.0
Node.js Full support 14.0.0

Дивіться також