Работа с оператором typeof в JavaScript немного похожа на работу с изможденным старым автомобилем (или ранней моделью Dell Inspiron). Он выполняет свою работу (в основном), и вы учитесь обходить причуды — но вы, вероятно, стремитесь к чему-то лучшему.
В этой статье я дам краткий обзор typeof, прежде чем вводить крошечную новую функцию, которая является полностью загруженной, более надежной альтернативой, которая работает непосредственно с внутренними компонентами языка.
Оператор typeOf
Как это используется?
Поскольку typeof является унарным оператором, операнд следует за оператором. Никакой дополнительной пунктуации не требуется.
typeof 2 //"number" typeof "belladonna" //"string"
Но это работает, когда я вызываю это как функцию?
Оператор typeof не является функцией. Вы можете заключить операнд в круглые скобки, чтобы выражение выглядело как вызов функции, но круглые скобки будут просто действовать как оператор группировки (уступая только оператору запятой в порядке следования неизвестности!). На самом деле вы можете украсить операнд всевозможными знаками препинания, не сбивая оператора с пути.
typeof (2) //"number" typeof(2) //"number" typeof ("a", 3) //"number" typeof (1 + 1) //"number"
Что это возвращает?
Возвращаемое значение является несколько произвольным представлением типа операнда. В таблице ниже (основанной на спецификации ES5 ) приводится сводная информация:
Тип Валь | Результат |
Неопределенный | «Неопределенный» |
Ноль | «Объект» |
логический | «Логическое» |
Число | «число» |
строка | «Строка» |
Объект (родной и не вызываемый) | «Объект» |
Объект (нативный или хост и вызываемый) |
«Функция» |
Объект (хост и не вызываемый) |
Реализация определенных |
Что не так с typeof?
Наиболее вопиющая проблема заключается в том, что typeof null возвращает «объект». Это просто ошибка. Говорят об исправлении в следующей версии спецификации ECMAScript, хотя это, несомненно, приведет к проблемам обратной совместимости.
var a; typeof a; //"undefined" typeof b; //"undefined" alert(a); //undefined alert(b); //ReferenceError
Кроме этого, typeof просто не очень разборчив. Когда typeof применяется к любому типу объекта, кроме Function, он возвращает «object». Он не различает универсальные объекты и другие встроенные типы (Array, Arguments, Date, JSON, RegExp, Math, Error и примитивные объекты-обертки Number, Boolean и String).
Ох, и вы услышите, что люди жалуются на это …
typeof NaN //"number"
… Но это не ошибка оператора typeof, поскольку в стандарте четко указано, что NaN действительно число.
Лучший путь?
[[Класс]]
Каждый объект JavaScript имеет внутреннее свойство, известное как [[Class]] (спецификация ES5 использует двойную квадратную скобку для представления внутренних свойств, то есть абстрактных свойств, используемых для определения поведения механизмов JavaScript). Согласно ES5 [[Class]] является «строковым значением, указывающим специфицированную классификацию объектов». Для нас с вами это означает, что каждый встроенный тип объекта имеет уникальное, не редактируемое, поддерживаемое стандартами значение для своего свойства [[Class]]. Это может быть очень полезно, если только мы сможем получить свойство [[Class]] …
Object.prototype.toString
… и оказывается, что мы можем. Взгляните на спецификацию ES 5 для Object.prototype.toString…
- Пусть O будет результатом вызова ToObject с передачей значения this в качестве аргумента.
- Пусть класс будет значение [[Class]] внутреннее свойство O .
- Возвращает значение String, являющееся результатом объединения трех строк «[object», class и «]».
Короче говоря, функция toString по умолчанию Object возвращает строку в следующем формате…
[объект [[Класс]] ]
… Где [[Class]] — это свойство класса объекта.
К сожалению, специализированные встроенные объекты в основном перезаписывают Object.prototype.toString собственными методами toString…
[1,2,3].toString(); //"1, 2, 3" (new Date).toString(); //"Sat Aug 06 2011 16:29:13 GMT-0700 (PDT)" /a-z/.toString(); //"/a-z/"
… к счастью, мы можем использовать функцию вызова для навязывания им общей функции toString …
Object.prototype.toString.call([1,2,3]); //"[object Array]" Object.prototype.toString.call(new Date); //"[object Date]" Object.prototype.toString.call(/a-z/); //"[object RegExp]"
Представляем функцию toType
Мы можем воспользоваться этой техникой, добавить каплю regEx и создать крошечную функцию — новую улучшенную версию оператора typeOf…
var toType = function(obj) { return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1] }
(поскольку новый универсальный объект всегда будет использовать функцию toString, определенную Object.prototype, мы можем безопасно использовать ({}). toString как сокращение для Object.prototype.toString)
Давайте попробуем это …
toType({a: 4}); //"Object" toType([1, 2, 3]); //"Array" (function() {console.log(toType(arguments))})(); //Arguments toType(new ReferenceError); //"Error" toType(new Date); //"Date" toType(/a-z/); //"RegExp" toType(Math); //"Math" toType(JSON); //"JSON" toType(new Number(4)); //"Number" toType(new String("abc")); //"String" toType(new Boolean(true)); //"Boolean"
… и теперь мы запустим те же тесты с оператором typeof (и постараемся не злорадствовать)…
typeof {a: 4}; //"object" typeof [1, 2, 3]; //"object" (function() {console.log(typeof arguments)})(); //object typeof new ReferenceError; //"object" typeof new Date; //"object" typeof /a-z/; //"object" typeof Math; //"object" typeof JSON; //"object" typeof new Number(4); //"object" typeof new String("abc"); //"object" typeof new Boolean(true); //"object"
Сравните с уткой
Типизирование утки проверяет характеристики объекта по списку известных атрибутов для данного типа (ходит как утка, говорит как утка …). Из-за ограниченной полезности оператора typeof типизирование утки популярно в JavaScript. Это также подвержено ошибкам. Например, объект arguments функции имеет свойство length и пронумерованные с нумерацией элементы, но это все еще не массив.
Использование toType — это надежная и простая альтернатива печати по утке. Надежный, потому что он напрямую взаимодействует с внутренним свойством объекта, которое устанавливается движком браузера и не редактируется; легко, потому что это проверка из трех слов.
Вот иллюстративный пример — фрагмент, который определяет несовместимый объект JSON. Функция jsonParseIt принимает функцию в качестве аргумента, которую она может использовать для проверки достоверности объекта JSON, прежде чем использовать его для анализа строки JSON….
window.JSON = {parse: function() {alert("I'm not really JSON - fail!")}}; function jsonParseIt(jsonTest) { if (jsonTest()) { return JSON.parse('{"a":2}'); } else { alert("non-compliant JSON object detected!"); } }
Давайте запустим его сначала с помощью утки
jsonParseIt(function() {return JSON && (typeof JSON.parse == "function")}) //"I'm not really JSON - fail!"
… упс! … а теперь с тестом toType …
jsonParseIt(function() {return toType(JSON) == "JSON"}); //"non-compliant JSON object detected!
Может ли ToType надежно защитить от недобросовестного обмена встроенными объектами JavaScript с самозванцами? Вероятно, нет, так как исполнитель мог предположительно также поменять местами функцию toType. Более безопасный тест может вызвать ({}). ToString напрямую …
function() { return ({}).toString.call(JSON).indexOf("JSON") > -1 }
… хотя даже это не получится, если Object.prototype.toString сам по себе будет перезаписан со злым умыслом. Еще каждая дополнительная защита помогает.
Сравните с instanceof
Оператор instanceof проверяет цепочку прототипов первого операнда на наличие свойства prototype второго операнда (ожидается, что второй операнд будет конструктором, а TypeError будет выдан, если он не является функцией):
new Date instanceof Date; //true [1,2,3] instanceof Array; //true function CustomType() {}; new CustomType instanceof CustomType; //true
На первый взгляд, это обещает хорошую проверку типов для встроенных модулей, однако при таком подходе есть как минимум две проблемы:
1. Некоторые встроенные объекты (Math, JSON и аргументы) не имеют связанных объектов-конструкторов, поэтому их нельзя проверить типом с помощью оператора instanceof.
Math instanceof Math //TypeError
2. Как отметили @kangax и другие, окно может содержать несколько кадров, что означает несколько глобальных контекстов и, следовательно, несколько конструкторов для каждого типа. В такой среде не гарантируется, что данный тип объекта будет экземпляром данного конструктора….
var iFrame = document.createElement('IFRAME'); document.body.appendChild(iFrame); var IFrameArray = window.frames[1].Array; var array = new IFrameArray(); array instanceof Array; //false array instanceof IFrameArray; //true;
Проверка типов хост-объектов
Хост-объекты — это созданные браузером объекты, которые не определены стандартом ES5. Все элементы DOM и глобальные функции являются хост-объектами. ES5 отказывается указывать возвращаемое значение для typeof при применении к хост-объектам, а также не предлагает значение для свойства [[Class]] хост-объектов. В результате кросс-браузерная проверка типов хост-объектов, как правило, ненадежна:
toType(window); //"global" (Chrome) "DOMWindow" (Safari) "Window" (FF/IE9) "Object" (IE7/IE8) toType(document); //"HTMLDocument" (Chrome/FF/Safari) "Document" (IE9) "Object" (IE7/IE8) toType(document.createElement('a')); //"HTMLAnchorElement" (Chrome/FF/Safari/IE) "Object" (IE7/IE8) toType(alert); //"Function" (Chrome/FF/Safari/IE9) "Object" (IE7/IE8)
Наиболее надежным кросс-браузерным тестом для элемента может быть проверка на наличие свойства nodeType…
function isElement(obj) { return obj.nodeType; }
… но это утка, так что нет никаких гарантий
Где должна жить функция toType?
Для краткости мои примеры определяют toType как глобальную функцию. Расширение Object.prototype приведет вас к драконам — я бы предпочел напрямую расширить Object, что отражает соглашение, установленное ES5 (и prototype.js до этого).
Object.toType = function(obj) { if((function() {return obj && (obj !== this)}).call(obj)) { //fallback on 'typeof' for truthy primitive values return typeof obj; } return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1] }
В качестве альтернативы вы можете добавить функцию toType в собственное пространство имен, например util.
Мы могли бы стать немного умнее (вдохновленный использованием Chrome «global» для окна. [[Class]]). Оборачивая функцию в глобальный модуль, мы можем также идентифицировать глобальный объект:
Object.toType = (function toType(global) { return function(obj) { if (obj === global) { return "Global"; } if((function() {return obj && (obj !== this)}).call(obj)) { //fallback on 'typeof' for truthy primitive values return typeof obj; } return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1] } })(this)
Давайте попробуем это …
Object.toType(window); //"Global" (all browsers) Object.toType([1,2,3]); //"Array" (all browsers) Object.toType(/a-z/); //"RegExp" (all browsers) Object.toType(JSON); //"JSON" (all browsers) //etc..
Что toType не делает
Функция toType не может защитить неизвестные типы от выдачи ReferenceErrors …
Object.toType (FFF); // ReferenceError |
Точнее, вызов toType вызывает ошибку, а не сама функция. Единственная защита от этого (как и при вызове любой функции) — это соблюдать правила гигиены кода.
window.fff && Object.toType(fff);
Заворачивать
Хорошо, я болтал намного дольше, чем собирался — так что поздравляю, если вы добрались до этого, я надеюсь, что вы нашли это полезным. Я преодолел много вопросов и, возможно, допустил некоторые ошибки — пожалуйста, не стесняйтесь, дайте мне знать о них. Также я хотел бы услышать о приключениях других людей в проверке типа.
Дальнейшее чтение
Юрий Зайцев («kangax»):
«instanceof» считается вредным (или как написать надежный «isArray»)
ECMA-262, 5-е издание:
оператор typeof
Внутренние свойства и методы объекта (подробнее о [[Class]])
Object.prototype.toString
Оператор instanceof
От http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator