Frontender Magazine

Исследование бездны 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

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Ryan Morr
Автор:
Ryan Morr
Сaйт:
http://ryanmorr.com/
GitHub:
ryanmorr
Email:
rm.morr@gmail.com
Антон Хлыновский
Переводчик:
Антон Хлыновский
GitHub:
subzey
Twitter:
@subzey

Комментарии (7 комментариев, если быть точным)

Автар пользователя
tyv

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

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

написано верно, а пример плохой, так как o сразу создался как объект когда мы спрашиваем 'str'.length, то происходит new String('str').length, но первый 'str' остается неизменный (у него не появляется возможности добавить 'str'.foo = 42)

``` console.log('str' == new String('str')) // true Объект приведется к строке через toString

console.log('str' === new String('str')) // false ```

Автар пользователя
subzey

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

Честно сказать, не хотелось раздувать примечание до развернутой лекции с простынями кода, статья все-таки не про это.

Автар пользователя
qwabra

ссылка на ЭТОТ комментарий на github (там нормальное фороматирование <кода>)


Упрощённо: JavaScript использует undefined
, а программисты должны использовать null.

origin


#VSCode (Visual Studio Code)
#TypeScript

ИМХО:

  • null - там, где переменная точно есть, но не определена
  • undefined - там, где переменная не обязательна

ts interface iA{ name: string|null; } interface iB{ name?: string|undefined; }


пример:

```ts interface iA { name: string|null; age: number|null; location: string|null; }

let yy:iA = {} //ошибка на этапе объявления

// file: 'file:/mnt/10gb/qqw/www/index.ts' // severity: 'Ошибка' // message: 'Type '{}' is not assignable to type 'iA'. // Property 'name' is missing in type '{}'.' // at: '52,5' // source: 'ts' ```

пример:

```ts

let y:iA = { age : null , location : null , name : null } console.log( y.age //null )

interface iB { name?: string|undefined; age?: number|undefined; location?: string|undefined; }

let z:iB = {} //а так - без проблем

console.log( z.age //undefined ) ```

Автар пользователя
ksetrin

Каждый из них является представителем своего типа: undefined — представляет тип Undefined, а null, соответственно — тип Null.

Автар пользователя
ksetrin

Каждый из них является представителем своего типа: undefined — представляет тип Undefined, а null, соответственно — тип Null.

Небольшая ошибочка. Проверил в консоле typeof(undefined) "undefined"

typeof(null) "object"

P.S. не знал, что не смогу отредактировать первый свой коммент

Автар пользователя
qwabra

©P.S. не знал, что не смогу отредактировать первый свой коммент

та же беда, был неприятно удивлён.
однако, можно - через гитхаб.

Автар пользователя
subzey

@ksetrin, нет, ошибки в статье нету, тип null действительно Null.

typeof null === 'object' же — отголосок недоработки в ранних версиях спецификации языка