en.javascript.info/1-js/7-js-misc/1-class-instanceof/article.md
2015-03-22 00:36:11 +03:00

13 KiB
Raw Blame History

Типы данных: Class, instanceof и утки

Время от времени бывает удобно создавать так называемые "полиморфные" функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.

Для реализации такой возможности нужен способ определить тип переменной.

Оператор typeof

Мы уже знакомы с простейшим способом -- оператором typeof.

Оператор typeof надежно работает с примитивными типами, кроме null, а также с функциями. Он возвращает для них тип в виде строки:

//+ 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':

//+ run
alert( typeof {} ); // 'object'
alert( typeof [] ); // 'object'
alert( typeof new Date ); // 'object'

Поэтому различить их при помощи typeof нельзя, и в этом его основной недостаток.

Секретное свойство Class

Для встроенных объектов есть одна "секретная" возможность узнать их тип, которая связана с методом toString.

Во всех встроенных объектах есть специальное свойство [[Class]], в котором хранится информация о его типе или конструкторе.

Оно взято в квадратные скобки, так как это свойство -- внутреннее. Явно получить его нельзя, но можно прочитать его "в обход", воспользовавшись методом toString стандартного объекта Object.

Его внутренняя реализация выводит [[Class]] в небольшом обрамлении, как "[object значение]".

Например:

//+ 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 obj = { name: "Вася" };
alert( toString.call(date) ); // [object Object]

В первой строке мы взяли метод toString, принадлежащий именно стандартному объекту {}. Нам пришлось это сделать, так как у Date и Array -- свои собственные методы toString, которые работают иначе.

Затем мы вызываем этот toString в контексте нужного объекта obj, и он возвращает его внутреннее, невидимое другими способами, свойство [[Class]].

Для получения [[Class]] нужна именно внутренняя реализация toString стандартного объекта Object, другая не подойдёт.

К счастью, методы в JavaScript -- это всего лишь функции-свойства объекта, которые можно скопировать в переменную и применить на другом объекте через call/apply. Что мы и делаем для {}.toString.

Метод также можно использовать с примитивами:

//+ run
alert( {}.toString.call(123) ); // [object Number]
alert( {}.toString.call("строка") ); // [object String]

[warn header="Вызов {}.toString в консоли может выдать ошибку"] При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку {}.toString.call(...) -- будет ошибка. С другой стороны, вызов alert( {}.toString... ) -- работает.

Эта ошибка возникает потому, что фигурные скобки { } в основном потоке кода интерпретируются как блок. Интерпретатор читает {}.toString.call(...) так:

//+ no-beautify
{ } // пустой блок кода
.toString.call(...) // а что это за точка в начале? не понимаю, ошибка!

Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки ( {}.toString... ) тоже сработает нормально. [/warn]

Для большего удобства можно сделать функцию getClass, которая будет возвращать только сам [[Class]]:

//+ run
function getClass(obj) {
  return {}.toString.call(obj).slice(8, -1);
}

alert( getClass(new Date) ); // Date
alert( getClass([1, 2, 3]) ); // Array

Заметим, что свойство [[Class]] есть и доступно для чтения указанным способом -- у всех встроенных объектов. Но его нет у объектов, которые создают наши функции. Точнее, оно есть, но равно всегда "Object".

Например:

//+ run
function User() {}

var user = new User();

alert( {}.toString.call(user) ); // [object Object], не [object User]

Поэтому узнать тип таким образом можно только для встроенных объектов.

Метод Array.isArray()

Для проверки на массивов есть специальный метод: Array.isArray(arr). Он возвращает true только если arr -- массив:

//+ run
alert( Array.isArray([1,2,3]) ); // true
alert( Array.isArray("not array")); // false

Но этот метод -- единственный в своём роде.

Других аналогичных, типа Object.isObject, Date.isDate -- нет.

Оператор instanceof

Оператор instanceof позволяет проверить, создан ли объект данной функцией, причём работает для любых функций -- как встроенных, так и наших.

//+ run
function User() {}

var user = new User();

alert( user instanceof User ); // true

Таким образом, instanceof, в отличие от [[Class]] и typeof может помочь выяснить тип для новых объектов, созданных нашими конструкторами.

Заметим, что оператор 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:

//+ run
var something = [1, 2, 3];

if (something.splice) {
  alert( 'Это утка! То есть, массив!' );
}

Обратите внимание -- в if мы не вызываем метод something.splice(), а пробуем получить само свойство something.splice. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте true.

Проверить на дату можно, определив наличие метода getTime:

//+ run
var x = new Date();

if (x.getTime) {
  alert( 'Дата!' );
  alert( x.getTime() ); // работаем с датой
}

С виду такая проверка хрупка, ее можно "сломать", передав похожий объект с тем же методом.

Но как раз в этом и есть смысл утиной типизации: если объект похож на дату, у него есть методы даты, то будем работать с ним как с датой (какая разница, что это на самом деле).

То есть, мы намеренно позволяем передать в код нечто менее конкретное, чем определённый тип, чтобы сделать его более универсальным.

[smart header="Проверка интерфейса"] Если говорить словами "классического программирования", то "duck typing" -- это проверка реализации объектом требуемого интерфейса. Если реализует -- ок, используем его. Если нет -- значит это что-то другое. [/smart]

Пример полиморфной функции

Пример полиморфной функции -- sayHi(who), которая будет говорить "Привет" своему аргументу, причём если передан массив -- то "Привет" каждому:

//+ run
function sayHi(who) {

  if (Array.isArray(who)) { 
    who.forEach(sayHi);
  } else {
    alert( 'Привет, ' + who );
  }
}

// Вызов с примитивным аргументом
sayHi("Вася"); // Привет, Вася

// Вызов с массивом
sayHi(["Саша", "Петя"]); // Привет, Саша... Петя

// Вызов с вложенными массивами - тоже работает!
sayHi(["Саша", "Петя", ["Маша", "Юля"]]); // Привет Саша..Петя..Маша..Юля

Проверку на массив в этом примере можно заменить на "утиную" -- нам ведь нужен только метод forEach:

//+ run
function sayHi(who) {

  if (who.forEach) {  // если есть forEach
    who.forEach(sayHi); // предполагаем, что он ведёт себя "как надо"
  } else {
    alert( 'Привет, ' + who );
  }
}

Итого

Для написания полиморфных (это удобно!) функций нам нужна проверка типов.

  • Для примитивов с ней отлично справляется оператор `typeof`.

    У него две особенности:

    1. Он считает `null` объектом, это внутренняя ошибка в языке.
    2. Для функций он возвращает `function`, по стандарту функция не считается базовым типом, но на практике это удобно и полезно.
  • Для встроенных объектов мы можем получить тип из скрытого свойства `Class`, при помощи вызова `{}.toString.call(obj).slice(8, -1)`. Не работает для конструкторов, которые объявлены нами.
  • Оператор `obj instanceof Func` проверяет, создан ли объект `obj` функцией `Func`, работает для любых конструкторов. Более подробно мы разберём его в главе [](/instanceof).
  • И, наконец, зачастую достаточно проверить не сам тип, а просто наличие нужных свойств или методов. Это называется "утиная типизация".