JavaScript, typeof, типы и классы.

Статья написана по мотивам бессмысленной и беспощадной дискуссии на javascript.ru.

Итак, есть у нас программа на языке JavaScript, в ней есть какие-то значения, а у значений есть какой-то тип. И вот вопрос: как нам узнать, этот самый тип значения?

И второй вопрос: а действительно ли нам надо его узнавать и нельзя ли обойтись другими средствами?

Под «типом» я здесь подразумеваю более широкое понятие, чем встроенный тип, возвращаемый оператором typeof. Это, например, «массив» или «элемент DOM», которые для typeof все на одно лицо ("object"). Вернее их было бы назвать «классами типа object», но не будем пока погружаться в терминологию.

Когда знание типа не нужно

В большинстве случаев выяснять тип не требуется.

Если функция принимает в качестве аргумента HTMLDivElement с которым выполняет какие-то манипуляции и в документации к ней указана, что она принимает HTMLDivElement, то она может рассчитывать на то, что получит именно HTMLDivElement.

А если программист вызывает её со строкой в качестве аргумента, то это проблемы программиста. И проблемы в том самом месте, где он её вызывает.

Когда знание типа может пригодиться

При попытке же написать более универсальную функцию, знание типа (класса) переменных бывает не лишним.

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

Здесь уже перебирать и сливать различные значения нужно по разному. Простые объекты через for (k in obj), массивы и подобные им (arguments, HTMLCollection) через for (i=0;i<len;i++). При этом Function, например, тоже объект со своими свойствами, но при слиянии мы вправе ожидать, что скопируется сам объект функции, а не будут рекурсивно сливаться его свойства.

Для этого нам нужно знать, что перед нами — сугубо говоря, «словарь», «список» или «индивидуальный объект».

Полиморфизм, не?

В некоторых языках мы можем попытаться спустить всё на полиморфизм. Работать с объектами через набор интерфейсов и надеяться на то, что сами объекты их корректно реализовывают.

И в Python’е наши надежды в большинстве случаев оправдываются. В JavaScript они оправдываются несколько реже.

Например, если попробовать перебирать через for..in любое значение, результаты могут отличаться от того, что мы в итоге ожидали. Более того, они могут отличаться от себя же самих в другом браузере.

Способы определения

Основных способов определения несколько. Более подробно мы их рассмотрим ниже.

1. Оператор typeof, первое что приходит в голову и то, чего достаточно в значительном проценте случаев.

А в остальных случаях недостаточно, так как определяет «встроенный тип», но не «класс» значения. То есть Object, Array, HTMLCollection и TextNode, это всё разные «классы» типа «object», а typeof гребёт их всех под одну гребёнку.

2. Для выявления же «классов», очевидно, следует использовать то, что их и определяет — конструкторы, цепочки прототипов, instanceof.

3. Также, в качестве хака, можно использовать строковое представление, возвращаемое методом toString.

4. Ещё более грязный хак: зацепиться за какое-нибудь свойство. Например, если есть tagName, значит это DOMElement. Ну или мы можем условно считать, что это DOMElement, хотя каждый может создать это свойство в своём объекте.

5. В некоторых случаях можно воспользоваться специальными методами, например, isArray(). Но нужный метод есть не всегда, а когда он есть, то его нет в IE :)

Типы, классы и всё такое

С теоретической точки зрения можно завести жаркую дискуссию о различии между понятиями «типа» и «класса» и перейти в ней на личности. Правильно ли ведёт себя typeof или неправильно.

Так же можно порассуждать о том, что к JS неприменимо понятие «класс» только по той причине, что в языке нет ключевого слова class.

С практической же точки зрения это не важно. Важно то, что вот у меня есть значение и я хочу знать что оно из себя представляет и как мне с ним можно работать. Если typeof не может удовлетворить всех потребностей, тогда дайте другое средство.

Тесты

Любой может посмотреть, что думает его браузер о различных значениях, на странице https://blgo.ru/t/jstypes/. (под любыми не подразумеваются пользователи IE < 9). И ещё я вероломно нарушил неприкосновенность личных данных вашего браузера и отправил все эти данные к себе на сервер для сбора статистики. В этой статистике сейчас и разберёмся.


Гугло-картинка по запросу
«javascript typeof»,
какбе намекает,
что здесь чёрт ногу сломит

Протестированные браузеры

В статистику угодили пользователи следующих браузеров:

  • Firefox 3, 4, 5, 8, 9, 10 (Windows)
  • Firefox 10 (Linux)
  • Firefox 10 (Mac)
  • Chrome (Windows и Linux)
  • Opera (Windows и Linux)
  • IE 6, 7, 8, 9 (7-й показал различные результаты на XP и Win-7)
  • Safari (Mac, Win-XP, Win-7)

Тестируемые значения

Протестированы все значения не object-типов:

  • number
  • boolean
  • undefined
  • null
  • string
  • function (встроенные и пользовательские)

Из объектов:

  • Простой объект «{}»
  • Простой массив «[]»
  • Объект пользовательского «класса» (создан пользовательским конструктором)
  • Объект arguments
  • Встроенные объекты «классов» Date, Error, RegExp
  • Объекты DOM-классов: DOMElement, TextNode, HTMLCollection
  • Стандартные объекты (тоже, по сути, DOM): document, window
  • Ещё из стандартных до кучи: navigator и location

Вообще, в гипотетической функции, определяющей «расширенный тип» объекта, нет смысла отдельно выделять уникальные объекты вроде window и navigator. Их можно определить просто obj === window. В тестах же они использовались просто потому что интересно на них посмотреть.

Примитивные значения

Как известно, в JavaScript всё как бы объект, но как бы не всегда.

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

Были протестированы как примитивы (5), так и объекты-обёртки (new Number(5)).

И, кроме того, специальные значения, относящиеся к данному типу (NaN, -Infinity, +Infinity).

Спецзначения во всех тестах ведут себя также, как и обычные примитивы.

А вот с обёртками заминка:

console.log( typeof 5 );                       // number
console.log( typeof Number(5) );               // number (просто приведение типа, на выходе - примитив "5")
console.log( typeof new Number(5) );           // object
 
console.log( typeof Number.NaN );              // number
console.log( typeof Number(Number.NaN) );      // number
console.log( typeof new Number(Number.NaN) );  // object

Видно, что объекты всегда object, даже если это Number.

Даже на MDN об этом сказано следующее: «this is confusing. Don’t use!».


Builtin-объекты и Host-объекты

Согласно стандарту ECMA-262, в JavaScript есть «родные объекты» и «объекты среды».

Родные (встроенные, native, built-in) — определяемые, собственно, этим самым ECMA, объекты, не зависящие от среды исполнения. Сюда же в нашем случае можно отнести и объекты создаваемые пользователем.

Объекты среды (host), создаёт, как подсказывает Кэп, среда. В нашем случае браузер.

И вот здесь возникает проблема. Поведение родных объектов определяется всё тем же стандартом ECMA-262. И, хотя местами определяется туманно, но в большинстве случаев можно надеяться на сходное поведение во всех браузеров. А вот для host-объектов написано «Implementation-dependent». То есть стандарт говорит браузерам: «а творите вы чего хотите». И браузеры творят, в чём мы ниже и убедимся.

Оператор typeof

Перейдём, наконец, к результатам.

Оператор typeof возвращает в общем-то практически тоже самое, что приводится в большинстве табличек в интернете (пример):

  • Undefined: «undefined»
  • Null: «object»
  • Boolean: «boolean»
  • Number: «number»
  • String: «string»
  • Function: «function»
  • Всё остальное: «object»

К этой таблице следует добавить следующие замечания:

1. typeof null === "object".

Теоретически здесь тонкий момент. В языках со статической типизацией переменная объектного типа может не содержать объекта (NULL, nil, нулевой указатель).

Практически — в JavaScript это неудобно. Поэтому разработчики ES 5.1 собираются сделать более интуитивно понятную вещь: typeof null === "null".

Но так как у нас пока кругом ES3, не ошибитесь, например, на таком:

