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