13 KiB
Типы данных: Class, instanceof и утки
Время от времени бывает удобно создавать так называемые "полиморфные" функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.
Для реализации такой возможности нужен способ определить тип переменной.
Оператор typeof
Мы уже знакомы с простейшим способом -- оператором typeof.
Оператор typeof
надежно работает с примитивными типами, кроме null
, а также с функциями. Он возвращает для них тип в виде строки:
//+ run no-beautify
alert( typeof 1 ); // 'number'
alert( typeof true ); // 'boolean'
alert( typeof "Текст" ); // 'string'
alert( typeof undefined ); // 'undefined'
alert( typeof null ); // 'object' (ошибка в языке)
alert( typeof alert ); // 'function'
...Но все объекты, включая массивы и даты для typeof
-- на одно лицо, они имеют один тип 'object'
:
//+ run
alert( typeof {} ); // 'object'
alert( typeof [] ); // 'object'
alert( typeof new Date ); // 'object'
Поэтому различить их при помощи typeof
нельзя, и в этом его основной недостаток.
Секретное свойство Class
Для встроенных объектов есть одна "секретная" возможность узнать их тип, которая связана с методом toString
.
Во всех встроенных объектах есть специальное свойство [[Class]]
, в котором хранится информация о его типе или конструкторе.
Оно взято в квадратные скобки, так как это свойство -- внутреннее. Явно получить его нельзя, но можно прочитать его "в обход", воспользовавшись методом toString
стандартного объекта Object
.
Его внутренняя реализация выводит [[Class]]
в небольшом обрамлении, как "[object значение]"
.
Например:
//+ run
var toString = {}.toString;
var arr = [1, 2];
alert( toString.call(arr) ); // [object Array]
var date = new Date;
alert( toString.call(date) ); // [object Date]
var obj = { name: "Вася" };
alert( toString.call(date) ); // [object Object]
В первой строке мы взяли метод toString
, принадлежащий именно стандартному объекту {}
. Нам пришлось это сделать, так как у Date
и Array
-- свои собственные методы toString
, которые работают иначе.
Затем мы вызываем этот toString
в контексте нужного объекта obj
, и он возвращает его внутреннее, невидимое другими способами, свойство [[Class]]
.
Для получения [[Class]]
нужна именно внутренняя реализация toString
стандартного объекта Object
, другая не подойдёт.
К счастью, методы в JavaScript -- это всего лишь функции-свойства объекта, которые можно скопировать в переменную и применить на другом объекте через call/apply
. Что мы и делаем для {}.toString
.
Метод также можно использовать с примитивами:
//+ run
alert( {}.toString.call(123) ); // [object Number]
alert( {}.toString.call("строка") ); // [object String]
[warn header="Вызов {}.toString
в консоли может выдать ошибку"]
При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку {}.toString.call(...)
-- будет ошибка. С другой стороны, вызов alert( {}.toString... )
-- работает.
Эта ошибка возникает потому, что фигурные скобки { }
в основном потоке кода интерпретируются как блок. Интерпретатор читает {}.toString.call(...)
так:
//+ no-beautify
{ } // пустой блок кода
.toString.call(...) // а что это за точка в начале? не понимаю, ошибка!
Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки ( {}.toString... )
тоже сработает нормально.
[/warn]
Для большего удобства можно сделать функцию getClass
, которая будет возвращать только сам [[Class]]
:
//+ run
function getClass(obj) {
return {}.toString.call(obj).slice(8, -1);
}
alert( getClass(new Date) ); // Date
alert( getClass([1, 2, 3]) ); // Array
Заметим, что свойство [[Class]]
есть и доступно для чтения указанным способом -- у всех встроенных объектов. Но его нет у объектов, которые создают наши функции. Точнее, оно есть, но равно всегда "Object"
.
Например:
//+ run
function User() {}
var user = new User();
alert( {}.toString.call(user) ); // [object Object], не [object User]
Поэтому узнать тип таким образом можно только для встроенных объектов.
Метод Array.isArray()
Для проверки на массивов есть специальный метод: Array.isArray(arr)
. Он возвращает true
только если arr
-- массив:
//+ run
alert( Array.isArray([1,2,3]) ); // true
alert( Array.isArray("not array")); // false
Но этот метод -- единственный в своём роде.
Других аналогичных, типа Object.isObject
, Date.isDate
-- нет.
Оператор instanceof
Оператор instanceof
позволяет проверить, создан ли объект данной функцией, причём работает для любых функций -- как встроенных, так и наших.
//+ run
function User() {}
var user = new User();
alert( user instanceof User ); // true
Таким образом, instanceof
, в отличие от [[Class]]
и typeof
может помочь выяснить тип для новых объектов, созданных нашими конструкторами.
Заметим, что оператор instanceof
-- сложнее, чем кажется. Он учитывает наследование, которое мы пока не проходили, но скоро изучим, и затем вернёмся к instanceof
в главе .
Утиная типизация
Альтернативный подход к типу -- "утиная типизация", которая основана на одной известной пословице: "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)".
В переводе: "Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)".
Смысл утиной типизации -- в проверке необходимых методов и свойств.
Например, мы можем проверить, что объект -- массив, не вызывая Array.isArray
, а просто уточнив наличие важного для нас метода, например splice
:
//+ run
var something = [1, 2, 3];
if (something.splice) {
alert( 'Это утка! То есть, массив!' );
}
Обратите внимание -- в if
мы не вызываем метод something.splice()
, а пробуем получить само свойство something.splice
. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте true
.
Проверить на дату можно, определив наличие метода getTime
:
//+ run
var x = new Date();
if (x.getTime) {
alert( 'Дата!' );
alert( x.getTime() ); // работаем с датой
}
С виду такая проверка хрупка, ее можно "сломать", передав похожий объект с тем же методом.
Но как раз в этом и есть смысл утиной типизации: если объект похож на дату, у него есть методы даты, то будем работать с ним как с датой (какая разница, что это на самом деле).
То есть, мы намеренно позволяем передать в код нечто менее конкретное, чем определённый тип, чтобы сделать его более универсальным.
[smart header="Проверка интерфейса"] Если говорить словами "классического программирования", то "duck typing" -- это проверка реализации объектом требуемого интерфейса. Если реализует -- ок, используем его. Если нет -- значит это что-то другое. [/smart]
Пример полиморфной функции
Пример полиморфной функции -- sayHi(who)
, которая будет говорить "Привет" своему аргументу, причём если передан массив -- то "Привет" каждому:
//+ run
function sayHi(who) {
if (Array.isArray(who)) {
who.forEach(sayHi);
} else {
alert( 'Привет, ' + who );
}
}
// Вызов с примитивным аргументом
sayHi("Вася"); // Привет, Вася
// Вызов с массивом
sayHi(["Саша", "Петя"]); // Привет, Саша... Петя
// Вызов с вложенными массивами - тоже работает!
sayHi(["Саша", "Петя", ["Маша", "Юля"]]); // Привет Саша..Петя..Маша..Юля
Проверку на массив в этом примере можно заменить на "утиную" -- нам ведь нужен только метод forEach
:
//+ run
function sayHi(who) {
if (who.forEach) { // если есть forEach
who.forEach(sayHi); // предполагаем, что он ведёт себя "как надо"
} else {
alert( 'Привет, ' + who );
}
}
Итого
Для написания полиморфных (это удобно!) функций нам нужна проверка типов.
- Для примитивов с ней отлично справляется оператор `typeof`.
У него две особенности:
- Он считает `null` объектом, это внутренняя ошибка в языке.
- Для функций он возвращает `function`, по стандарту функция не считается базовым типом, но на практике это удобно и полезно.
- Для встроенных объектов мы можем получить тип из скрытого свойства `Class`, при помощи вызова `{}.toString.call(obj).slice(8, -1)`. Не работает для конструкторов, которые объявлены нами.
- Оператор `obj instanceof Func` проверяет, создан ли объект `obj` функцией `Func`, работает для любых конструкторов. Более подробно мы разберём его в главе [](/instanceof).
- И, наконец, зачастую достаточно проверить не сам тип, а просто наличие нужных свойств или методов. Это называется "утиная типизация".