252 lines
13 KiB
Markdown
252 lines
13 KiB
Markdown
# Типы данных: [[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 );
|
||
}
|
||
}
|
||
```
|
||
|
||
## Итого
|
||
|
||
Для написания полиморфных (это удобно!) функций нам нужна проверка типов.
|
||
|
||
<ul>
|
||
<li>Для примитивов с ней отлично справляется оператор `typeof`.
|
||
|
||
У него две особенности:
|
||
<ol>
|
||
<li>Он считает `null` объектом, это внутренняя ошибка в языке.</li>
|
||
<li>Для функций он возвращает `function`, по стандарту функция не считается базовым типом, но на практике это удобно и полезно.</li>
|
||
</ol>
|
||
</li>
|
||
<li>Для встроенных объектов мы можем получить тип из скрытого свойства `[[Class]]`, при помощи вызова `{}.toString.call(obj).slice(8, -1)`. Не работает для конструкторов, которые объявлены нами.
|
||
</li>
|
||
<li>Оператор `obj instanceof Func` проверяет, создан ли объект `obj` функцией `Func`, работает для любых конструкторов. Более подробно мы разберём его в главе [](/instanceof).</li>
|
||
<li>И, наконец, зачастую достаточно проверить не сам тип, а просто наличие нужных свойств или методов. Это называется "утиная типизация".</li>
|
||
</ul>
|
||
|