Типы данных: [[Class]], instanceof и утки
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/instanceof.
Время от времени бывает удобно создавать так называемые «полиморфные» функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.
Для реализации такой возможности нужен способ определить тип переменной.
Оператор typeof
Мы уже знакомы с простейшим способом – оператором typeof.
Оператор typeof надёжно работает с примитивными типами, кроме null , а также с функциями. Он возвращает для них тип в виде строки:
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’ :
alert( typeof <> ); // 'object' alert( typeof [] ); // 'object' alert( typeof new Date ); // 'object'
Поэтому различить их при помощи typeof нельзя, и в этом его основной недостаток.
Секретное свойство [[Class]]
Для встроенных объектов есть одна «секретная» возможность узнать их тип, которая связана с методом toString .
Во всех встроенных объектах есть специальное свойство [[Class]] , в котором хранится информация о его типе или конструкторе.
Оно взято в квадратные скобки, так как это свойство – внутреннее. Явно получить его нельзя, но можно прочитать его «в обход», воспользовавшись методом toString стандартного объекта Object .
Его внутренняя реализация выводит [[Class]] в небольшом обрамлении, как «[object значение]» .
var toString = <>.toString; var arr = [1, 2]; alert( toString.call(arr) ); // [object Array] var date = new Date; alert( toString.call(date) ); // [object Date] var user = < name: "Вася" >; alert( toString.call(user) ); // [object Object]
В первой строке мы взяли метод toString , принадлежащий именно стандартному объекту <> . Нам пришлось это сделать, так как у Date и Array – свои собственные методы toString , которые работают иначе.
Затем мы вызываем этот toString в контексте нужного объекта obj , и он возвращает его внутреннее, невидимое другими способами, свойство [[Class]] .
Для получения [[Class]] нужна именно внутренняя реализация toString стандартного объекта Object , другая не подойдёт.
К счастью, методы в JavaScript – это всего лишь функции-свойства объекта, которые можно скопировать в переменную и применить на другом объекте через call/apply . Что мы и делаем для <>.toString .
Метод также можно использовать с примитивами:
alert( <>.toString.call(123) ); // [object Number] alert( <>.toString.call("строка") ); // [object String]
При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку <>.toString.call(. ) – будет ошибка. С другой стороны, вызов alert( <>.toString. ) – работает.
Эта ошибка возникает потому, что фигурные скобки < >в основном потоке кода интерпретируются как блок. Интерпретатор читает <>.toString.call(. ) так:
< >// пустой блок кода .toString.call(. ) // а что это за точка в начале? не понимаю, ошибка!
Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки ( <>.toString. ) тоже сработает нормально.
Для большего удобства можно сделать функцию getClass , которая будет возвращать только сам [[Class]] :
function getClass(obj) < return <>.toString.call(obj).slice(8, -1); > alert( getClass(new Date) ); // Date alert( getClass([1, 2, 3]) ); // Array
Заметим, что свойство [[Class]] есть и доступно для чтения указанным способом – у всех встроенных объектов. Но его нет у объектов, которые создают наши функции. Точнее, оно есть, но равно всегда «Object» .
function User() <> var user = new User(); alert( <>.toString.call(user) ); // [object Object], не [object User]
Поэтому узнать тип таким образом можно только для встроенных объектов.
Метод Array.isArray()
Для проверки типа на массив есть специальный метод: Array.isArray(arr) . Он возвращает true только если arr – массив:
alert( Array.isArray([1,2,3]) ); // true alert( Array.isArray("not array")); // false
Но этот метод – единственный в своём роде.
Других аналогичных, типа Object.isObject , Date.isDate – нет.
Оператор instanceof
Оператор instanceof позволяет проверить, создан ли объект данной функцией, причём работает для любых функций – как встроенных, так и наших.
function User() <> var user = new User(); alert( user instanceof User ); // true
Таким образом, instanceof , в отличие от [[Class]] и typeof может помочь выяснить тип для новых объектов, созданных нашими конструкторами.
Заметим, что оператор instanceof – сложнее, чем кажется. Он учитывает наследование, которое мы пока не проходили, но скоро изучим и затем вернёмся к 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 :
var something = [1, 2, 3]; if (something.splice)
Обратите внимание – в if мы не вызываем метод something.splice() , а пробуем получить само свойство something.splice . Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте true .
Проверить на дату можно, определив наличие метода getTime :
var x = new Date(); if (x.getTime) < alert( 'Дата!' ); alert( x.getTime() ); // работаем с датой >
С виду такая проверка хрупка, её можно «сломать», передав похожий объект с тем же методом.
Но как раз в этом и есть смысл утиной типизации: если объект похож на дату, у него есть методы даты, то будем работать с ним как с датой (какая разница, что это на самом деле).
То есть мы намеренно позволяем передать в код нечто менее конкретное, чем определённый тип, чтобы сделать его более универсальным.
Если говорить словами «классического программирования», то «duck typing» – это проверка реализации объектом требуемого интерфейса. Если реализует – ок, используем его. Если нет – значит это что-то другое.
Пример полиморфной функции
Пример полиморфной функции – sayHi(who) , которая будет говорить «Привет» своему аргументу, причём если передан массив – то «Привет» каждому:
function sayHi(who) < if (Array.isArray(who)) < who.forEach(sayHi); >else < alert( 'Привет, ' + who ); >> // Вызов с примитивным аргументом sayHi("Вася"); // Привет, Вася // Вызов с массивом sayHi(["Саша", "Петя"]); // Привет, Саша. Петя // Вызов с вложенными массивами - тоже работает! sayHi(["Саша", "Петя", ["Маша", "Юля"]]); // Привет Саша..Петя..Маша..Юля
Проверку на массив в этом примере можно заменить на «утиную» – нам ведь нужен только метод forEach :
Итого
Для написания полиморфных (это удобно!) функций нам нужна проверка типов.
- Для примитивов с ней отлично справляется оператор typeof . У него две особенности:
- Он считает null объектом, это внутренняя ошибка в языке.
- Для функций он возвращает function , по стандарту функция не считается базовым типом, но на практике это удобно и полезно.
Задачи
Полиморфная функция formatDate
Напишите функцию formatDate(date) , которая возвращает дату в формате dd.mm.yy .
Её первый аргумент должен содержать дату в одном из видов:
- Как объект Date .
- Как строку, например yyyy-mm-dd или другую в стандартном формате даты.
- Как число секунд с 01.01.1970 .
- Как массив [гггг, мм, дд] , месяц начинается с нуля
Для этого вам понадобится определить тип данных аргумента и, при необходимости, преобразовать входные данные в нужный формат.
function formatDate(date) < /* ваш код */ >alert( formatDate('2011-10-02') ); // 02.10.11 alert( formatDate(1234567890) ); // 14.02.09 alert( formatDate([2014, 0, 1]) ); // 01.01.14 alert( formatDate(new Date(2014, 0, 1)) ); // 01.01.14
Для определения примитивного типа строка/число подойдёт оператор typeof.
alert( typeof 123 ); // "number" alert( typeof "строка" ); // "string" alert( typeof new Date() ); // "object" alert( typeof [] ); // "object"
Оператор typeof не умеет различать разные типы объектов, они для него все на одно лицо: «object» . Поэтому он не сможет отличить Date от Array .
Для отличия Array используем вызов Array.isArray . Если он неверен, значит у нас дата.
function formatDate(date) < if (typeof date == 'number') < // перевести секунды в миллисекунды и преобразовать к Date date = new Date(date * 1000); >else if (typeof date == 'string') < // строка в стандартном формате автоматически будет разобрана в дату date = new Date(date); >else if (Array.isArray(date)) < date = new Date(date[0], date[1], date[2]); >// преобразования для поддержки полиморфизма завершены, // теперь мы работаем с датой (форматируем её) return date.toLocaleString("ru", ); /* // можно и вручную, если лень добавлять в старый IE поддержку локализации 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; return formattedDate; */ >