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
Странный вопрос, не правда ли? Как может иметь значение то, что означает отсутствие значения? Но, тут есть два момента:
- «Значение, обозначающее отсутствие значения» — само по себе значение. А значит должен быть способ убедиться в том, что есть конкретно это значение, а не какое-либо другое.
- В 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
для индексов и т.п. Этим вы облегчите жизнь не только себе, но и другим, кто будет использовать ваш интерфейс.
Кто для закрепления хочет посмотреть видео, вот тематические ссылки:
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: