Слышал про оператор (??) нулевого слияния? Можешь рассказать?

👨‍💻 Frontend Developer ⚡ Высокий 🎚️ Легкий
#JavaScript #База JS

Краткий ответ

Оператор нулевого слияния ?? (Nullish Coalescing) возвращает правый операнд, если левый равен null или undefined, в отличие от оператора ||, который проверяет на “ложность”:

// Оператор ??
const name = null ?? "Гость";        // "Гость"
const age = 0 ?? 18;                 // 0 (не заменяется!)
const count = "" ?? "пусто";         // "" (не заменяется!)
 
// Сравнение с ||
const name2 = null || "Гость";       // "Гость"
const age2 = 0 || 18;                // 18 (заменяется!)
const count2 = "" || "пусто";        // "пусто" (заменяется!)

Рекомендация: Используйте ?? для работы с null/undefined, а || для работы с “ложными” значениями.


Что такое оператор нулевого слияния

Синтаксис и базовое использование

// Базовый синтаксис
const result = leftValue ?? rightValue;
 
// Примеры
const userName = user.name ?? "Анонимный пользователь";
const userAge = user.age ?? 0;
const userSettings = user.settings ?? {};

Когда срабатывает ??

Оператор ?? возвращает правый операнд только если левый равен:

  • null
  • undefined
// Срабатывает (возвращает правый операнд)
console.log(null ?? "default");      // "default"
console.log(undefined ?? "default"); // "default"
 
// НЕ срабатывает (возвращает левый операнд)
console.log(0 ?? "default");         // 0
console.log("" ?? "default");        // ""
console.log(false ?? "default");     // false
console.log(NaN ?? "default");       // NaN

Сравнение ?? и ||

Значение?? “default”|| “default”Объяснение
null"default""default"Оба оператора заменяют
undefined"default""default"Оба оператора заменяют
00"default"?? сохраняет, || заменяет
"""""default"?? сохраняет, || заменяет
falsefalse"default"?? сохраняет, || заменяет
NaNNaN"default"?? сохраняет, || заменяет
"text""text""text"Оба сохраняют
424242Оба сохраняют

Ключевые различия

// Работа с числами
const score = 0;
console.log(score ?? 100);  // 0 (сохраняет ноль)
console.log(score || 100);  // 100 (заменяет ноль)
 
// Работа со строками
const message = "";
console.log(message ?? "Нет сообщения");  // "" (сохраняет пустую строку)
console.log(message || "Нет сообщения");  // "Нет сообщения" (заменяет)
 
// Работа с boolean
const isVisible = false;
console.log(isVisible ?? true);  // false (сохраняет false)
console.log(isVisible || true);  // true (заменяет false)

Практические примеры

1. Настройки пользователя

// ❌ Проблема с ||
function getUserSettings(user) {
  return {
    theme: user.theme || "light",           // Проблема: "" станет "light"
    fontSize: user.fontSize || 16,          // Проблема: 0 станет 16
    notifications: user.notifications || true // Проблема: false станет true
  };
}
 
const user = {
  theme: "",        // Пользователь хочет пустую тему
  fontSize: 0,      // Пользователь хочет минимальный размер
  notifications: false // Пользователь отключил уведомления
};
 
console.log(getUserSettings(user));
// { theme: "light", fontSize: 16, notifications: true } - НЕ то, что хотел пользователь!
 
// ✅ Правильно с ??
function getUserSettingsCorrect(user) {
  return {
    theme: user.theme ?? "light",
    fontSize: user.fontSize ?? 16,
    notifications: user.notifications ?? true
  };
}
 
console.log(getUserSettingsCorrect(user));
// { theme: "", fontSize: 0, notifications: false } - Именно то, что хотел пользователь!

2. API ответы

// Обработка ответа от API
function processApiResponse(response) {
  // ❌ Плохо с ||
  const badResult = {
    id: response.id || "unknown",
    count: response.count || 0,        // Проблема: если count = 0
    isActive: response.isActive || false // Проблема: если isActive = false
  };
 
  // ✅ Хорошо с ??
  const goodResult = {
    id: response.id ?? "unknown",
    count: response.count ?? 0,
    isActive: response.isActive ?? false
  };
 
  return goodResult;
}
 
// Тестовые данные
const apiResponse = {
  id: "user123",
  count: 0,        // Валидное значение!
  isActive: false  // Валидное значение!
};
 
console.log(processApiResponse(apiResponse));
// { id: "user123", count: 0, isActive: false }

3. Конфигурация приложения

// Загрузка конфигурации
function loadConfig(userConfig = {}) {
  const defaultConfig = {
    apiUrl: "https://api.example.com",
    timeout: 5000,
    retries: 3,
    debug: false
  };
 
  return {
    apiUrl: userConfig.apiUrl ?? defaultConfig.apiUrl,
    timeout: userConfig.timeout ?? defaultConfig.timeout,
    retries: userConfig.retries ?? defaultConfig.retries,
    debug: userConfig.debug ?? defaultConfig.debug
  };
}
 
// Пользователь хочет отключить отладку и установить 0 повторов
const userConfig = {
  timeout: 0,     // Без таймаута
  retries: 0,     // Без повторов
  debug: false    // Без отладки
};
 
console.log(loadConfig(userConfig));
// Сохранит пользовательские настройки: timeout: 0, retries: 0, debug: false

Цепочки операторов ??

Множественные проверки

// Проверка нескольких источников
const userName = 
  user.preferredName ?? 
  user.firstName ?? 
  user.email ?? 
  "Гость";
 
// Эквивалентно:
let userName2;
if (user.preferredName !== null && user.preferredName !== undefined) {
  userName2 = user.preferredName;
} else if (user.firstName !== null && user.firstName !== undefined) {
  userName2 = user.firstName;
} else if (user.email !== null && user.email !== undefined) {
  userName2 = user.email;
} else {
  userName2 = "Гость";
}

Комбинирование с Optional Chaining

// Безопасное обращение к вложенным свойствам
const city = user?.address?.city ?? "Не указан";
const phone = user?.contacts?.phone ?? "Не указан";
const avatar = user?.profile?.avatar?.url ?? "/default-avatar.png";
 
// Работа с массивами
const firstItem = data?.items?.[0] ?? "Нет элементов";
const lastItem = data?.items?.at?.(-1) ?? "Нет элементов";

Nullish Coalescing Assignment (??=)

Присваивание с проверкой

// Оператор ??= (ES2021)
let config = {};
 
// Устанавливает значение только если оно null или undefined
config.theme ??= "dark";
config.language ??= "ru";
config.timeout ??= 5000;
 
console.log(config);
// { theme: "dark", language: "ru", timeout: 5000 }
 
// Не перезаписывает существующие значения
config.theme = "light";
config.theme ??= "dark";  // Не изменится
console.log(config.theme); // "light"

Практическое применение ??=

// Инициализация кэша
class DataCache {
  constructor() {
    this.cache = {};
  }
 
  get(key) {
    // Создаем запись в кэше только если её нет
    this.cache[key] ??= this.fetchData(key);
    return this.cache[key];
  }
 
  fetchData(key) {
    console.log(`Загружаем данные для ${key}`);
    return `Данные для ${key}`;
  }
}
 
const cache = new DataCache();
console.log(cache.get("user1")); // Загружаем данные для user1
console.log(cache.get("user1")); // Берем из кэша

Практические задачи

Задача 1: Что выведет консоль?

const a = 0;
const b = "";
const c = false;
const d = null;
 
console.log(a ?? "default");
console.log(b ?? "default");
console.log(c ?? "default");
console.log(d ?? "default");
Ответ

0, "", false, "default"

  • a ?? "default"0 (ноль не равен null/undefined)
  • b ?? "default""" (пустая строка не равна null/undefined)
  • c ?? "default"false (false не равен null/undefined)
  • d ?? "default""default" (null заменяется)

Задача 2: Исправьте функцию

// ❌ Проблемная функция
function createUser(data) {
  return {
    name: data.name || "Пользователь",
    age: data.age || 18,
    isAdmin: data.isAdmin || false,
    score: data.score || 0
  };
}
 
// Тестовые данные
const userData = {
  name: "",           // Пользователь не указал имя
  age: 0,             // Новорожденный
  isAdmin: false,     // Обычный пользователь
  score: 0            // Начальный счет
};
 
console.log(createUser(userData));
// Что будет не так?
Ответ

Проблема: Все значения будут заменены на значения по умолчанию из-за оператора ||.

Результат: { name: "Пользователь", age: 18, isAdmin: false, score: 0 }

Исправленная версия:

function createUser(data) {
  return {
    name: data.name ?? "Пользователь",
    age: data.age ?? 18,
    isAdmin: data.isAdmin ?? false,
    score: data.score ?? 0
  };
}
 
// Теперь результат: { name: "", age: 0, isAdmin: false, score: 0 }

Задача 3: Цепочка операторов

const user = {
  profile: {
    social: {
      twitter: null,
      facebook: undefined,
      instagram: ""
    }
  }
};
 
// Найдите первую доступную социальную сеть
const socialNetwork = 
  user.profile.social.twitter ?? 
  user.profile.social.facebook ?? 
  user.profile.social.instagram ?? 
  "Не указано";
 
console.log(socialNetwork); // ?
Ответ

"" (пустая строка)

  • twitter равен null → переходим к следующему
  • facebook равен undefined → переходим к следующему
  • instagram равен "" → возвращаем "" (пустая строка не равна null/undefined)

Задача 4: Оператор ??=

const settings = {
  theme: "dark",
  language: null,
  notifications: undefined
};
 
settings.theme ??= "light";
settings.language ??= "en";
settings.notifications ??= true;
settings.newFeature ??= "enabled";
 
console.log(settings);
Ответ
{
  theme: "dark",           // Не изменилось (уже было значение)
  language: "en",          // Изменилось (было null)
  notifications: true,     // Изменилось (было undefined)
  newFeature: "enabled"    // Добавилось (не существовало)
}

Задача 5: Комбинирование с Optional Chaining

