Другое

Лучший способ проверки вложенных свойств в JavaScript

Узнайте лучшие методы безопасного доступа к глубоко вложенным свойствам JavaScript. Откройте возможности optional chaining, Lodash.get и собственных решений.

Как лучше проверить наличие свойств в глубоко вложенных объектах JavaScript?

У меня есть ссылка на объект JavaScript, который может в дальнейшем содержать вложенные объекты:

javascript
var test = {};

Объект может выглядеть так:

javascript
{level1: {level2: {level3: "level3"}}};

При попытке напрямую обратиться к вложенным свойствам:

javascript
alert(test.level1); // yields undefined
alert(test.level1.level2.level3); // throws an error

Мой текущий подход — проверять каждый уровень:

javascript
if (test.level1 && test.level1.level2 && test.level1.level2.level3) {
    alert(test.level1.level2.level3);
}

Есть ли более эффективный или элегантный способ проверить наличие глубоко вложенных свойств в объектах JavaScript?

Лучший способ проверить глубоко вложенные свойства в JavaScript — использовать оператор опциональной цепочки (?.), введённый в ES2020. Он позволяет безопасно обращаться к вложенным свойствам без ручных проверок null, автоматически возвращая undefined, если любой промежуточный объект не существует. Для сложных сценариев или совместимости с более старыми браузерами можно использовать утилиты, такие как _.get() из Lodash, либо написать собственные вспомогательные функции, которые безопасно обходят пути объекта.

Содержание

Опциональная цепочка (ES2020)

Оператор опциональной цепочки (?.) является самым элегантным и современным решением для безопасного доступа к глубоко вложенным свойствам в JavaScript. Введённый в ES2020, он автоматически обрабатывает случаи, когда промежуточные свойства равны null или undefined, не выбрасывая ошибок.

javascript
var test = {};
var level3Value = test?.level1?.level2?.level3; // Возвращает undefined, без ошибки

Ключевые особенности:

  • Короткое замыкание: если любое свойство в цепочке равно null или undefined, выражение сразу возвращает undefined.
  • Чистый синтаксис: сокращает громоздкие проверки null до одной строки.
  • Гибкое использование: работает с свойствами, вызовами методов и индексами массивов.

Примеры:

javascript
// Базовый доступ к свойству
const user = {
  name: 'John',
  address: {
    street: '123 Main St',
    city: 'New York'
  }
};

const street = user?.address?.street; // '123 Main St'
const zipCode = user?.address?.zipCode; // undefined (без ошибки)
const phoneNumber = user?.contact?.phone; // undefined (без ошибки)

// С массивами
const data = {
  users: [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ]
};

const secondUserName = data?.users?.[1]?.name; // 'Bob'
const thirdUserName = data?.users?.[2]?.name; // undefined

// С вызовами методов
const user = { profile: { getName: () => 'John' } };
const userName = user?.profile?.getName?.(); // 'John'

Согласно MDN Web Docs, оператор опциональной цепочки возвращает undefined, если свойство объекта не определено или равно null (вместо выброса ошибки), что делает его идеальным для безопасного доступа к свойствам.

Метод _.get() из Lodash

Для проектов, уже использующих Lodash, или когда нужна поддержка старых браузеров, метод _.get() предоставляет надёжное решение для безопасного доступа к вложенным свойствам.

javascript
import _ from 'lodash';

var test = {};
var level3Value = _.get(test, 'level1.level2.level3', 'default value');

Преимущества:

  • Синтаксис строки пути: доступ к свойствам через строку с точечной нотацией.
  • Значения по умолчанию: задаёт запасное значение, если свойства не существует.
  • Поддержка массивов: работает с индексами массивов в путях.
  • Универсальная совместимость: работает во всех средах JavaScript.

Примеры:

javascript
const data = {
  user: {
    profile: {
      name: 'Alice',
      preferences: {
        theme: 'dark'
      }
    }
  }
};

// Базовое использование
const name = _.get(data, 'user.profile.name'); // 'Alice'
const theme = _.get(data, 'user.profile.preferences.theme'); // 'dark'

// С значениями по умолчанию
const fontSize = _.get(data, 'user.profile.preferences.fontSize', 16); // 16
const email = _.get(data, 'user.profile.email', 'no-email@example.com'); // 'no-email@example.com'

// С индексами массивов
const items = _.get(data, 'user.profile.preferences.items.0.name', 'default');

Как отмечено в обсуждениях на Stack Overflow, _.get() из Lodash — проверенное решение, которое прошло испытание в продакшене.

Собственные вспомогательные функции

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

Базовая функция на основе reduce:

javascript
const getNestedValue = (obj, path, defaultValue) => {
  return path.split('.').reduce((item, key) => {
    if (item && typeof item === 'object' && key in item) {
      return item[key];
    }
    return defaultValue;
  }, obj);
};

// Использование
var test = {};
var level3Value = getNestedValue(test, 'level1.level2.level3', 'default');

