Исследование бездны null и undefined в JavaScript

Шапка

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

Понимание null и undefined

В JavaScript null — это литерал и ключевое слово языка, которое представляет собой отсутствие какого-либо объекта. Другими словами, null указывает «в никуда». В свою очередь, хоть и похожий по смыслу undefined, олицетворяет отсутствие значения как такового. Оба абсолютно неизменны, не имеют свойств и методов и не способны их иметь. Фактически, попытка обратиться к какому-нибудь свойству, или присвоить его, приведёт к ошибке TypeError. Оба этих примитива, как намекают их имена, совершенно лишены значений.

Это самое отсутствие значения приводит к тому, что они считаются ложными, в том смысле, что они приводятся к false если используются в качестве условия, например, в конструкции if. А если сравнить null и undefined с другими ложными значениями при помощи оператора нестрогого сравнения (==), то окажется, что они не равны ничему, кроме самих себя:

null == 0; // false
undefined == ""; // false
null == false; // false
undefined == false; // false
null == undefined; // true

Несмотря на эти сходства, null и undefined не эквивалентны. Каждый из них является представителем своего типа: undefined — представляет тип Undefined, а null, соответственно — тип Null. Это легко доказать, сравнив их при помощи оператора строгого сравнения (===), который принимает в расчёт не только значения, но и типы данных:

undefined === null; // false

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

Получение undefined

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

Любая объявленная переменная, которой при создании не присвоено никакого значения, имеет значение undefined:

var foo; // по умолчанию undefined

Значение undefined также получается при попытке обратиться к несуществующему свойству объекту или элементу массива:

var array = [1, 2, 3];
var foo = array.foo; // свойство foo не существует, возвращается undefined
var item = array[5]; // в массиве нет элемента 5, возвращается undefined

Если в функции нет оператора return, она возвращает undefined:

var value = (function(){})(); // возвращает undefined

Если функции не был передан какой-либо аргумент, он становится undefined:

(function(undefined){
    // параметр равен undefined
})();

Помимо всего вышеперечисленного, для получения undefined может использоваться оператор void. Некоторые библиотеки, вроде Underscore пользуются этим для надежной проверки типов, потому как void нельзя переопределить, и он всегда возвращает undefined:

function isUndefined(obj){
    return obj === void 0;
}

Наконец, undefined — это предопределённая глобальная переменная (а не ключевое слово, как null), которая равна undefined:

'undefined' in window; // true

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

Применение null

В первую очередь null отличается своим применением, и в отличие от undefined, null больше используется для присваивания значения. Как раз из-за этого оператор typeof для null возвращает «object». Изначально это объяснялось тем, что null использовался (и используется) как пустая ссылка там, где ожидается объект, что-то вроде заглушки. Такое поведение typeof было позже признано багом, и, хотя было предложено это поведение исправить, пока что, в целях обратной совместимости, всё остается как есть.

Вот, почему окружение JavaScript не выставляет никаких значений в null, и это делается только программно. В документации на MDN написано следующее:

В различных API null часто возвращается в тех местах, где ожидается объект, но такой объект подобрать нельзя.

Это правдиво для DOM, который не зависит от языка и никак не описывается в документации ECMAScript. Из-за того, что используется внешний API, попытка получить отсутствующий элемент возвращает null, а не undefined.

Вообще, если нужно присвоить «не-значение» переменной или свойству, передать его в функцию, или вернуть из функции, то null — это почти всегда лучший вариант. Упрощённо: JavaScript использует undefined, а программисты должны использовать null.

Другой способ применения null — явное «зануливание» переменной (object = null), когда ссылка на объект больше не требуется. Кстати, это считается хорошей практикой. Присваивая null, вы фактически удаляете ссылку на объект, и если на него нет других ссылок, он отправляется к сборщику мусора, таким образом возвращая доступную память.

Копнём глубже

Причина того, что null и undefined эдакие чёрные дыры, кроется не только в их поведении, но ещё и в том, как они обрабатываются внутри окружения JavaScript. Они не обладают теми характеристиками, которые обычно присущи другим примитивам и встроенным объектам.

Начиная с ES5 метод Object.prototype.toString, ставший стандартом де-факто для проверки типов, стал полезен в этом отношении и для null с undefined:

Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(undefined); // [object Undefined]

Однако, на самом деле у null и undefined этот метод не возвращает внутреннее свойство [[Class]]. По документации он работает следующим образом:

  1. Если значение this равно undefined, вернуть "[object Undefined]".
  2. Если значение this равно null, вернуть "[object Null]".
  3. Пусть O равно результату вызова ToObject с this, переданным как аргумент.
  4. Пусть class равно внутреннему свойству [[Class]] объекта O.
  5. Вернуть значение String, которое является результатом сложения трёх строк "[object ", class, и "]".

Этот метод просто возвращает заготовленную строку, если обнаруживает null или undefined, просто чтобы унифицировать функциональность с другими объектами. Такое поведение встречается сплошь и рядом во всей документации, большая часть методов содержат простую проверку, и если встретился null или undefined, возвращают значение сразу. Фактически, нигде не написано, что у них содержатся какие-либо внутренние свойства, обычно имеющиеся у каждого нативного объекта. Это как если бы они вообще не были объектами. Интересно, эти примитивы в окружении JavaScript как-то явно и особо обрабатываются? Может быть, кто-то более знакомый с имплементацией мог бы подсказать.

Заключение

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


Комментарий переводчика

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

var s = 'test', o = Object(s);
o.foo = 42;
s.foo = 42;
o.foo; // 42
s.foo; // undefined

Дело в том, что null и undefined просто нельзя преобразовать в объект, на чем и строится объяснение ключевых особенностей этих примитивов автором этой статьи.

Также, фраза про то, что null в окружении JavaScript без явного присваивания не используется, неверна. В конце цепочки прототипов находится null, и это как раз тот случай, когда ожидается объект, но его нет:

Object.getPrototypeOf(Object.prototype); // null