Undefined в JavaScript: погружаемся в бездну

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

Говоря о примитивных типах данных в JavaScript, большинство имеет в виду самые основные из них: String, Number и Boolean. Эти примитивы достаточно предсказуемы, обычно они работают так, как от них и ожидается. Однако речь в этой статье пойдет о менее обыденном типе данных — Undefined. Необычном, непонятном и в некотором смысле даже ужасном.

Содержание:
1. Концепция undefined
2. Что такое undefined в JavaScript?
3. Как работать с undefined?
4. Все еще страннее для массивов
5. Скрытый тормоз
Избегаем неопределенностей (Заключение)

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

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

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

В этом можно провести параллель с особым объектом null, который мы рассматривали ранее («Null в JavaScript: как не сломать себе шею на ровном месте»). Однако смысл и значение undefined отличаются. Как — мы рассмотрим дальше.

2. Что такое undefined в JavaScript?

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

Формально говоря, в JavaScript нет задекларированных и неопределенных переменных, так как в момент декларации им формально присваивается значение undefined. Нам — не англоговорящим — это понять легче, так как для нас undefined — это явно другое слово, нежели «неопределенный». А вот для англоговорящих тут может возникнуть путаница, так как на уровне формальной логики это выглядит как:

В JS для обозначения состояния неопределенного значения используется специальный тип глобальной переменной, определенный как «неопределенный» со значением, определенным как «неопределенное».

(На самом деле для англоязычных все еще запутаннее, так как в случае попытки использовать незадекларированный идентификатор получается ошибка «... is not defined», что по смыслу аналогично undefined, но по сути отражает совершенно другое состояние, которое к переменной undefined отношения не имеет).

Проверим в консоли браузера:

typeof undefined ===  ‘undefined’    // true

а поскольку все глобальные переменные являются свойствами глобального объекта window, то

undefined in window // true

И поскольку тип специфичен, то строгое сравнение истинно только при сравнении с самим собой (в этом он похож на null).

undefined ===  undefined    // true
undefined ===  false        // false
undefined ===  null        // false
undefined ===  ‘’        // false

Ну и надо обязательно сказать, что тип undefined — это примитив, который не имеет никаких признаков объекта то есть каких-либо свойств, которые можно адресовать. Есть только тип и значение. То есть любое действие, подобное

undefined.property, вызовет ошибку TypeError.

Особо пытливые могут заметить, что если уж undefined — это глобальная переменная (свойство window) и не константа так как была введена еще до ES6, то может возникнуть соблазн изменить ее значение. Ну что ж, попробуем:

window.undefined = 1; // 1, ошибки нет. Значит, не константа

window.undefined // undefined. Значит, значение не изменилось.

Вот такая себе неизменяемая переменная с особым типом. В отличие от null, который является специальным объектом, а не специальным типом.

3. Как работать с undefined?

Существует много споров, является гибкость и «толерантность» JavaScript его преимуществом или проклятием. Адепты строгих языков хватаются за голову при виде кода, позволяющего делать вызов неопределенной функции, а фронтендщики сладострастно потирают руки —

«ого, чо мы теперь можем мутить, теперь можем менять функции на ходу».

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

Давайте сначала посмотрим, во что может конвертироваться undefined.

Boolean(undefined)        // false
Number(undefined)        // NaN
String(undefined)        // 'undefined'

Это значит, что в логических выражениях undefined будет работать как ложь (что очень удобно), в числовых — сделает результат NaN (что часто доставляет неудобства), а в строковых — добавит обычно нежелательный текст.

let x;
x || false         // false
x + 5                // NaN
x + ‘ mess’        // 'undefined mess'

Также необходимо помнить, что:

undefined == null    // true
undefined == 0        // false
undefined == ''        // false

и неочевидное

undefined == false    // false

Такое неоднозначное поведение заставляет добавлять проверки на undefined везде, где теоретически возможно появление такого значения. Вот вам и плата за гибкость.

«Ну и что, — возразите вы, — разве сложно добавить проверки переменных?».

В целом не сложно, даже если учитывать замусоривание кода рутинными операциями. Однако этот же тип используется как индикатор отсутствия адресуемого поля объекта или индекса массива. И вот тут уже начинаются сложности, поскольку у объектов может быть много свойств.

Они не объявляются как переменные, они могут добавляться/удаляться по ходу дела, а проверять каждый раз наличие конкретного свойства затратно, плюс — свойство может само по себе иметь значение undefined, плюс — имя свойства может быть ‘undefinedкак в объекте window, но вполне уже изменяемое:

const x = {};
x.something         // undefined
x.undefined         // undefined
undefined in x         // false
x.undefined = 5;
x.undefined         // 5
undefined in x         // true

Самое неприятное в этом то, что если вы ожидаете объект по какому-то свойству, а он отсутствует, то попытка адресовать ожидаемое свойство отсутствующего объекта вызовет ошибку типа:

const x = {};
x.something.any    //TypeError: Cannot read properties of undefined

И получается, что вместо очень удобной записи цепочки свойств

x.something.any.value

приходится до свойства value добираться в несколько итераций. К счастью, в стандарт языка уже добавлен оператор опциональной последовательности ?. свежие версии всех браузеров его поддерживают, кроме Internet Explorer, который возвращает изящность адресации возможно отсутствующих объектов:

x?.something?.any?.value

Такая запись уже не вызовет сбоя, если свойство something или any оказалось undefined или null. Однако злоупотреблять таким оператором не стоит, поскольку надо помнить о дополнительных проверках у него «под капотом», что делает его медленнее стандартной адресации свойств.

4. Все еще страннее для массивов

Если для объектов отсутствие свойства означает, что оно буквально отсутствует и не может быть найдено в списке ключей, то у массива есть три варианта неопределенности:

  1. если индекс выходит за пределы размера массива;
  2. если индекс находится в пределах размера массива, но элемент не инициализирован. Такой элемент называется пустым слотом (empty);
  3. если индекс находится в пределах размера массива, и элемент имеет значение undefined.

Во всех трех случаях обращение по индексу вернет значение undefined. Однако в методах массивов пустые слоты ведут себя иначе. В отличие от декларируемых переменных элементы массивов не имеют по умолчанию ссылки на глобальную переменную undefined, но имеют зарезервированное место.

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

Получается еще один «undefined», но не тот, что undefined, и не тот, что «not defined»надеюсь, вы понимаете логику JavaScript.

Например:
const x = Array(2);    // x -> [empty × 2]
x.length            // 2
x[0]                // undefined
x[1]                // undefined
x[1] = undefined        // x -> [empty, undefined]
x.map(d => +d)        // [empty, NaN]

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

5. Скрытый тормоз

Есть с undefined типом неочевидная и в разных браузерах по-разному работающая вещь: это скорость выборки из объекта или массива.

Что не так с объектами: когда вы запрашиваете несуществующее свойство объекта, то чтобы вернуть значение undefined, движку придется проверить все существующие собственные свойства объекта, потом свойства его прототипа, потом прототипа прототипа и так до последней «инстанции» — типа Object. Если количество свойств объекта велико, задержка может быть очень существенной. Это также надо помнить и с оператором опциональной последовательности, который был упомянут выше.

Что не так с массивами: тут эффект менее очевиден, поскольку выборка идет по индексу и, казалось бы, какая разница, какое в массиве значение. Но современные JavaScript-движки кроме компиляции кода занимаются еще и его оптимизацией, в том числе и во время выполнения.

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

Избегаем неопределенностей (Заключение)

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

Делать это можно несколькими путями.

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

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

const x = ‘значение’;

Отказаться от объявления переменных через var в пользу блоковых let. Так проще контролировать их использованиеда и на быстродействии сказывается. Mozilla, например, прямо рекомендует не использовать var.

for (let prop in obj) { … }

В функциях пользоваться параметрами по умолчанию.

function x(data = ‘значение’) { … }

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

function x(data = {}) {
const { y = ‘значение’ } = data;
…
}

И, конечно, не забывать возможность рассматривать отсутствие свойства как ошибку, и превентивно обрабатывать ее через try/catch.

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

Обучение 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