В этой главе мы рассмотрим, как создавать полиморфные функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.
Для реализации такой возможности нужен способ определить тип переменной. И здесь в JavaScript есть целый «зоопарк» способов, но мы в нем сейчас разберемся.
Как мы знаем, существует несколько примитивных типов:
null
- Специальный тип, содержит только значение
null
. undefined
- Специальный тип, содержит только значение
undefined
. number
- Числа:
0
, 3.14
, а также значения NaN
и Infinity
boolean
true
, false
.string
- Строки, такие как
"Мяу"
или пустая строка ""
.
Все остальные значения являются объектами, включая функции и массивы.
Оператор typeof
Оператор typeof
возвращает тип аргумента. У него есть два синтаксиса:
- Синтаксис оператора:
typeof x
.
- Синтаксис функции:
typeof(x)
.
Работают они одинаково, но первый синтаксис короче.
Результатом typeof
является строка, содержащая тип:
Последние две строки помечены, потому что typeof
ведет себя в них по-особому.
- Результат
typeof null == "object"
— это официально признанная ошибка в языке, которая сохраняется для совместимости.
На самом деле null
— это не объект, а примитив. Это сразу видно, если попытаться присвоить ему свойство:
- Для функции
f
значением typeof f
является "function"
. Конечно же, функция является объектом. С другой стороны, такое выделение функций на практике скорее плюс, т.к. позволяет легко определить функцию.
В старом коде можно иногда увидеть код вроде такого:
if ( typeof jQuery !== 'undefined' ) { |
Его автор, видимо, хочет проверить, существует ли переменная
jQuery
. Причем, он имеет в виду именно глобальную переменную
jQuery
, которая создается во внешнем скрипте, так как про свои локальные он и так все знает.
Более короткий код if (jQuery)
выдаст ошибку, если переменная не определена, аtypeof jQuery
в таких случаях ошибку не выдает, а возвращает undefined
.
Но как раз здесь typeof
не нужен! Есть другой способ:
1 |
if (window.jQuery !== undefined ) { ... } |
4 |
if (window.jQuery) { ... } |
При доступе к глобальной переменной через
window
не будет ошибки, ведь по синтаксису это — обращение к свойству объекта, а такие обращения при отсутствующем свойстве просто возвращают
undefined
.
Оператор typeof
надежно работает с примитивными типами, кроме null
, а также с функциями. Но обычные объекты, массивы и даты для typeof
все на одно лицо, они имеют тип 'object'
:
Поэтому различить их при помощи typeof
нельзя.
[[Class]]
для встроенных объектов
Основная проблема typeof
— неумение различать объекты, кроме функций. Но есть и другой способ получения типа.
У всех встроенных объектов есть скрытое свойство [[Class]]
. Оно равно "Array"
для массивов,"Date"
для дат и т.п.
Это свойство нельзя получить напрямую, есть трюк для того, чтобы прочитать его.
Дело в том, что toString
от стандартного объекта выводит [[Class]]
в небольшой обертке. Например:
Здесь внутри [object ...]
указано как раз значение [[Class]]
, которое для обычного объекта как раз и есть "Object"
. Для дат оно будет Date
, для массивов — Array
и т.п.
Большинство встроенных объектов в JavaScript имеют свой собственный метод toString
. Поэтому мы будем использовать технику, которая называется «одалживание метода» («method borrowing»).
Мы возьмем функцию toString
от стандартного объекта и будем запускать его в контексте тех значений, для которых нужно получить тип:
Разберем происходящее более подробно.
- Можно переписать эту строку в две:
var toClass = obj.toString; |
Иначе говоря, мы создаем пустой объект {}
и копируем ссылку на его метод toString
в переменную toClass
.
Мы делаем это, потому что внутренняя реализация toString
стандартного объекта Object
возвращает [[Class]]
. У других объектов (Date
, Array
и т.п.) toString
свой и для этой цели не подойдет.
- Вызываем скопированный метод в контексте нужного объекта
obj
.
Мы могли бы поступить проще:
…Но зачем копировать лишнее свойство в объект? Синтаксис toClass.call(arr)
делает то же самое, поэтому используем его.
- Все, класс получен. Об этом говорит сайт https://intellect.icu . При желании можно убрать обертку
[object ...]
, взяв подстроку вызовомslice(8,-1)
.
Метод также работает с примитивами:
…Но без use strict
вызов call
с аргументами null
или undefined
передает this = window
. Таково поведение старого стандарта JavaScript.
Этот метод может дать тип только для встроенных объектов. Для пользовательских конструкторов всегда [[Class]] = "Object"
:
При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку {}.toString.call(...)
— будет ошибка. С другой стороны, вызовalert( {}.toString... )
— работает.
Эта ошибка возникает потому, что фигурные скобки { }
в основном потоке кода интерпретируются как блок. Интерпретатор читает {}.toString.call(...)
так:
Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки
( {}.toString... )
тоже сработает нормально.
«Утиная» типизация
Утиная типизация основана на одной известной пословице: «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)».
В переводе: «Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)».
Смысл утиной типизации — в проверке методов и свойств, безотносительно типа объекта.
Проверить массив мы можем, уточнив наличие метода splice
:
Обратите внимание — в if(x.splice)
мы не вызываем метод x.splice()
, а пробуем получить само свойство x.splice
. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте true
.
Проверить на дату можно, проверив наличие метода getTime
:
С виду такая проверка хрупка, ее можно сломать, передав похожий объект с тем же методом. Но на практике утиная типизация хорошо и стабильно работает, особенно когда важен не сам тип, а поддержка методов.
Проверка типа для пользовательских объектов
Для проверки, кем был создан объект, есть оператор instanceof
.
Синтаксис: obj instanceof Func
.
Например:
Оператор instanceof
также работает для встроенных объектов:
Оператор instanceof
может лишь осуществить проверку, он не позволяет получить тип в виде строки, но в большинстве случаев этого и не требуется.
Как мы видели, instanceof
успешно работает со встроенными объектами:
…Однако, есть случай, когда такой способ нас подведет. А именно, если объект создан в другом окне или iframe
, а оттуда передан в текущее окно.
При этом arr instanceof Array
вернет false
, т.к. в каждом окне и фрейме — свой собственный набор встроенных объектов. Массив arr
является Array
в контекстетого window
.
Метод [[Class]]
свободен от этого недостатка.
Полиморфизм
Используем проверку типов для того, чтобы создать полиморфную функцию sayHi
.
Она будет работать в трех режимах:
- Без аргументов: выводит
Привет
.
- С аргументом, который не является массивом: выводит «привет» и этот аргумент.
- С аргументом, который является массивом — говорит всем «привет».
Пример такой функции:
Обратите внимание, получилась даже поддержка вложенных массивов
Итого
Для получения типа есть два способа:
typeof
- Хорош для примитивов и функций, врет про
null
. - Свойство
[[Class]]
- Можно получить, используя
{}.toString.call(obj)
. Это свойство содержит тип для встроенных объектов и примитивов, кроме null
и undefined
.
Для проверки типов есть еще два способа:
- Утиная типизация
- Можно проверить поддержку метода.
- Оператор
instanceof
- Работает с любыми объектами, встроенными и созданными посетителем при помощи конструкторов:
if (obj instanceof User) { ... }
.
Используется проверка типа, как правило, для создания полиморфных функций, то есть таких, которые по-разному работают в зависимости от типа аргумента.
Важность: 5
Для определения примитивного типа строка/число подойдет оператор typeof.
Примеры его работы:
Оператор typeof
не умеет различать разные типы объектов, они для него все на одно лицо: "object"
. Поэтому он не сможет отличить Date
от Array
.
Используем для того, чтобы их различать, свойство [[Class]]
.
Функция:
[Открыть задачу в новом окне]
Важность: 2
- Да. Так как
undefined == null
, то строка (1)
завершит выполнение возвратомfalse
.
- Если убрать строку
(1)
, то браузеры ведут себя более интересно…
По идее, если нестрогий режим, то вызов f.call(null/undefined)
должен передать в f
глобальный объект в качестве контекста this
. Так что браузер должен метод {}.toString
запустить в контексте window
. А далее — результат зависит от того, какое у него свойство [[Class]]
.
В реальности же — большинство браузеров применяют здесь современный стандарт и возвращают [object Undefined]
. Попробуйте сами..
Комментарии
Оставить комментарий
Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)
Термины: Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)