ai-benchmark/tests/summarization/nuancesprog.ru_p_30448.txt
second_constantine 25e0a2a96a Remove "Лог файл" column from report
Remove the "Лог файл" (Log file) column from the report generation as it's no longer needed. This simplifies the report structure and removes unused functionality.
2026-01-26 22:40:44 +03:00

3 lines
16 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Объект, который не является объектом В 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.