Настоящее и будущее безопасной работы с null и undefined в JavaScript

https://dev.to/pichardoj/optional-null-safe-in-javascript-1b7k
  • Перевод
Автору материала, перевод которого мы сегодня публикуем, недавно попался один вопрос на 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 нравится вам больше всего?

RUVDS.com

726,49

RUVDS – хостинг VDS/VPS серверов

Поделиться публикацией
Комментарии 15
    0
    В общем халявы нет.

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

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

    Решение с proxy кажется самым хорошим но у прокси есть (были?) проблемы с оптимизацией. V8 плотно занималась этим вопросом но даже они (насколько я читал) не довели его до конца. И если в обычной жизни это не проблема, то обращения к свойствам происходят часто, и часто в циклах. Т.е. по перформансу может ударить очень прилично. Хотя, хотя, Vue собираются использовать именно proxy для мониторинга изменения значения объектов. Так что возможно все не так и плохо.
      +1
      В ожидании включения предложения «optional chaining» в EcmaScript комитетом TC39, можно пользоваться очень удобной библиотекой, которая оборачивает опасный доступ ко вложенному значению с помощью магии в виде try/catch + regex.

      github.com/facebookincubator/idx
        0

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

        0
        Частично могут помочь появившиеся в ES6 параметры функций по умолчанию. С их использованием по крайней мере внутри функций уже не нужно делать подобные проверки. Ну а из предложенного — optional chaining выглядит вполне достойно.
          +2
          Частично помогает Typescript в строгом режиме. По опыту, в большинстве случаев проверка на undefined/null делается не потому что там и в самом деле может не оказаться объекта — а просто на всякий случай. Статическая типизация способна значительно уменьшить количество «всяких случаев».
            0
            Почему самой нелюбимой? Вполне любимая, если смотреть на то, что функции возвращают.
              +1
              Хороший вариант, годный. Но! Вариант с вопросом тоже избыточный, на мой взгляд. Вот смотрите:

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

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

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

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

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

              a = ?obj.prop1.prop2.prop3;

              То есть контролировать все этапы получения значения в выражении, перед которым условный символ, скажем наш ?, причём для странных ситуаций, где в середине выражения проверки не нужны, скобки помогут.
                0
                Мне кажется, Вы не вполне точно изложили семантику null-а. В спецификации сказано: primitive value that represents the intentional absence of any object value.
                  –2
                  Мне кажется, правильнее всего просто избегать подобных проблем:
                  let obj = {};
                  console.log(obj.someProp); //ошибки не будет
                  


                  Ну или если мы совсем не влияем на то что к нам приходит:
                  if (typeof obj === "object") {
                      console.log(obj.someProp); //ошибки не будет
                  } else {
                      ...
                  }
                  
                    +1
                    Код с прокси не очень корректный. Если, например, требуемое конечное значение — объект, то у нас вернется проксированный вариант, возвращающий дефолтное значение в случае отсутствия свойства

                    ...
                    const obj = {
                      items: [{ someObj: {} }]
                    };
                    
                    const someObj = optional(obj, target => target.items[0].someObj, "def");
                    console.log(someObj.someProp) // "def"
                    


                    ну и вызов proxify со вторым аргументом не нужен — proxify принимает один аргумент
                      0
                      Вообще конструкция вида a = b.c.d.e.f — это code smell. У нас в команде это не пройдет code review. Придется переделывать.
                      Я даже сходу не могу придумать вариант когда мне нужно больше чем a.b.c сделать, да и это я скорее всего заменю на a.some()
                        –2
                        ((obj.prop1 || {}).prop2 || {}).prop3
                          0
                          За что минусы?
                            +2

                            Куча скобочек из ниоткуда, превращающих чтение кода в игру "найди пару". Плюс false, 0 и пустая строка превращаются в пустой объект, что может оказаться багом. А уж вызов метода в таком стиле выглядит еще страшнее:


                            (obj.prop1 || (() => {}))();
                              0
                              false, 0 и пустая строка превращаются в пустой объект

                              На практика в большинстве случаев можно расчитывать, что на месте prop1 или prop2 может быть только объект, null и undefined, а prop3 уже не заменяется.


                              А уж вызов метода в таком стиле выглядит еще страшнее

                              я и не говорил, что решение универсальное, в каких-то случаях подходит, в каких-то нет. Скажем a?.b я бы на сегодняшний день записал как a && a.b, а вот конструкцию a.b.c.d?.e однозначно как (a.b.c.d || {}).e, вариант a.b.c.d && a.b.c.d.e при не односимвольных именах будет заметно длиннее.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое