Автору материала, перевод которого мы сегодня публикуем, недавно попался один вопрос на StackOverflow, который заставил его задуматься об обработке значений null и undefined в JavaScript. Здесь он приводит анализ текущей ситуации, показывает некоторые приёмы безопасной работы с null и undefined, а также, говоря о будущем, рассматривает оператор ?..

image

Основные сведения


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)


  1. Drag13
    30.05.2018 13:17

    В общем халявы нет.

    Решение с && имеет сложности с чтением и имеет тенденцию к разрастанию, что еще больше усугубляет первую проблему.

    Решение с методом _.get(object, path, [defaultValue]) не идиоматично, так как чтение свойства происходит через функцию, что тоже усложняет понимание кода.

    Решение с proxy кажется самым хорошим но у прокси есть (были?) проблемы с оптимизацией. V8 плотно занималась этим вопросом но даже они (насколько я читал) не довели его до конца. И если в обычной жизни это не проблема, то обращения к свойствам происходят часто, и часто в циклах. Т.е. по перформансу может ударить очень прилично. Хотя, хотя, Vue собираются использовать именно proxy для мониторинга изменения значения объектов. Так что возможно все не так и плохо.


  1. biziwalker
    30.05.2018 15:16
    +1

    В ожидании включения предложения «optional chaining» в EcmaScript комитетом TC39, можно пользоваться очень удобной библиотекой, которая оборачивает опасный доступ ко вложенному значению с помощью магии в виде try/catch + regex.

    github.com/facebookincubator/idx


    1. justboris
      30.05.2018 19:52

      Важное дополнение: в комплекте также идет babel-plugin, который позволяет превратить эту конструкцию в набор тернарных операторов, чтобы избавиться от try/catch в продакшене.


  1. paratagas
    30.05.2018 15:37

    Частично могут помочь появившиеся в ES6 параметры функций по умолчанию. С их использованием по крайней мере внутри функций уже не нужно делать подобные проверки. Ну а из предложенного — optional chaining выглядит вполне достойно.


  1. mayorovp
    30.05.2018 16:44
    +2

    Частично помогает Typescript в строгом режиме. По опыту, в большинстве случаев проверка на undefined/null делается не потому что там и в самом деле может не оказаться объекта — а просто на всякий случай. Статическая типизация способна значительно уменьшить количество «всяких случаев».


  1. VolCh
    31.05.2018 00:40

    Почему самой нелюбимой? Вполне любимая, если смотреть на то, что функции возвращают.


  1. Arbane
    31.05.2018 03:41

    Хороший вариант, годный. Но! Вариант с вопросом тоже избыточный, на мой взгляд. Вот смотрите:

    a = obj?.prop1?.prop2.?prop3?;

    А теперь, часто вам попадается такой; вариант:

    a = obj?.prop1?.prop2.prop3?;

    Наверное нечасто! Тем, кто не заметил: идет обращение к свойству свойства prop2 без контроля на null.

    Я предложил бы писать так:

    a = ?obj.prop1.prop2.prop3;

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


  1. torbasow
    31.05.2018 09:21

    Мне кажется, Вы не вполне точно изложили семантику null-а. В спецификации сказано: primitive value that represents the intentional absence of any object value.


  1. agentx001
    31.05.2018 10:37
    -1

    Мне кажется, правильнее всего просто избегать подобных проблем:

    let obj = {};
    console.log(obj.someProp); //ошибки не будет
    


    Ну или если мы совсем не влияем на то что к нам приходит:
    if (typeof obj === "object") {
        console.log(obj.someProp); //ошибки не будет
    } else {
        ...
    }
    


  1. 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 принимает один аргумент


  1. sentyaev
    31.05.2018 23:26

    Вообще конструкция вида a = b.c.d.e.f — это code smell. У нас в команде это не пройдет code review. Придется переделывать.
    Я даже сходу не могу придумать вариант когда мне нужно больше чем a.b.c сделать, да и это я скорее всего заменю на a.some()


  1. Riim
    02.06.2018 17:12

    ((obj.prop1 || {}).prop2 || {}).prop3