en.javascript.info/1-js/7-js-misc/1-class-instanceof/article.md
2015-04-01 19:08:41 +03:00

252 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Типы данных: [[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>