Null в JavaScript: как не сломать себе шею на ровном месте

Андрей Худотеплый

Ноль, ничего, отсутствует, пусто — в нашей повседневной жизни это слова-синонимы, обозначающие примерно одно и то же. Но в программировании с этим есть нюансы, о которых мы мало задумываемся в обыденной жизни. Например, как выразить то, что отсутствует вообще, или что было, но теперь его нет? Если мы предполагаем, что что-то должно быть, а его пока нет? Что изменится, если это можно было бы посчитать? А если нет? И это не исчерпывающий список возможных вариантов для «типизации» пустоты.

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

1. Концепция null

Итак, у нас в распоряжении есть указатель (имя), тип и значение. Какие их состояния могут описывать отсутствие чего-то? Классически — это когда у типа данных есть значение, обозначающее «ничего». Как 0 у чисел и пустая строка у строк. Но давайте порассуждаем, что может быть за рамками классических значений.

Например, если отсутствует указатель, можно ли это как-то использовать в коде? В целом нет, поскольку компилятор выдаст ошибку и код не запустится. Даже учитывая, что в JavaScript код компилируется на лету, ошибка (not defined) случится на этапе выполнения, что не может быть правильно работающим кодом.

А если отсутствует тип? Во многих языках это также может привести к ошибке компиляции. Но в JavaScript отсутствие типа данных обычно не ошибка, поскольку тип определяется при присвоении значения. Это состояние имеет отдельный тип — undefined.

var x;
typeof x     // undefined

И такой же тип — undefined — будет у переменной в JavaScript до того момента, пока ее значение не будет присвоено (проинициализировано).

Строго говоря, в JavaScript не возникает ситуации, когда у идентификатора переменной абсолютно отсутствует тип или значение, поскольку undefined — это глобальный идентификатор с типом undefined и единственным значением undefined. Просто интерпретируется это как отсутствие какой-либо информации о типе и значении.

Но undefined — не всегда достаточный способ показать фактическое (плановое) отсутствие какого-либо значения. Для этих целей и была введена концепция null — как особого объекта, имеющего только одно возможное значение, и обозначающего отсутствие какого-либо типа и значения в принципе хотя часто упоминается, что в JavaScript null был введен для обозначения отсутствующего объекта. То есть с одной стороны мы получаем «правильно» декларированную и инициализированную переменную, а с другой — ее значение означает отсутствие значения.

Тут надо сказать, что JavaScript этим не исчерпывает состояния неопределенного типа, но о них надо говорить отдельно. Узнать эту информацию можно на специальных курсах от наших друзей из Mate Academy.

2. Что такое null в JavaScript

Концепция null поддерживается во многих языках. JavaScript не исключение. Однако в JavaScript есть одна историческая особенность: основной тип данных в этом языке — объект. Тип «объект» кодируется нулем. Это создает сложности с использованием null, как это делается в других языках, где 0 — это отсутствие типа. Поэтому для совместимости с «легаси» было принято решение создать особый тип объекта, который и будет подставляться как значение в случаях, где предполагается отсутствие и типа, и значения.

Попробуйте набрать в консоли браузера, чтобы убедиться, что null — это объект:

typeof null     // object

Но если попытаться найти шаблон или хоть какие-то атрибуты объекта null, то этого у вас не получится сделать. Null не имеет никаких видимых дополнительных свойств, кроме своего значения, как объекта.

Это легко проверить, попробовав его конвертировать в строку стандартное свойство Object:

null.toString()

Получается ошибка:

Uncaught TypeError: Cannot read properties of null (reading 'toString')

Отсутствие свойств можно также проверить, подставив null как шаблон для нового объекта:

const o = Object.create(null);

Проинспектировав o, вы увидите, что это пустой объект без каких-либо наследуемых свойств.

То есть с одной стороны null — это объект, но с другой — объект не стандартного типа Object. Интересная ситуация, но и это еще не все.

3. Значение null в JavaScript

Странный вопрос, не правда ли? Как может иметь значение то, что означает отсутствие значения? Но, тут есть два момента:

  1. «Значение, обозначающее отсутствие значения» — само по себе значение. А значит должен быть способ убедиться в том, что есть конкретно это значение, а не какое-либо другое.
  2. В JavaScript широко используется неявная конвертация типов, и в различных выражениях мы вместо null будем автоматически получать другие типы, а значит, и другие значения что может многих неприятно удивить.

Попробуем выяснить, во что может конвертироваться null. По идее, такой объект должен конвертироваться в значения других типов, обозначающие отсутствующие величины. Для этого в консоли браузера выполним:

Boolean(null)         // false
Number(null)         // 0

Пока все логично. Неочевидно, но тоже логично:

isNaN(null)         // false (null конвертируется в 0)

Но:

Object(null)         // {} Object

Объект логично пустой, но это уже стандартный объект со всеми наследуемыми от типа Object свойствами, а не тот пустой объект, что получился при

Object.create(null).

Пока все выглядит логично, но давайте попробуем вот это:

String(null)         // 'null'

Ээээ…. Что? У типа «строка» есть свое пустое значение — ‘’. Логично было бы преобразовать null в такую пустую строку, но имеем строку с вполне конкретным и непустым значением. Логика тут есть, но она другая и выходит из особенностей работы с объектами JavaScript, а не из особенностей null, потому мы не будем сейчас углубляться в это. Просто пока надо запомнить, что в случае конвертации null в строку получим не то, что означает отсутствие значения.

Дальше — интереснее. Если мы попытаемся использовать null как аргумент в конструкторах структур данных, то получим следующее:

Array(null)     // [null]

хотя

Array(0)         // []

то есть тут null не конвертируется в 0.

new Set(null)     // Set(0) {size: 0}
new Map(null)     // Map(0) {size: 0}

хотя конструкторы и Set, и Map ожидают в качестве аргумента итерируемые объекты.

Наверное, достаточно экспериментов, чтобы понять, что null не так прост, как это видится с первого взгляда, и что надо копнуть этого зверя глубже.

4. Как определить наличие значения null у переменной?

Итак. Упомянутое выше «значение, обозначающее отсутствие значения» должно подразумевать способ его проверки. Поскольку null — это особый объект, то он не может быть правильно определен операторами typeof или instanceof. Точнее,

typeof x === 'object'

не дает нам достаточной информации, чтобы утверждать, что объект типа null, а команда

x intanceof null

выдаст ошибку, поскольку null — это не класс объекта.

Остается попробовать сравнение без приведения типов:

x === null

проверим как

null === null         // true

что предполагает рассмотрение null как примитива.

Итак, единственно правильным методом выяснения значения null является строгое сравнение переменной с объектом null:

x === null

Строго говоря, это не единственный способ. Можно еще использовать Object.is:

Object.is(null, null) // true

Но последний метод рекомендуется использовать только лишь в случаях реальной необходимости.

5. Null и все-все-все

Что еще было бы неплохо знать о null, есть ли там что-то еще глубже?

Мы уже знаем, что строгое сравнение null с чем-либо кроме себя, будет давать ложь. Также мы выяснили, в какие значения других типов может конвертироваться null. Но в каких случаях будет работать такая конвертация? Неожиданный вопрос, не правда ли? Давайте попробуем выполнить некоторые нестрогие сравнения:

null == undefined     // true

Обе части — примитивы, обозначающие отсутствие значения. Логично, что в нестрогом сравнении они должны быть равны по смыслу. Но теперь давайте попробуем сравнить с нулем:

0 == null         // false

Логично предположить, что null конвертируется в 0, поскольку Number(null) === 0, по той же схеме, как происходит конвертация типов в 0 == ‘0’ (true). Однако значение выражения 0 == null — ложь! Упс… Неожиданно, правда? А следующее будет не только неожиданно, но и вообще многим унесет крышу в попытке понять это:

null == 0; // false
null < 0;  // false
null > 0; // false
null >= 0; // true (!)
null <= 0; // true (!)

А? Как вам финт от JavaScript? Но это не ошибка. Все укладывается в спецификации ECMA. А все, что задокументировано — уже не баг, а фича 😉

И уж после этого вполне «нормально» воспринимается следующее:

null == ‘’     // false
null == NaN     // false

поскольку при сравнении вызывается оператор ToPrimitive, который в данном случае null не преобразовывает в другие типы.

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

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

6. А может не надо?

Согласитесь, в JavaScript простая в своей идее концепция объекта, обозначающего отсутствие объекта, вылилась в довольно противоречиво-витиеватую схему, требующую держать в голове много условий. Если отбросить «легаси», где null появляется исторически, так ли необходимо и оправдано его использование в ООП? Насколько часто появляются ситуации, когда условия бизнес-логики не позволяют определить тип используемых или ожидаемых данных?

Многие именитые гуру программирования и авторы популярных книг прямо советуют избегать использования null не столько по причине «особенности» этого объекта, сколько по причинам архитектурным. Например, состояния неопределенности требуют дополнительной обработки, а в случае с null также являются источниками ошибок исключений.

И потому там, где неопределенности можно избежать, ее желательно избегать, применив какой-то другой способ обозначения «пустого» значения, чтобы выбранный тип данных совпадал с ожидаемым. Например, объект с дефолтными свойствами для конструкторов, -1 для индексов и т.п. Этим вы облегчите жизнь не только себе, но и другим, кто будет использовать ваш интерфейс.

Кто для закрепления хочет посмотреть видео, вот тематические ссылки:

 

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023