/* Функция ищет какой-то объект и возвращает его или null если ничего не найдено */
function search() {}
 
var obj = search();
 
if (typeof obj === "object") { // действительно ли мы нашли объект (FAIL)
    obj.method();
}

2. Не забываем про объекты-обёртки (typeof new Number(5) === "object").

3. И не забываем на право браузеров творить что угодно с host-объектами.

Не удивляйтесь тому, что Safari упорно считает HTMLCollection типом function, а IE ранее 9-й версии держат нашу любимую функцию alert() за object. Также Chrome раньше считал RegExp за function, но теперь, кажется образумился и отвечает на неё object.

toString()

Пытаться узнать тип значения по результату его метода toString() бессмысленно. Во всех «классах» этот метод переопределён на свой.

Для вывода отладочной информации метод хорош, но типа переменной по нему не определить.

Object.prototype.toString()

Хотя toString внутри конкретных «классов» переопределён, у нас всё равно есть его изначальная реализация из Object. Попробуем воспользоваться ей:

console.log( Object.prototype.toString.call(value) );

Клинтон разбавляет эту тягомотину

Как ни странно, метод этот работает на удивление хорошо.

Для скалярных типов возвращает [object Number], [object Boolean], [object String], [object Function].

Самое смешное, что даже new Number(5) на котором засбоил typeof здесь возвращает [object Number].

На null и undefined метод давать сбои. Разные браузеры возвращают, то ожидаемые [object Null] и [object Undefined], то [object Object], то вообще [object Window]. Впрочем определить тип этих двух значений легко можно и без этого.

Интересное начинается, когда мы подходим к объектам (тем, у которых typeof === "object").

built-in объекты отрабатывают, практически, на ура:

  • {} — [object Object]
  • [] — [object Array]
  • Date — [object Date]
  • Error — [object Error]
  • RegExp — [object RegExp]

Единственно, выпадает из списка arguments, который то [object Arguments], то [object Object].
С host-объектами опять всё хуже.

В IE DOM-объекты стали становиться «нормальными» объектами только с 8-й версии и то не совсем до конца. Поэтому в IE 6-8 все эти объекты (HTMLCOllection, DOMElement, TextNode, а заодно document и window) приводятся просто к [object Object].

Во всех остальных браузерах (включая IE9) с результатом toString уже можно что-то делать. Хотя тоже всё непросто: HTMLCollection там [object HTMLCollection], то [object NodeList]. window — то [object Window], то [object DOMWindow], то [object global]. Но из этого уже можно попытаться что-то выудить.

Сложнее с DOMElement: он выводится в виде [object HTMLDivElement], [object HTMLSpanElement] — свой формат для каждого тега. Но и здесь регулярка нам поможет.

С другими host-объектами (в тестах location и navigator) примерно таже история. Везде, кроме IE, их можно идентифицировать по строке.

Из минусов использования Object.prototype.toString():

1. Возможность сия не освящена стандартом. И мы здесь должны скорее радоваться, что всё так удачно работает, а не сокрушаться по поводу некоторых изъянов.

2. Определение типа по синтаксическому разбору строки, возвращаемой методом, который вообще не для определения типа, да и ещё вызывается на объекте к которому не относится, оставляет некоторый осадок на душе.

3. В старых IE, как видно, host-объекты нормально не идентифицировать.

Однако же, это вполне рабочая штука при использовании совместно с другими средствами.


Конструкторы

Ну и, наконец, конструкторы. Кто может лучше сказать о «классе» объекта в JS, если не его конструктор?

У null и undefined нет ни объектов-обёрток, ни конструкторов.

У остальных скалярных типов обёртки есть, соответственно, можно получить и конструктор:

(5).constructor === Number;
(Number.NaN).constructor === Number;
(true).constructor === Boolean;
("string").constructor === String;

А вот instanceof здесь не пройдёт:

5 instanceof Number;          // false
Number.NaN instanceof Number; // false
true instanceof Boolean;      // false
"string" instanceof String;   // false

(instanceof сработает для многострадального new Number(5))