const response = {
  data: {
    users: [
      { id: 1, profile: { name: "Анна" } },
      { id: 2, profile: null },
      { id: 3 }
    ]
  }
};
 
// Получите имена всех пользователей с fallback
const names = response.data.users.map(user => 
  user.profile?.name ?? "Имя не указано"
);
 
console.log(names);
Ответ

["Анна", "Имя не указано", "Имя не указано"]

  • Первый пользователь: profile.name существует → "Анна"
  • Второй пользователь: profile равен null?.name возвращает undefined?? возвращает "Имя не указано"
  • Третий пользователь: profile не существует → ?.name возвращает undefined?? возвращает "Имя не указано"

Лучшие практики

1. Используйте ?? для null/undefined

// ✅ Хорошо: для проверки null/undefined
const userName = user.name ?? "Гость";
const userAge = user.age ?? 0;
 
// ❌ Плохо: для проверки "ложных" значений
const isVisible = element.visible ?? true; // Используйте || для boolean

2. Комбинируйте с Optional Chaining

// ✅ Хорошо: безопасное обращение к свойствам
const city = user?.address?.city ?? "Не указан";
const avatar = user?.profile?.avatar?.url ?? "/default.png";
 
// ❌ Плохо: длинные проверки
const city2 = (user && user.address && user.address.city) ?? "Не указан";

3. Используйте ??= для инициализации

// ✅ Хорошо: инициализация свойств
config.timeout ??= 5000;
config.retries ??= 3;
 
// ❌ Плохо: избыточные проверки
if (config.timeout === null || config.timeout === undefined) {
  config.timeout = 5000;
}

4. Документируйте намерения

// ✅ Хорошо: ясно, что проверяем только null/undefined
function processData(data) {
  // Используем пустой объект только если data не передан
  const safeData = data ?? {};
  
  // Используем 0 как валидное значение для счетчика
  const count = safeData.count ?? 0;
  
  return { count };
}
 
// ❌ Плохо: неясно, что именно проверяем
function processDataBad(data) {
  const safeData = data || {}; // Заменит и пустые объекты!
  const count = safeData.count || 0; // Заменит и валидный ноль!
  return { count };
}

Поддержка браузерами

Совместимость

  • Chrome: 80+
  • Firefox: 72+
  • Safari: 13.1+
  • Edge: 80+
  • Node.js: 14+

Полифил для старых браузеров

// Простой полифил для ??
if (!window.nullishCoalescing) {
  function nullishCoalescing(left, right) {
    return (left !== null && left !== undefined) ? left : right;
  }
  
  // Использование
  const result = nullishCoalescing(value, "default");
}
 
// Или используйте Babel для автоматической трансформации

Современные возможности

Logical Assignment Operators (ES2021)

// Три новых оператора присваивания
let a, b, c;
 
// Nullish coalescing assignment
a ??= "default";  // a = a ?? "default"
 
// Logical OR assignment
b ||= "default";   // b = b || "default"
 
// Logical AND assignment
c &&= "value";     // c = c && "value"

Комбинирование с другими операторами

// Мощные комбинации
const userPreferences = {
  theme: user?.settings?.theme ?? 
         localStorage.getItem("theme") ?? 
         "system",
         
  language: user?.profile?.language ?? 
            navigator.language?.split("-")?.[0] ?? 
            "en"
};
 
// Условное присваивание
user.lastLogin ??= new Date();
user.preferences ??= {};
user.preferences.notifications ??= true;

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

1. Путаница с || оператором

// ❌ Плохо: неправильный выбор оператора
function setVolume(volume) {
  // Проблема: volume = 0 будет заменен на 50
  this.volume = volume || 50;
}
 
// ✅ Хорошо: правильный выбор
function setVolume(volume) {
  // 0 - валидное значение для громкости
  this.volume = volume ?? 50;
}

2. Неправильное использование с boolean

// ❌ Плохо: ?? не подходит для boolean логики
function isFeatureEnabled(flag) {
  // Проблема: false не будет заменен на true
  return flag ?? true;
}
 
// ✅ Хорошо: используйте || для boolean
function isFeatureEnabled(flag) {
  return flag !== false; // Или другая логика
}

3. Избыточные проверки

// ❌ Плохо: избыточно
const value = (data !== null && data !== undefined) ? data : "default";
 
// ✅ Хорошо: лаконично
const value = data ?? "default";

Заключение

Золотые правила:

  1. Используйте ?? для проверки null/undefined — это точно и предсказуемо
  2. Используйте || для проверки “ложных” значений — когда нужно заменить 0, "", false
  3. Комбинируйте с Optional Chaining (?.) — для безопасного доступа к свойствам
  4. Используйте ??= для инициализации — лаконично и читаемо
  5. Документируйте намерения — делайте код понятным для других разработчиков

Помните: Оператор ?? решает конкретную задачу — работу с null и undefined. Не пытайтесь использовать его везде, выбирайте правильный инструмент для каждой ситуации.


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice(@AleksandrEmolov_EasyAdvice), добавляйте сайт в закладки и прокачивайтесь каждый день 💪