В этой главе мы рассмотрим, как создавать полиморфные функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.
Для реализации такой возможности нужен способ определить тип переменной. И здесь в JavaScript есть целый «зоопарк» способов, но мы в нем сейчас разберемся.
Как мы знаем, существует несколько примитивных типов:
null- Специальный тип, содержит только значение
null. undefined- Специальный тип, содержит только значение
undefined. number- Числа:
0, 3.14, а также значения NaN и Infinity booleantrue, 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)