С функциями (которые к тому же объекты) пройдёт и instanceof:

console.log( (function () {}) instanceof Function );      // true
console.log( (function () {}).constructor === Function ); // true

Все объекты встроенных классов также легко идентифицируются по конструкторам: Array, Date, RegExp, Error.

Одна проблема возникает здесь с arguments, конструктор которого Object.

А вторая с самим Object, вернее как отнести к нему объект, созданный через пользовательский конструктор.

Так можно определить только базовый объект:

obj.constructor === Object;

А это захватит также и все остальные Array, Date и т.п:

obj instanceof Object;

Как один из вариантов определения — перебрать все остальные возможные типы (Array, Error …) и если ни под один не подпал — «object».

Конструкторы и host-объекты

С host-объектами всё хуже.

Начнём с того, что IE до 7-й версии включительно их вообще за нормальные объекты не считает. У них там просто нет конструкторов и прототипов (во всяком случае программисту до них не достучаться).

В других браузерах дела получше. Конструкторы есть и по ним можно определить класс значения. Только называются они в разных браузерах по разному. Например для HTMLCollection конструктор будет или HTMLCollection или NodeList, а то и вовсе NodeListConstructor.

Также следует определить базовый конструктор для DOMElement. В FF, это, например, HTMLElement, от которого уже наследуются HTMLDivElement и другие.

Подлянку подкидывают FireFox ниже 10-й версии и Opera ниже 11. Там конструктор коллекции — Object.

constructor.name

Ещё у конструкторов есть свойство name, которое может быть полезно.

Оно содержит имя функции-конструктора, например, (5).constructor.name === "Number".

Однако:
1. В IE его нет вообще, даже в 9-м.
2. В Host-объекты браузеры опять лепят каждый что горазд (а зачастую те вообще не имеют этого свойства). В Opera’е у DOMElement имя конструктора вообще Function.prototype.
3. arguments вновь «object«.

Выводы

Ни один из представленных способов не даёт стопроцентного определения типа/класса значения во всех браузерах. Однако, в совокупности они позволяют это сделать.

В ближайшее время попробую собрать все данные в таблички и привести пример функции-определялки.

11 комментариев »

  • Я то надеялся, что ты найдешь серебряную пулю.

    artoodetoo, 5.03.2012, 11:03

  • Серебрянные пули для нубов

    vasa_c, 5.03.2012, 12:04

  • Еле дочитал, но статья супер, в избранное!
    > В ближайшее время попробую собрать все данные в таблички и привести пример функции-определялки.

    А как дело обстоит у фреймворков не смотрел, а coffeescript?

    adw0rd, 5.03.2012, 16:02

  • …подписался на пост, не обращайте внимание…

    adw0rd, 5.03.2012, 16:03

  • Нет, coffescript не употребляю.

    vasa_c, 5.03.2012, 17:25

  • ни один из способов контрацепции не дает стопроцентного результата…)

    Sinkler, 5.03.2012, 19:12

  • > Нет, coffescript не употребляю.

    Я имел ввиду реализацию у них typeof/isinstance, как они это сделали

    adw0rd, 5.03.2012, 19:44

  • http://api.jquery.com/jQuery.type/ посмотри реализацию
    http://coffeescriptcookbook.com/chapters/classes_and_objects/type-function
    http://javascript.crockford.com/remedial.html

    adw0rd, 5.03.2012, 19:52

  • >http://api.jquery.com/jQuery.type/ посмотри реализацию
    они только Array, Date и RegExp добавили.
    самое интересное начинается на DOM-элементах )

    vasa_c, 6.03.2012, 14:38

  • coffescript:
    for name in «Boolean Number String Function Array Date RegExp».split(» «)
    classToType[«[object » + name + «]»] = name.toLowerCase()

    тут тоже из ограниченнного выбирают.

    vasa_c, 6.03.2012, 14:41

  • Я таки дочитал эту статью до конца. :)
    Что-то ты vasa_c еще охуеннее чем обычно. Жду продолжения.

    dallone, 8.03.2012, 19:05

Leave a comment