Лучший способ проверки вложенных свойств в JavaScript
Узнайте лучшие методы безопасного доступа к глубоко вложенным свойствам JavaScript. Откройте возможности optional chaining, Lodash.get и собственных решений.
Как лучше проверить наличие свойств в глубоко вложенных объектах JavaScript?
У меня есть ссылка на объект JavaScript, который может в дальнейшем содержать вложенные объекты:
var test = {};
Объект может выглядеть так:
{level1: {level2: {level3: "level3"}}};
При попытке напрямую обратиться к вложенным свойствам:
alert(test.level1); // yields undefined
alert(test.level1.level2.level3); // throws an error
Мой текущий подход — проверять каждый уровень:
if (test.level1 && test.level1.level2 && test.level1.level2.level3) {
alert(test.level1.level2.level3);
}
Есть ли более эффективный или элегантный способ проверить наличие глубоко вложенных свойств в объектах JavaScript?
Лучший способ проверить глубоко вложенные свойства в JavaScript — использовать оператор опциональной цепочки (?.), введённый в ES2020. Он позволяет безопасно обращаться к вложенным свойствам без ручных проверок null, автоматически возвращая undefined, если любой промежуточный объект не существует. Для сложных сценариев или совместимости с более старыми браузерами можно использовать утилиты, такие как _.get() из Lodash, либо написать собственные вспомогательные функции, которые безопасно обходят пути объекта.
Содержание
- Опциональная цепочка (ES2020)
- Метод
_.get()из Lodash - Собственные вспомогательные функции
- Решения на основе Proxy ES6
- Совместимость с браузерами и полифилы
- Сравнение производительности
- Лучшие практики
Опциональная цепочка (ES2020)
Оператор опциональной цепочки (?.) является самым элегантным и современным решением для безопасного доступа к глубоко вложенным свойствам в JavaScript. Введённый в ES2020, он автоматически обрабатывает случаи, когда промежуточные свойства равны null или undefined, не выбрасывая ошибок.
var test = {};
var level3Value = test?.level1?.level2?.level3; // Возвращает undefined, без ошибки
Ключевые особенности:
- Короткое замыкание: если любое свойство в цепочке равно
nullилиundefined, выражение сразу возвращаетundefined. - Чистый синтаксис: сокращает громоздкие проверки
nullдо одной строки. - Гибкое использование: работает с свойствами, вызовами методов и индексами массивов.
Примеры:
// Базовый доступ к свойству
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() предоставляет надёжное решение для безопасного доступа к вложенным свойствам.
import _ from 'lodash';
var test = {};
var level3Value = _.get(test, 'level1.level2.level3', 'default value');
Преимущества:
- Синтаксис строки пути: доступ к свойствам через строку с точечной нотацией.
- Значения по умолчанию: задаёт запасное значение, если свойства не существует.
- Поддержка массивов: работает с индексами массивов в путях.
- Универсальная совместимость: работает во всех средах 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:
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');
Расширенная функция с поддержкой массивов:
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:
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:
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:
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 для транспиляции синтаксиса опциональной цепочки:
npm install --save-dev @babel/core @babel/preset-env
// .babelrc
{
"presets": ["@babel/preset-env"]
}
Полифилл для опциональной цепочки:
// Простой полифилл для опциональной цепочки
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:
// Лёгкая функция 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, хотя опциональная цепочка относительно новая, она быстро набирает популярность и теперь поддерживается во всех основных современных браузерах.
Сравнение производительности
Разные подходы имеют разные характеристики производительности:
Прямой доступ к свойству против опциональной цепочки:
// Настройка
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. Используйте опциональную цепочку, когда это возможно
// Хорошо
const userName = user?.profile?.name;
// Плохо
if (user && user.profile && user.profile.name) {
const userName = user.profile.name;
}
2. Комбинируйте с оператором объединения с нулём
// Хорошо
const displayName = user?.profile?.displayName ?? 'Guest';
// Вместо
const displayName = user?.profile?.displayName || 'Guest';
3. Используйте значения по умолчанию разумно
// Хорошо
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 для типовой безопасности
interface User {
profile?: {
name?: string;
preferences?: {
theme?: string;
};
};
}
function getTheme(user: User): string {
return user?.profile?.preferences?.theme ?? 'light';
}
5. Кешируйте сложные шаблоны доступа
// Вместо многократного доступа
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. Обрабатывайте крайние случаи
// Обрабатывайте null/undefined в массивах
const firstItem = data?.items?.[0] ?? null;
// Обрабатывайте вызовы функций безопасно
const result = data?.process?.() ?? 'default';
Как отмечено в CoreUI, ключ к безопасному доступу к свойствам — понять, что опциональная цепочка делает значение перед ней «опциональным», но не дальше в цепочке.
Заключение
Безопасный доступ к глубоко вложенным свойствам в JavaScript значительно эволюционировал, предлагая несколько элегантных решений, зависящих от требований проекта и среды:
- Используйте опциональную цепочку (
?.) для современных проектов – это самое лаконичное и читаемое решение, полностью поддерживаемое современными браузерами и Node.js, и должно быть вашим первым выбором для нового кода. - Рассмотрите
_.get()из Lodash для сложных приложений – когда вам нужны дополнительные возможности, такие как значения по умолчанию, поддержка массивов, или вы уже используете Lodash. - Реализуйте собственные функции для специфических нужд – когда требуется лёгкое решение без внешних зависимостей или нужна кастомная логика, например, логирование отсутствующих свойств.
- Используйте Proxy‑решения для продвинутых случаев – когда нужно автоматически оборачивать все вложенные объекты в безопасные прокси или реализовать дополнительные проверки.
- Всегда учитывайте совместимость с браузерами – используйте транспиляцию и полифилы при поддержке старых сред, но отдавайте предпочтение современному синтаксису для будущей совместимости.
Оператор опциональной цепочки представляет собой текущую лучшую практику, предлагая баланс между безопасностью, читаемостью и производительностью. Для большинства приложений он обеспечивает самую чистую и надёжную схему доступа к глубоко вложенным свойствам без громоздких проверок null.