Статьи

Исправление JavaScript typeof оператора

Работа с оператором 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…

  1. Пусть O будет результатом вызова ToObject с передачей значения this в качестве аргумента.
  2. Пусть класс будет значение [[Class]] внутреннее свойство O .
  3. Возвращает значение 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