Расширенная функция с поддержкой массивов:

javascript
const safeGet = (obj, path, defaultValue = undefined) => {
  if (!obj || typeof obj !== 'object') return defaultValue;
  
  const keys = Array.isArray(path) ? path : path.split('.');
  let current = obj;
  
  for (const key of keys) {
    if (current === null || current === undefined) {
      return defaultValue;
    }
    if (Array.isArray(current) && !isNaN(key)) {
      current = current[parseInt(key)];
    } else {
      current = current[key];
    }
  }
  
  return current !== undefined ? current : defaultValue;
};

// Использование
const data = { users: [{ name: 'Alice' }] };
const userName = safeGet(data, 'users.0.name'); // 'Alice'
const userAge = safeGet(data, 'users.0.age', 25); // 25

Функциональный подход с Ramda:

javascript
import R from 'ramda';

const getValue = R.curry((path, obj) => R.path(path, obj));

const safeGet = R.curry((path, obj, defaultValue) => 
  R.defaultTo(defaultValue, getValue(path, obj))
);

// Использование
const data = { user: { profile: { name: 'Bob' } } };
const name = safeGet(['user', 'profile', 'name'], data); // 'Bob'
const email = safeGet(['user', 'profile', 'email'], data, 'default@example.com'); // 'default@example.com'

Согласно JavaScript Inside, функциональные подходы с использованием Ramda’s Either или Maybe монады обеспечивают дополнительную безопасность и могут быть особенно полезны в сложных приложениях.


Решения на основе Proxy ES6

Для продвинутых случаев можно использовать объекты Proxy ES6, чтобы создать «безопасные» объекты, которые автоматически обрабатывают доступ к свойствам без выброса ошибок.

Базовый безопасный объект Proxy:

javascript
const createSafeObject = (target) => {
  return new Proxy(target, {
    get(obj, prop) {
      if (obj && typeof obj === 'object' && prop in obj) {
        const value = obj[prop];
        if (typeof value === 'object' && value !== null) {
          return createSafeObject(value);
        }
        return value;
      }
      return undefined;
    }
  });
};

// Использование
var test = {};
const safeTest = createSafeObject(test);
console.log(safeTest.level1?.level2?.level3); // undefined (без ошибки)

Расширенный безопасный доступ Proxy:

javascript
class SafeAccessProxy {
  constructor(target) {
    return new Proxy(target, {
      get(obj, prop) {
        const value = obj?.[prop];
        if (value && typeof value === 'object') {
          return new SafeAccessProxy(value);
        }
        return value;
      }
    });
  }
}

// Использование
const data = {
  user: {
    profile: {
      name: 'Alice',
      preferences: { theme: 'dark' }
    }
  }
};

const safeData = new SafeAccessProxy(data);
console.log(safeData.user?.profile?.name); // 'Alice'
console.log(safeData.user?.profile?.fontSize); // undefined
console.log(safeData.user?.settings?.notifications); // undefined

Как объясняется в gidi.io, решения на основе Proxy автоматически оборачивают вложенные объекты в новые безопасные прокси, обеспечивая бесшовный доступ к глубинным свойствам без ручных проверок.


Совместимость с браузерами и полифилы

Поддержка опциональной цепочки:

Современные браузеры и Node.js полностью поддерживают опциональную цепочку:

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

Для старых сред можно использовать Babel для транспиляции синтаксиса опциональной цепочки:

bash
npm install --save-dev @babel/core @babel/preset-env
javascript
// .babelrc
{
  "presets": ["@babel/preset-env"]
}

Полифилл для опциональной цепочки:

javascript
// Простой полифилл для опциональной цепочки
if (!Object.prototype.hasOwnProperty.call(Object, 'getPrototypeOf')) {
  Object.prototype.getPrototypeOf = function(obj) {
    return obj.__proto__;
  };
}

// Полифилл опциональной цепочки
function optionalChaining(obj, ...path) {
  let current = obj;
  for (const key of path) {
    if (current == null) return undefined;
    current = current[key];
  }
  return current;
}

// Использование
const result = optionalChaining(test, 'level1', 'level2', 'level3');

Альтернатива Lodash для старых браузеров:

Для проектов, которым нужна поддержка очень старых браузеров, можно использовать лёгкую альтернативу Lodash:

javascript
// Лёгкая функция get
function get(obj, path, defaultValue) {
  const keys = Array.isArray(path) ? path : path.split('.');
  let result = obj;
  
  for (const key of keys) {
    if (result == null) return defaultValue;
    result = result[key];
  }
  
  return result !== undefined ? result : defaultValue;
}

Согласно W3Schools, хотя опциональная цепочка относительно новая, она быстро набирает популярность и теперь поддерживается во всех основных современных браузерах.


Сравнение производительности

Разные подходы имеют разные характеристики производительности:

Прямой доступ к свойству против опциональной цепочки:

javascript
// Настройка
const data = {
  level1: {
    level2: {
      level3: 'value'
    }
  }
};

// Тест производительности
console.time('direct');
for (let i = 0; i < 1000000; i++) {
  try {
    const val = data.level1.level2.level3;
  } catch (e) {}
}
console.timeEnd('direct');

console.time('optional');
for (let i = 0; i < 1000000; i++) {
  const val = data?.level1?.level2?.level3;
}
console.timeEnd('optional');

console.time('lodash');
for (let i = 0; i < 1000000; i++) {
  const val = _.get(data, 'level1.level2.level3');
}
console.timeEnd('lodash');

console.time('custom');
for (let i = 0; i < 1000000; i++) {
  const val = getNestedValue(data, 'level1.level2.level3');
}
console.timeEnd('custom');

Типичные результаты:

  • Прямой доступ: Самый быстрый, когда свойства существуют, но падает, если их нет.
  • Опциональная цепочка: Немного медленнее, чем прямой доступ, но безопасна.
  • Lodash: Умеренная нагрузка из‑за дополнительной функциональности.
  • Собственные функции: Производительность зависит от реализации.

Потребление памяти:

  • Опциональная цепочка: Минимальная нагрузка.
  • Proxy‑решения: Больше памяти из‑за обёртки прокси.
  • Lodash: Дополнительный размер библиотеки (~70 КБ минифицировано).
  • Собственные функции: Минимальный след.

Для большинства приложений различия в производительности незначительны, если только вы не обрабатываете миллионы операций. Как отмечено в 2ality.com, небольшая нагрузка опциональной цепочки обычно оправдана за счёт безопасности и читаемости.


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

1. Используйте опциональную цепочку, когда это возможно

javascript
// Хорошо
const userName = user?.profile?.name;

// Плохо
if (user && user.profile && user.profile.name) {
  const userName = user.profile.name;
}

2. Комбинируйте с оператором объединения с нулём

javascript
// Хорошо
const displayName = user?.profile?.displayName ?? 'Guest';

// Вместо
const displayName = user?.profile?.displayName || 'Guest';

3. Используйте значения по умолчанию разумно

javascript
// Хорошо
const settings = _.get(config, 'app.settings', { theme: 'light' });

// Плохо
let settings;
if (config && config.app && config.app.settings) {
  settings = config.app.settings;
} else {
  settings = { theme: 'light' };
}

4. Рассмотрите TypeScript для типовой безопасности

typescript
interface User {
  profile?: {
    name?: string;
    preferences?: {
      theme?: string;
    };
  };
}

function getTheme(user: User): string {
  return user?.profile?.preferences?.theme ?? 'light';
}

5. Кешируйте сложные шаблоны доступа

javascript
// Вместо многократного доступа
const userName = data?.user?.profile?.name;
const userEmail = data?.user?.profile?.email;
const userAge = data?.user?.profile?.age;

// Кешируйте промежуточные объекты
const userProfile = data?.user?.profile;
const userName = userProfile?.name;
const userEmail = userProfile?.email;
const userAge = userProfile?.age;

6. Обрабатывайте крайние случаи

javascript
// Обрабатывайте null/undefined в массивах
const firstItem = data?.items?.[0] ?? null;

// Обрабатывайте вызовы функций безопасно
const result = data?.process?.() ?? 'default';

Как отмечено в CoreUI, ключ к безопасному доступу к свойствам — понять, что опциональная цепочка делает значение перед ней «опциональным», но не дальше в цепочке.


Заключение

Безопасный доступ к глубоко вложенным свойствам в JavaScript значительно эволюционировал, предлагая несколько элегантных решений, зависящих от требований проекта и среды:

  1. Используйте опциональную цепочку (?.) для современных проектов – это самое лаконичное и читаемое решение, полностью поддерживаемое современными браузерами и Node.js, и должно быть вашим первым выбором для нового кода.
  2. Рассмотрите _.get() из Lodash для сложных приложений – когда вам нужны дополнительные возможности, такие как значения по умолчанию, поддержка массивов, или вы уже используете Lodash.
  3. Реализуйте собственные функции для специфических нужд – когда требуется лёгкое решение без внешних зависимостей или нужна кастомная логика, например, логирование отсутствующих свойств.
  4. Используйте Proxy‑решения для продвинутых случаев – когда нужно автоматически оборачивать все вложенные объекты в безопасные прокси или реализовать дополнительные проверки.
  5. Всегда учитывайте совместимость с браузерами – используйте транспиляцию и полифилы при поддержке старых сред, но отдавайте предпочтение современному синтаксису для будущей совместимости.

Оператор опциональной цепочки представляет собой текущую лучшую практику, предлагая баланс между безопасностью, читаемостью и производительностью. Для большинства приложений он обеспечивает самую чистую и надёжную схему доступа к глубоко вложенным свойствам без громоздких проверок null.

Авторы
Проверено модерацией
Модерация