null
и undefined
в JavaScript. Здесь он приводит анализ текущей ситуации, показывает некоторые приёмы безопасной работы с null
и undefined
, а также, говоря о будущем, рассматривает оператор ?.
.Основные сведения
Undefined
— это примитивное значение, которое автоматически назначается объявленным, но неинициализированным переменным. Это значение можно получить, обратившись к несуществующему свойству объекта или к несуществующему аргументу функции.Null
— это ещё одно примитивное значение, которое представляет собой отсутствие значения. Например, если переменной присвоено значение null
, это можно трактовать как то, что на данном этапе работы программы эта переменная существует, но у неё пока нет ни типа, ни значения.Посмотрим, что произойдёт, если выполнить такой код:
let obj;
console.log(obj.someProp);
Он выдаст следующую ошибку:
TypeError: Cannot read property 'someProp' of undefined
Похожее можно видеть и при попытках работы с переменными, имеющими значение
null
.Проверка на null и undefined
Как избежать подобных явлений? К счастью для нас, JavaScript поддерживает вычисление логических выражений по сокращённой схеме (short-circuit evaluation). Это означает, что для того, чтобы избежать вышеописанной ошибки
TypeError
, можно написать следующий код:let obj;
console.log(obj && obj.someProp); // вывод undefined
Но что если нужно пойти глубже, например, добраться до чего-то вроде
obj.prop1.prop2.prop3
? В подобной ситуации можно попытаться решить задачу, выполнив множество проверок:console.log( obj && obj.prop1 && obj.prop1.prop2 && obj.prop1.prop2.prop3 );
Хотя это и работает, смотрятся такие конструкции не очень-то хорошо.
А что если нам нужно выводить некое стандартное значение, если в подобной цепочке встречается
undefined
или null
? Это возможно, но тогда кода придётся писать ещё больше:const evaluation = obj && obj.prop1 && obj.prop1.prop2 && obj.prop1.prop2.prop3;
console.log( evaluation != null ? evaluation : "SomeDefaultValue" );
Прежде чем продолжить размышления о
null
и undefined
в JavaScript, поговорим о том, как подобные значения обрабатываются в других языках.Другие языки
Надо отметить, что проблема работы с неопределёнными значениями присутствует в большинстве языков программирования. Посмотрим, как проводятся проверки, подобные вышеописанным, в других языках.
?Java
В Java есть api
Optional
:SomeClass object;
Optional.ofNullable(object)
.map(obj -> obj.prop1)
.map(obj -> obj.prop2)
.map(obj -> obj.prop3)
.orElse("SomeDefaultValue");
?Kotlin
В Kotlin (он, как и Java, использует JVM) существуют операторы elvis (
?:
) и safe-call (?.
).val object: SomeClass?
object?.prop1?.prop2?.prop3 ?: "SomeDefaultValue";
?C#
И, наконец, в C# есть операторы null-condition (
?.
), и null-coalescing (??
).SomeClass object;
object?.prop1?.prop2?.prop3 ?? "SomeDefaultValue";
Как работать с undefined в JS?
После рассмотрения возможностей по работе с неопределёнными значениями в других языках, я задался вопросом о том, есть ли возможность безопасно работать с
undefined
в JavaScript и при этом не писать километры кода. Поэтому я приступил к экспериментам с регулярными выражениями. Мне хотелось создать функцию, которая позволила бы безопасно получать доступ к свойствам объектов.function optionalAccess(obj, path, def) {
const propNames = path.replace(/\]|\)/, "").split(/\.|\[|\(/);
return propNames.reduce((acc, prop) => acc[prop] || def, obj);
}
const obj = {
items: [{ hello: "Hello" }]
};
console.log(optionalAccess(obj, "items[0].hello", "def")); // Вывод Hello
console.log(optionalAccess(obj, "items[0].he", "def")); // Вывод def
После того, как я создал эту функцию, я узнал о методе
lodash._get
, который имеет такую же сигнатуру:_.get(object, path, [defaultValue])
Однако, честно говоря, я не отношусь к фанатам строковых путей, поэтому я начал искать способ, который позволил бы избежать их использования. В результате я создал решение, основанное на прокси-объектах:
// Именно здесь происходит всё самое интересное
function optional(obj, evalFunc, def) {
// Обработчик прокси
const handler = {
// Перехват всех операций доступа к свойствам
get: function(target, prop, receiver) {
const res = Reflect.get(...arguments);
//Если наш ответ является объектом, обернём его в прокси, иначе - просто вернём
return typeof res === "object" ? proxify(res) : res != null ? res : def;
}
};
const proxify = target => {
return new Proxy(target, handler);
};
// Вызовем функцию с проксированным объектом
return evalFunc(proxify(obj, handler));
}
const obj = {
items: [{ hello: "Hello" }]
};
console.log(optional(obj, target => target.items[0].hello, "def")); // => Hello
console.log(optional(obj, target => target.items[0].hell, { a: 1 })); // => { a: 1 }
Будущее безопасной работы с null и undefined в JavaScript
Сейчас в комитете TC39 имеется предложение, которое позволяет пользоваться следующими конструкциями:
obj?.arrayProp?[0]?.someProp?.someFunc?.();
По мне, так выглядит это очень аккуратно. Однако это предложение пока находится на достаточно ранней стадии согласования, то есть, его потенциальное появление в языке может занять некоторое время. Но, несмотря на это, существует плагин для babel, который позволяет уже сегодня пользоваться подобными конструкциями.
Итоги
Значения вроде
null
и undefined
присутствуют в программировании уже очень давно, и ничто не указывает на то, что они в скором времени исчезнут. Вероятно, концепция значения null
является самой нелюбимой в программировании, однако у нас есть средства для обеспечения безопасной работы с подобными значениями. В том, что касается работы с null
и undefined
в JavaScript, можно воспользоваться несколькими подходами рассмотренными выше. В частности, вот код функций, предложенных автором этого материала. Если вас заинтересовал оператор ?.
, появление которого ожидается в JS, взгляните на одну из наших предыдущих публикаций, в которой затрагивается этот вопрос.Уважаемые читатели! Какой из упомянутых в этом материале способов безопасной работы с null и undefined в JavaScript нравится вам больше всего?
Комментарии (12)
biziwalker
30.05.2018 15:16+1В ожидании включения предложения «optional chaining» в EcmaScript комитетом TC39, можно пользоваться очень удобной библиотекой, которая оборачивает опасный доступ ко вложенному значению с помощью магии в виде try/catch + regex.
github.com/facebookincubator/idxjustboris
30.05.2018 19:52Важное дополнение: в комплекте также идет babel-plugin, который позволяет превратить эту конструкцию в набор тернарных операторов, чтобы избавиться от try/catch в продакшене.
paratagas
30.05.2018 15:37Частично могут помочь появившиеся в ES6 параметры функций по умолчанию. С их использованием по крайней мере внутри функций уже не нужно делать подобные проверки. Ну а из предложенного — optional chaining выглядит вполне достойно.
mayorovp
30.05.2018 16:44+2Частично помогает Typescript в строгом режиме. По опыту, в большинстве случаев проверка на undefined/null делается не потому что там и в самом деле может не оказаться объекта — а просто на всякий случай. Статическая типизация способна значительно уменьшить количество «всяких случаев».
VolCh
31.05.2018 00:40Почему самой нелюбимой? Вполне любимая, если смотреть на то, что функции возвращают.
Arbane
31.05.2018 03:41Хороший вариант, годный. Но! Вариант с вопросом тоже избыточный, на мой взгляд. Вот смотрите:
a = obj?.prop1?.prop2.?prop3?;
А теперь, часто вам попадается такой; вариант:
a = obj?.prop1?.prop2.prop3?;
Наверное нечасто! Тем, кто не заметил: идет обращение к свойству свойства prop2 без контроля на null.
Я предложил бы писать так:
a = ?obj.prop1.prop2.prop3;
То есть контролировать все этапы получения значения в выражении, перед которым условный символ, скажем наш ?, причём для странных ситуаций, где в середине выражения проверки не нужны, скобки помогут.
agentx001
31.05.2018 10:37-1Мне кажется, правильнее всего просто избегать подобных проблем:
let obj = {}; console.log(obj.someProp); //ошибки не будет
Ну или если мы совсем не влияем на то что к нам приходит:
if (typeof obj === "object") { console.log(obj.someProp); //ошибки не будет } else { ... }
sp3ber
31.05.2018 19:44+1Код с прокси не очень корректный. Если, например, требуемое конечное значение — объект, то у нас вернется проксированный вариант, возвращающий дефолтное значение в случае отсутствия свойства
... const obj = { items: [{ someObj: {} }] }; const someObj = optional(obj, target => target.items[0].someObj, "def"); console.log(someObj.someProp) // "def"
ну и вызов proxify со вторым аргументом не нужен — proxify принимает один аргумент
sentyaev
31.05.2018 23:26Вообще конструкция вида
a = b.c.d.e.f
— это code smell. У нас в команде это не пройдет code review. Придется переделывать.
Я даже сходу не могу придумать вариант когда мне нужно больше чемa.b.c
сделать, да и это я скорее всего заменю наa.some()
Drag13
В общем халявы нет.
Решение с && имеет сложности с чтением и имеет тенденцию к разрастанию, что еще больше усугубляет первую проблему.
Решение с методом _.get(object, path, [defaultValue]) не идиоматично, так как чтение свойства происходит через функцию, что тоже усложняет понимание кода.
Решение с proxy кажется самым хорошим но у прокси есть (были?) проблемы с оптимизацией. V8 плотно занималась этим вопросом но даже они (насколько я читал) не довели его до конца. И если в обычной жизни это не проблема, то обращения к свойствам происходят часто, и часто в циклах. Т.е. по перформансу может ударить очень прилично. Хотя, хотя, Vue собираются использовать именно proxy для мониторинга изменения значения объектов. Так что возможно все не так и плохо.