|
| 1 | +# Типы данных: [[Class]], instanceof и утки |
| 2 | + |
| 3 | +Время от времени бывает удобно создавать так называемые "полиморфные" функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты. |
| 4 | + |
| 5 | +Для реализации такой возможности нужен способ определить тип переменной. |
| 6 | + |
| 7 | +## Оператор typeof |
| 8 | + |
| 9 | +Мы уже знакомы с простейшим способом -- оператором [typeof](#type-typeof). |
| 10 | + |
| 11 | +Оператор `typeof` надежно работает с примитивными типами, кроме `null`, а также с функциями. Он возвращает для них тип в виде строки: |
| 12 | + |
| 13 | +```js |
| 14 | +//+ run no-beautify |
| 15 | +alert( typeof 1 ); // 'number' |
| 16 | +alert( typeof true ); // 'boolean' |
| 17 | +alert( typeof "Текст" ); // 'string' |
| 18 | +alert( typeof undefined ); // 'undefined' |
| 19 | +alert( typeof null ); // 'object' (ошибка в языке) |
| 20 | +alert( typeof alert ); // 'function' |
| 21 | +``` |
| 22 | + |
| 23 | +...Но все объекты, включая массивы и даты для `typeof` -- на одно лицо, они имеют один тип `'object'`: |
| 24 | + |
| 25 | +```js |
| 26 | +//+ run |
| 27 | +alert( typeof {} ); // 'object' |
| 28 | +alert( typeof [] ); // 'object' |
| 29 | +alert( typeof new Date ); // 'object' |
| 30 | +``` |
| 31 | + |
| 32 | +Поэтому различить их при помощи `typeof` нельзя, и в этом его основной недостаток. |
| 33 | + |
| 34 | +## Секретное свойство [[Class]] |
| 35 | + |
| 36 | +Для встроенных объектов есть одна "секретная" возможность узнать их тип, которая связана с методом `toString`. |
| 37 | + |
| 38 | +Во всех встроенных объектах есть специальное свойство `[[Class]]`, в котором хранится информация о его типе или конструкторе. |
| 39 | + |
| 40 | +Оно взято в квадратные скобки, так как это свойство -- внутреннее. Явно получить его нельзя, но можно прочитать его "в обход", воспользовавшись методом `toString` стандартного объекта `Object`. |
| 41 | + |
| 42 | +Его внутренняя реализация выводит `[[Class]]` в небольшом обрамлении, как `"[object значение]"`. |
| 43 | + |
| 44 | +Например: |
| 45 | + |
| 46 | +```js |
| 47 | +//+ run |
| 48 | +var toString = {}.toString; |
| 49 | + |
| 50 | +var arr = [1, 2]; |
| 51 | +alert( toString.call(arr) ); // [object Array] |
| 52 | + |
| 53 | +var date = new Date; |
| 54 | +alert( toString.call(date) ); // [object Date] |
| 55 | + |
| 56 | +var obj = { name: "Вася" }; |
| 57 | +alert( toString.call(date) ); // [object Object] |
| 58 | +``` |
| 59 | + |
| 60 | +В первой строке мы взяли метод `toString`, принадлежащий именно стандартному объекту `{}`. Нам пришлось это сделать, так как у `Date` и `Array` -- свои собственные методы `toString`, которые работают иначе. |
| 61 | + |
| 62 | +Затем мы вызываем этот `toString` в контексте нужного объекта `obj`, и он возвращает его внутреннее, невидимое другими способами, свойство `[[Class]]`. |
| 63 | + |
| 64 | +**Для получения `[[Class]]` нужна именно внутренняя реализация `toString` стандартного объекта `Object`, другая не подойдёт.** |
| 65 | + |
| 66 | +К счастью, методы в JavaScript -- это всего лишь функции-свойства объекта, которые можно скопировать в переменную и применить на другом объекте через `call/apply`. Что мы и делаем для `{}.toString`. |
| 67 | + |
| 68 | +Метод также можно использовать с примитивами: |
| 69 | + |
| 70 | +```js |
| 71 | +//+ run |
| 72 | +alert( {}.toString.call(123) ); // [object Number] |
| 73 | +alert( {}.toString.call("строка") ); // [object String] |
| 74 | +``` |
| 75 | + |
| 76 | +[warn header="Вызов `{}.toString` в консоли может выдать ошибку"] |
| 77 | +При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку `{}.toString.call(...)` -- будет ошибка. С другой стороны, вызов `alert( {}.toString... )` -- работает. |
| 78 | + |
| 79 | +Эта ошибка возникает потому, что фигурные скобки `{ }` в основном потоке кода интерпретируются как блок. Интерпретатор читает `{}.toString.call(...)` так: |
| 80 | + |
| 81 | +```js |
| 82 | +//+ no-beautify |
| 83 | +{ } // пустой блок кода |
| 84 | +.toString.call(...) // а что это за точка в начале? не понимаю, ошибка! |
| 85 | +``` |
| 86 | + |
| 87 | +Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки `( {}.toString... )` тоже сработает нормально. |
| 88 | +[/warn] |
| 89 | + |
| 90 | + |
| 91 | +Для большего удобства можно сделать функцию `getClass`, которая будет возвращать только сам `[[Class]]`: |
| 92 | + |
| 93 | +```js |
| 94 | +//+ run |
| 95 | +function getClass(obj) { |
| 96 | + return {}.toString.call(obj).slice(8, -1); |
| 97 | +} |
| 98 | + |
| 99 | +alert( getClass(new Date) ); // Date |
| 100 | +alert( getClass([1, 2, 3]) ); // Array |
| 101 | +``` |
| 102 | + |
| 103 | +Заметим, что свойство `[[Class]]` есть и доступно для чтения указанным способом -- у всех *встроенных* объектов. Но его нет у объектов, которые создают *наши функции*. Точнее, оно есть, но равно всегда `"Object"`. |
| 104 | + |
| 105 | +Например: |
| 106 | + |
| 107 | +```js |
| 108 | +//+ run |
| 109 | +function User() {} |
| 110 | + |
| 111 | +var user = new User(); |
| 112 | + |
| 113 | +alert( {}.toString.call(user) ); // [object Object], не [object User] |
| 114 | +``` |
| 115 | + |
| 116 | +Поэтому узнать тип таким образом можно только для встроенных объектов. |
| 117 | + |
| 118 | +## Метод Array.isArray() |
| 119 | + |
| 120 | +Для проверки на массивов есть специальный метод: `Array.isArray(arr)`. Он возвращает `true` только если `arr` -- массив: |
| 121 | + |
| 122 | +```js |
| 123 | +//+ run |
| 124 | +alert( Array.isArray([1,2,3]) ); // true |
| 125 | +alert( Array.isArray("not array")); // false |
| 126 | +``` |
| 127 | + |
| 128 | +Но этот метод -- единственный в своём роде. |
| 129 | + |
| 130 | +Других аналогичных, типа `Object.isObject`, `Date.isDate` -- нет. |
| 131 | + |
| 132 | + |
| 133 | +## Оператор instanceof |
| 134 | + |
| 135 | +Оператор `instanceof` позволяет проверить, создан ли объект данной функцией, причём работает для любых функций -- как встроенных, так и наших. |
| 136 | + |
| 137 | +```js |
| 138 | +//+ run |
| 139 | +function User() {} |
| 140 | + |
| 141 | +var user = new User(); |
| 142 | + |
| 143 | +alert( user instanceof User ); // true |
| 144 | +``` |
| 145 | + |
| 146 | +Таким образом, `instanceof`, в отличие от `[[Class]]` и `typeof` может помочь выяснить тип для новых объектов, созданных нашими конструкторами. |
| 147 | + |
| 148 | +Заметим, что оператор `instanceof` -- сложнее, чем кажется. Он учитывает наследование, которое мы пока не проходили, но скоро изучим, и затем вернёмся к `instanceof` в главе [](/instanceof). |
| 149 | + |
| 150 | + |
| 151 | +## Утиная типизация |
| 152 | + |
| 153 | +Альтернативный подход к типу -- "утиная типизация", которая основана на одной известной пословице: *"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)"*. |
| 154 | + |
| 155 | +В переводе: *"Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)"*. |
| 156 | + |
| 157 | +Смысл утиной типизации -- в проверке необходимых методов и свойств. |
| 158 | + |
| 159 | +Например, мы можем проверить, что объект -- массив, не вызывая `Array.isArray`, а просто уточнив наличие важного для нас метода, например `splice`: |
| 160 | + |
| 161 | +```js |
| 162 | +//+ run |
| 163 | +var something = [1, 2, 3]; |
| 164 | + |
| 165 | +if (something.splice) { |
| 166 | + alert( 'Это утка! То есть, массив!' ); |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +Обратите внимание -- в `if` мы не вызываем метод `something.splice()`, а пробуем получить само свойство `something.splice`. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте `true`. |
| 171 | + |
| 172 | +Проверить на дату можно, определив наличие метода `getTime`: |
| 173 | + |
| 174 | +```js |
| 175 | +//+ run |
| 176 | +var x = new Date(); |
| 177 | + |
| 178 | +if (x.getTime) { |
| 179 | + alert( 'Дата!' ); |
| 180 | + alert( x.getTime() ); // работаем с датой |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +С виду такая проверка хрупка, ее можно "сломать", передав похожий объект с тем же методом. |
| 185 | + |
| 186 | +Но как раз в этом и есть смысл утиной типизации: если объект похож на дату, у него есть методы даты, то будем работать с ним как с датой (какая разница, что это на самом деле). |
| 187 | + |
| 188 | +То есть, мы намеренно позволяем передать в код нечто менее конкретное, чем определённый тип, чтобы сделать его более универсальным. |
| 189 | + |
| 190 | +[smart header="Проверка интерфейса"] |
| 191 | +Если говорить словами "классического программирования", то "duck typing" -- это проверка реализации объектом требуемого интерфейса. Если реализует -- ок, используем его. Если нет -- значит это что-то другое. |
| 192 | +[/smart] |
| 193 | + |
| 194 | + |
| 195 | +## Пример полиморфной функции |
| 196 | + |
| 197 | +Пример полиморфной функции -- `sayHi(who)`, которая будет говорить "Привет" своему аргументу, причём если передан массив -- то "Привет" каждому: |
| 198 | + |
| 199 | +```js |
| 200 | +//+ run |
| 201 | +function sayHi(who) { |
| 202 | + |
| 203 | + if (Array.isArray(who)) { |
| 204 | + who.forEach(sayHi); |
| 205 | + } else { |
| 206 | + alert( 'Привет, ' + who ); |
| 207 | + } |
| 208 | +} |
| 209 | + |
| 210 | +// Вызов с примитивным аргументом |
| 211 | +sayHi("Вася"); // Привет, Вася |
| 212 | + |
| 213 | +// Вызов с массивом |
| 214 | +sayHi(["Саша", "Петя"]); // Привет, Саша... Петя |
| 215 | + |
| 216 | +// Вызов с вложенными массивами - тоже работает! |
| 217 | +sayHi(["Саша", "Петя", ["Маша", "Юля"]]); // Привет Саша..Петя..Маша..Юля |
| 218 | +``` |
| 219 | + |
| 220 | +Проверку на массив в этом примере можно заменить на "утиную" -- нам ведь нужен только метод `forEach`: |
| 221 | + |
| 222 | +```js |
| 223 | +//+ run |
| 224 | +function sayHi(who) { |
| 225 | + |
| 226 | + if (who.forEach) { // если есть forEach |
| 227 | + who.forEach(sayHi); // предполагаем, что он ведёт себя "как надо" |
| 228 | + } else { |
| 229 | + alert( 'Привет, ' + who ); |
| 230 | + } |
| 231 | +} |
| 232 | +``` |
| 233 | + |
| 234 | +## Итого |
| 235 | + |
| 236 | +Для написания полиморфных (это удобно!) функций нам нужна проверка типов. |
| 237 | + |
| 238 | +<ul> |
| 239 | +<li>Для примитивов с ней отлично справляется оператор `typeof`. |
| 240 | + |
| 241 | +У него две особенности: |
| 242 | +<ol> |
| 243 | +<li>Он считает `null` объектом, это внутренняя ошибка в языке.</li> |
| 244 | +<li>Для функций он возвращает `function`, по стандарту функция не считается базовым типом, но на практике это удобно и полезно.</li> |
| 245 | +</ol> |
| 246 | +</li> |
| 247 | +<li>Для встроенных объектов мы можем получить тип из скрытого свойства `[[Class]]`, при помощи вызова `{}.toString.call(obj).slice(8, -1)`. Не работает для конструкторов, которые объявлены нами. |
| 248 | +</li> |
| 249 | +<li>Оператор `obj instanceof Func` проверяет, создан ли объект `obj` функцией `Func`, работает для любых конструкторов. Более подробно мы разберём его в главе [](/instanceof).</li> |
| 250 | +<li>И, наконец, зачастую достаточно проверить не сам тип, а просто наличие нужных свойств или методов. Это называется "утиная типизация".</li> |
| 251 | +</ul> |
| 252 | + |
0 commit comments