Объект, который не является объектом В JavaScript есть свои забавные особенности, и одна из них — оператор typeof, который определяет тип значения. Ожидаемое поведение: typeof 100 //число typeof "string" //строка typeof {} //объект typeof Symbol //функция typeof undefinedVariable // undefined А вот это — наш фаворит, главный герой сегодняшней статьи: typeof null // объект JavaScript, как и другие языки программирования, имеет типы, которые можно разделить на «примитивные» — те, которые возвращают единственное значение (null, undefined, boolean, symbol, bigint, string), и типы «object» со сложной структурой. Проще говоря, например, boolean в JavaScript представляет собой нечто, не имеющее очень сложной структуры, так как возвращает только одно значение: true или false. К примеру, в современной реализации Firefox используется техника под названием «pointer tagging», где 64-битное значение кодирует тип и значение или адрес в куче. Посмотрим, как в этой реализации обрабатываются булевы значения: const flagTrue = true; Keyword | Tag               |  Payload false   | JSVAL_TAG_BOOLEAN | (0xFFFE*) 0x000000000000 true    | JSVAL_TAG_BOOLEAN | (0xFFFE*) 0x000000000001 Можно заметить, что старшие биты отвечают за определение типа данных, а младшие — за полезную нагрузку или адрес выделенного объекта в куче. Таким образом, в данном случае наши true/false представлены в двоичном виде как 1/0. Вероятно, вы задаетесь вопросом, какое отношение это имеет к тому, что typeof null возвращает object вместо null. Чтобы понять это, нужно вернуться на 30 лет назад к оригинальной реализации JavaScript в Netscape, которая использовала 32-битную схему разметки, совершенно отличную от современных движков. Брендану Эйху, которого наняла Netscape, в то время являвшаяся основным игроком на рынке браузеров, из-за значительных требований рынка и жесткой конкуренции со стороны таких компаний, как Microsoft и Sun Microsystems, поручили создание прототипа языка программирования, который должен был соответствовать ключевым критериям: быть простым для широкого круга людей (без статической типизации и установки компилятора); позволять пользователям манипулировать DOM на базовом уровне. Через 10 дней был создан язык программирования, который носил поочередно такие названия, как Mocha, LiveScript и, наконец, JavaScript — из-за маркетингового давления с целью использования популярности Java. Спустя 10 дней родился прототип языка программирования, который, несмотря на последующий закат браузера Netscape из-за конкуренции с Microsoft и установки Internet Explorer по умолчанию в Windows, сохранился до наших дней и продолжает развиваться. Браузер Netscape был написан на C, как и сама реализация JavaScript. Поэтому обратимся к реализации typeof в Netscape Navigator 1.3, которая приветствовала программистов того времени командой help с таким сообщением: js> help() JavaScript-C 1.3 1998 06 30 А код, реализующий typeof, выглядел так: JS_TypeOfValue(JSContext *cx, jsval v) {     JSType type;     JSObject *obj;     JSObjectOps *ops;     JSClass *clasp;     CHECK_REQUEST(cx);     if (JSVAL_IS_VOID(v)) {        type = JSTYPE_VOID;     } else if (JSVAL_IS_OBJECT(v)) {        obj = JSVAL_TO_OBJECT(v);         if (obj &&             (ops = obj->map->ops,       ops == &js_ObjectOps              ? (clasp = OBJ_GET_CLASS(cx, obj),               clasp->call || clasp == &js_FunctionClass)              : ops->call != 0)) {             type = JSTYPE_FUNCTION;         } else {             type = JSTYPE_OBJECT;         }     } else if (JSVAL_IS_NUMBER(v)) {          type = JSTYPE_NUMBER;     } else if (JSVAL_IS_STRING(v)) {          type = JSTYPE_STRING;     } else if (JSVAL_IS_BOOLEAN(v)) {          type = JSTYPE_BOOLEAN;     }     return type; } Макросы, определяющие типы данных в Netscape 1.3, выглядели следующим образом: #define JSVAL_OBJECT            0x0     /* непомеченная ссылка на объект */ #define JSVAL_INT               0x1     /* помеченное 31-битное целочисленное значение */ #define JSVAL_DOUBLE            0x2     /* помеченная ссылка на число двойной точности */ #define JSVAL_STRING            0x4     /* помеченная ссылка на строку */ #define JSVAL_BOOLEAN           0x6     /* помеченное булево значение */ Что соответствовало такому представлению в памяти (32-битная система): Type     Tag (Low 3 bits)      Memory (32 bits)          Value Object   000 (0x0)             [29-bit pointer][000]     0x12345000 Integer  001 (0x1)             [29-bit int value][001]   0x00006401 (42) Double   010 (0x2)             [29-bit pointer][010]     0xABCDE002 → heap String   100 (0x4)             [29-bit pointer][100]     0x78901004 → “hello” Boolean  110 (0x6)             [29-bit value][110]       0x00000006 (true) На основе этой информации можно создать упрощенную программу, перенеся несколько макросов из Netscape для исследования задачи (код упрощен в учебных целях): #include <stdlib.h> #include <stdio.h> typedef unsigned long   pruword; typedef long            prword; typedef prword    jsval; #define PR_BIT(n)       ((pruword)1 << (n)) #define PR_BITMASK(n)   (PR_BIT(n) - 1) #define JSVAL_OBJECT            0x0     /* непомеченная ссылка на объект */ #define OBJECT_TO_JSVAL(obj)    ((jsval)(obj)) #define JSVAL_NULL              OBJECT_TO_JSVAL(0) #define JSVAL_TAGMASK           PR_BITMASK(JSVAL_TAGBITS) #define JSVAL_TAG(v)            ((v) & JSVAL_TAGMASK) #define JSVAL_IS_OBJECT(v)      (JSVAL_TAG(v) == JSVAL_OBJECT) #define JSVAL_TAGBITS           3 struct JSObject {     struct JSObjectMap *map; }; struct JSObjectMap { }; // Вспомогательная функция для вывода двоичного представления void print_binary(unsigned long n) {     for (int i = 31; i >= 0; i--) {         printf("%d", (n >> i) & 1);     }     printf("\n"); } int main() {  struct JSObject* obj = malloc(sizeof(struct JSObject));  jsval objectValue = OBJECT_TO_JSVAL(obj);  jsval null = JSVAL_NULL;  printf("Is object %d\n", JSVAL_IS_OBJECT(objectValue));  printf("Is null an object %d\n", JSVAL_IS_OBJECT(null));  printf("Binary representation of object: ");  print_binary(objectValue);  printf("Binary representation of null: ");  print_binary(null); } Результат выполнения этой программы: Is object 1 Is null an object 1 Binary representation of object: 01011000000010100011000111100000 Binary representation of null: 00000000000000000000000000000000 Как видите, null и object возвращают одинаковое значение в макросе JSVAL_IS_OBJECT. Почему же null и object неразличимы при проведении такой проверки? Объяснение этому заключается в упомянутой выше модели разметки и использовании памяти в качестве идентификатора типов object в JavaScript. Поскольку JavaScript является языком с динамической типизацией, объявления типов должны были где-то храниться, поэтому в данном случае разработчик решил выделить 3 младших бита для идентификации типа. Установка 000 в качестве идентификатора объекта происходит из механизма работы 32-битной архитектуры и требований аппаратного обеспечения, связанных с выравниванием памяти. Объекты и массивы — это структуры, более сложные, чем примитивные типы, поэтому они выделяются в куче. В 32-битной архитектуре ЦП загружает данные порциями по 32 бита (4 байта), и система управления памятью обеспечивает выравнивание адресов объектов по границам 4 байт. Это означает, что каждый адрес указателя на объект делится на 4, что в двоичном представлении приводит к тому, что адреса объектов всегда оканчиваются двумя нулями в двоичной записи (поскольку 4 = 100 в двоичной системе). Однако на практике в качестве меток использовались три младших бита, поэтому адреса имели 8-байтовое выравнивание, что обеспечивало три нуля в конце. В случае представления null мы видим, что это значение 0 (все нули), которое ссылается на нулевой указатель в C, что в большинстве архитектур определяется как ((void*)0), означая несуществующее место в памяти. Поскольку null представлен как 0x00000000, а три младших бита — 000, макрос JSVAL_IS_OBJECT считает null объектом! Можно ли было это исправить? Конечно! Как видим, представление null — это просто 0, несуществующее место в памяти, тогда как объект — это нечто существующее, а макрос, который корректно проверял на null, присутствовал в коде, но не использовался в функции typeof! #define JSVAL_IS_NULL(v)        ((v) == JSVAL_NULL) Поэтому функция typeof должна выглядеть так: JS_TypeOfValue(JSContext *cx, jsval v) {     JSType type;     JSObject *obj;     JSObjectOps *ops;     JSClass *clasp;     CHECK_REQUEST(cx);     if (JSVAL_IS_NULL(v)) { //проверка, является ли значение null!         type = JSTYPE_NULL;     } else if (JSVAL_IS_VOID(v)) {         type = JSTYPE_VOID;     } else if (JSVAL_IS_OBJECT(v)) {          obj = JSVAL_TO_OBJECT(v);         if (obj &&             (ops = obj->map->ops,               ops == &js_ObjectOps              ? (clasp = OBJ_GET_CLASS(cx, obj),                 clasp->call || clasp == &js_FunctionClass)              : ops->call != 0)) {             type = JSTYPE_FUNCTION;         } else {             type = JSTYPE_OBJECT;         }     } else if (JSVAL_IS_NUMBER(v)) {      type = JSTYPE_NUMBER;     } else if (JSVAL_IS_STRING(v)) {      type = JSTYPE_STRING;     } else if (JSVAL_IS_BOOLEAN(v)) {      type = JSTYPE_BOOLEAN;     }     return type; } Пример реализации с извлеченным кодом, который можно скомпилировать, находится здесь. Если ошибку было так просто исправить, почему ее не исправили? Дело в том, что миллионы страниц уже начали использовать JavaScript с этой ошибкой; о ней знали и обрабатывали ее соответствующим образом. Более того, в 2013 году поступило официальное предложение исправить это поведение в стандарте ECMAScript, но оно было отклонено именно из-за обратной совместимости — слишком большое количество уже написанного кода могло перестать работать. Поэтому, несмотря на то, что прошло 30 лет, это поведение напоминает нам о контексте создания JavaScript и исторических решениях в его разработке. Чтобы действительно проверить, является ли значение объектом, а не null, нужно обрабатывать это следующим образом: if (value !== null && typeof value === 'object') {  // это настоящий объект! } Читайте также: Ключевые понятия JavaScript, которые должен знать каждый разработчик — часть 2 5 основных методов работы с @Cacheable в JavaScript Функции call, apply и bind: использование и сравнение Читайте нас в Telegram, VK и Дзен Перевод статьи Piotr Zarycki: Why typeof null === object
==============
`Оператор typeof в JavaScript определяет тип значения. Значение null, по умолчанию, определяется как объект, поскольку в старых реализациях JavaScript (например, в Netscape Navigator 1.3) использовалась 32-битная схема разметки, где null отображался как несуществующее место в памяти (address), а выравнивание памяти было задано по 4 байта. Это требовало, чтобы адреса объектов заканчивались двумя нулями. Однако, современные движки JavaScript, такие как V8 (используемый в Chrome и Node.js), используют 64-битные архитектуры и другие методы, что приводит к тому, что null определяется как null, а не как объект. Поэтому, чтобы правильно определить, является ли значение объектом, необходимо дополнительно проверять, что значение не равно null.