- Оператор
typeof [[Class]]для встроенных объектов- «Утиная» типизация
- Проверка типа для пользовательских объектов
- Итого
Полиморфные функции - это такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.
Для реализации такой возможности нужен способ определить тип переменной. И здесь в JavaScript есть некоторый «зоопарк» способов, но мы в нем сейчас разберемся.
Как мы знаем, существует несколько примитивных типов:
null- Специальный тип, содержит только значение
null. undefined- Специальный тип, содержит только значение
undefined. number- Числа:
0,3.14, а также значенияNaNиInfinity booleantrue,false.string- Строки, такие как
"Мяу"или пустая строка"".
Все остальные значения являются объектами, включая функции и массивы.
Оператор typeof
Оператор typeof возвращает тип аргумента. У него есть два синтаксиса:
- Синтаксис оператора:
typeof x. - Синтаксис функции:
typeof(x).
Работают они одинаково, но первый синтаксис короче.
Результатом typeof является строка, содержащая тип:
typeof undefined // "undefined"
typeof 0 // "number"
typeof true // "boolean"
typeof "foo" // "string"
typeof {} // "object"
*!*
typeof null // "object"
typeof function(){} // "function"
*/!*
Последние две строки помечены, потому что typeof ведет себя в них по-особому.
- Результат
typeof null == "object"- это официальная ошибка в языке, которая сохраняется для совместимости.На самом деле
null- это не объект, а примитив. Мы можем это даже проверить, попытавшись присвоить свойство:var x = null; x.prop = 1; // ошибка, т.к. нельзя присвоить свойство примитиву
- Для функции
fзначениемtypeof fявляется"function". Это не совсем корректно, т.к. функция является объектом, и непонятна причина, по которой их следует выделять. С другой — на практике это плюс, т.к. позволяет легко определить функцию.
typeof и объекты
Оператор typeof надежно работает с примитивными типами (кроме null). Но он ничего не сообщит об объектах, кроме функций.
Обычные объекты, массивы и даты для typeof все на одно лицо, они имеют тип 'object':
alert( typeof {} ); // 'object'
alert( typeof [] ); // 'object'
alert( typeof new Date ); // 'object'
[[Class]] для встроенных объектов
Основная проблема typeof - неумение различать объекты, кроме функций. Но есть и другой способ получения типа.
У всех встроенных объектов есть скрытое свойство [[Class]]. Оно равно "Array" для массивов, "Date" для дат и т.п.
Это свойство нельзя получить напрямую, есть трюк для того, чтобы прочитать его.
Дело в том, что toString от стандартного объекта выводит [[Class]] в небольшой обертке. Например:
var obj = {};
alert( obj ); // [object Object]
Здесь внутри [object ...] указано как раз значение [[Class]], которое для обычного объекта как раз и есть "Object". Для дат оно будет Date, для массивов — Array и т.п.
К сожалению, большинство объектов переопределяют toString на свой собственный. Поэтому мы будем использовать технику, которая называется «одалживание метода» («method borrowing»).
Мы возьмем функцию toString от стандартного объекта и будем запускать его в контексте тех значений, для которых нужно получить тип:
var toStandardString = {}.toString; // (1)
var arr = [1,2];
alert( toStandardString.call(arr) ); // (2) [object Array]
var date = new Date;
alert( toStandardString.call(date) ); // [object Date]
var cls = toStandardString.call(date).slice(8, -1); // (3)
alert(cls); // Date
Разберем происходящее более подробно.
- Можно переписать эту строку в две:
var obj = {}; var toStandardString = obj.toString;Иначе говоря, мы создаём пустой объект
{}и копируем ссылку на его методtoStringв переменнуюtoStandardString.Мы делаем это, потому что внутренняя реализация
toStringстандартного объектаObjectвозвращает[[Class]]. У других объектов (Date,Arrayи т.п.)toStringсвой и для этой цели не подойдёт. - Вызываем скопированный метод в контексте нужного объекта
obj.Мы могли бы поступить проще:
var arr = [1,2]; arr.toStandardString = {}.toString; alert( arr.toStandardString() ); // [object Array].. Но зачем копировать лишнее свойство в объект? СинтаксисtoStandardString.call(arr)делает то же самое, поэтому используем его. - Все, класс получен. При желании можно убрать обертку
[object ...], взяв подстрокуslice(8,-1). Использована переменнаяcls, т.к.class— зарезервированное слово в JavaScript.
Метод также работает с примитивами:
alert( toStandardString.call(123) ) // [object Number]
alert( toStandardString.call("строка")) ) // [object String]
… Кроме специальных значений null и undefined, поскольку вызов call с аргументами null или undefined, по спецификации, передает this = window.
Метод используется только для встроенных объектов. Для пользовательских конструкторов [[Class]] = "Object":
function Animal(name) {
this.name = name;
}
var animal = new Animal("Винни-пух");
var cls = {}.toString.call( animal );
alert(cls); // [object Object]
«Утиная» типизация
Утиная типизация основана на одной известной пословице: «If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck (who cares what it really is)».
В переводе: «Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)».
Смысл утиной типизации — в проверке методов и свойств, безотносительно типа объекта.
Проверить массив мы можем, уточнив наличие метода splice:
var x = [1,2,3];
if (x.splice) {
alert('Массив!');
}
Обратите внимание — в if(x.splice) мы не вызываем метод x.splice(), а пробуем получить само свойство x.splice. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте true.
Проверить на дату можно, проверив наличие метода getTime:
var x = new Date();
if (x.getTime) {
alert('Дата!');
}
С виду такая проверка хрупка, ее можно сломать, передав похожий объект с тем же методом. Но на практике утиная типизация хорошо и стабильно работает, особенно когда важен не сам тип, а поддержка методов.
Проверка типа для пользовательских объектов
Для проверки, кем был создан объект, есть оператор instanceof.
Синтаксис: obj instanceof Func.
Например:
function Animal(name) {
this.name = name;
}
var animal = new Animal("Винни-пух");
*!*
alert( animal instanceof Animal ); // true
*/!*
Оператор instanceof также работает для встроенных объектов:
var d = new Date();
alert(d instanceof Date); // true
function f() { }
alert(f instanceof Function); // true
Оператор instanceof может лишь осуществить проверку, он не позволяет получить тип в виде строки, но в большинстве случаев этого и не требуется.
[[Class]] надежнее instanceof?Как мы видели, instanceof успешно работает со встроенными объектами:
var arr = [1,2,3]; alert(arr instanceof Array); // true
…Однако, есть случай, когда такой способ нас подведет. А именно, если объект создан в другом окне или iframe, а оттуда передан в текущее окно.
При этом arr instanceof Array вернет false, т.к. в каждом окне и фрейме — свой собственный набор встроенных объектов. Массив arr является Array в контексте того window.
Метод [[Class]] свободен от этого недостатка.
Итого
Для получения типа есть два способа:
typeof- Хорош для примитивов и функций, врёт про
null. - Свойство
[[Class]] - Можно получить, используя
{}.toString.call(obj). Это свойство содержит тип для встроенных объектов и примитивов, кромеnullиundefined.
Для проверки типов есть еще два способа:
- Утиная типизация
- Можно проверить поддержку метода.
- Оператор
instanceof - Работает с любыми объектами, встроенными и созданными посетителем при помощи конструкторов:
if (obj instanceof User) { ... }.
Напишите функцию outputDate(date), которая выводит дату в формате dd.mm.yy.
Ее первый аргумент должен содержать дату в одном из видов:
- Как объект
Date. - Как строку в формате
yyyy-mm-dd. - Как число секунд с
01.01.1970. - Как массив
[гггг, мм, дд], месяц начинается с нуля
Для этого вам понадобится определить тип данных аргумента и, при необходимости, преобразовать входные данные в нужный формат.
Пример работы:
function outputDate(date) { /* ваш код */
outputDate( '2011-10-02' ); // 02.10.11
outputDate( 1234567890 ); // 14.02.09
outputDate( [2000,0,1] ); // 01.01.00
outputDate( new Date(2000,0,1) ); // 01.01.00
Для определения примитивного типа строка/число подойдет оператор typeof.
Примеры его работы:
alert(typeof 123); // "number" alert(typeof "строка"); // "string" alert(typeof new Date()); // "object" alert(typeof []); // "object"
Оператор typeof не умеет различать разные типы объектов, они для него все на одно лицо: "object". Поэтому он не сможет отличить Date от Array.
Используем для того, чтобы их различать, свойство [[Class]].
Функция:
function outputDate(date) {
if (typeof date == 'number') {
// перевести секунды в миллисекунды и преобразовать к Date
date = new Date(date*1000);
} else if(typeof date == 'string') {
// разобрать строку и преобразовать к Date
date = date.split('-');
date = new Date(date[0], date[1]-1, date[2]);
} else if ( {}.toString.call(date) == '[object Array]' ) {
date = new Date(date[0], date[1], date[2]);
}
var day = date.getDate();
if (day < 10) day = '0' + day;
var month = date.getMonth()+1;
if (month < 10) month = '0' + month;
// взять 2 последние цифры года
var year = date.getFullYear() % 100;
if (year < 10) year = '0' + year;
var formattedDate = day + '.' + month + '.' + year;
alert(formattedDate);
}
outputDate( '2011-10-02' ); // 02.10.11
outputDate( 1234567890 ); // 14.02.09
outputDate( [2000,0,1] ); // 01.01.00
outputDate( new Date(2000,0,1) ); // 01.01.00
Есть код для проверки, является ли значение объектом Object:
function isObject(x) {
if (x == null) return false; // (1)
return {}.toString.call(x) == "[object Object]"; // (2)
}
Как будет работать этот код в случае вызова isObject(undefined)?
- Какая строка
isObjectвернёт ответ:(1)или(2)? - Что будет, если убрать строку
(1)? Т.е. что вернёт{}.toString.call(undefined)?
- Так как
undefined == null, то строка(1)вернётfalse. - Если же убрать строку
(1), то наблюдается кросс-браузерный «зоопарк» с{}.toString.call(undefined).По стандарту, если нестрогий режим, то вызов
f.call(null/undefined)передаёт вfглобальный объект в качестве контекстаthis. Так что браузер должен метод{}.toStringзапустить в контекстеwindow. А далее — результат зависит от того, какое у него свойство[[Class]].В реальности же — попробуйте сами.. Некоторые браузеры используют контекст
undefined, некоторыеwindow— нет…// возможны варианты: // [object Undefined] // [object global] // [object DOMWindow] // [object Object] alert( {}.toString.call(undefined) );
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.