Как мы рассмотрели в прошлых статьях, система типов в JavaScript не так, чтобы уж очень стройная.
И есть ещё один теоретический нюанс. Object в JS, по-сути, отвечает за две совершенно различные сущности.
С одной стороны это объект из ООП: отдельная индивидуальность со своим поведением и интерфейсом.
С другой: хранилище структурированных данных. Тоже, что и struct в Си, Dict в Питоне, Hash в Перле или ассоциативные массивы в PHP.
В питоне Dict, конечно, тоже объект, но там мы чётко можем разобрать — это словарь а не что-то иное. В JS разобрать это сложнее.
Зачем?
Где нам это может пригодиться? Например, нам нужно написать функцию `merge()` для рекурсивного слияния структур. Зачем нам эта функция? Например, у нас есть список каких-то настроек по умолчанию:
var options = { /* Параметры AJAX-запроса */ 'ajax_request' : { 'url' : "/ajax/action/", 'method' : "POST", 'data' : { 'x': 1, 'y': 2 } }, /* Обработчик результата */ 'handler': function (result) { /* ... */ }, /* Периодичность отправки запроса */ 'period': 1000 }; |
Мы хотим для одного конкретного объекта переопределить некоторые настройки, оставив остальные со значениями по умолчанию:
var spec_options = { 'ajax_request': { 'method': "GET", // запрос другим методом 'data': { 'x': 3 // немного другие данные } }, 'handler': function (result) { // и другой обработчик /* ... */ } }; |
Теперь нам нужно рекурсивно смержить options
и spec_options
. Проходим по свойствам:
1. Нашли ajax_request
, он есть в обоих структурах и в обоих он — объект. Рекурсивно сливаем его.
2. Внутри есть method
, он — скаляр. Заменяем.
3. Нашли внутри data
— object, сливаем.
Дальше нашли handler
: это функция. Что делать с ней? По идее, функция, это тоже объект со своим списком свойств. Можно также понапихать в изначальный handler
свойства из переопределённого. Но, тот, кто это писал, явно подразумевал, что обработчик следует заменить новым.
С функцией просто, у неё typeof === "function"
. Так что определяем «расширенный» тип и всё, что не «object» заменяем. Всё, что «object» — сливаем.
Здорово, но вот опять незадача:
var options = { /* ... */ /* Объект, которому передавать данные о запросе Интерфейс: listener.onRequest(request) */ 'listener': new MyClass() }; var spec_options = { 'listener': new MyOtherClass("arg1", "arg2") }; |
listener
здесь объект. У него и typeof === Object
и Object.prototype.toString.call(listener) === "[object Object]"
.
Но явно, здесь подразумевается не структура с данными, а индивидуальный объект, который следует заменять целиком.
Поэтому всегда в подобных функциях требуется различать две этих сущности.
isDict() — простой словарь или нет
Напишем isDict
:
function isDict(value) { return value && (value.constructor === Object); } |
Элементарно, Ватсон: простой объект создаётся через new Object
. Объекты же, созданные через наши конструкторы будут ссылаться на них. В приведённом примере: listener.constructor === MyClass
.
Не учли три вещи:
1. Если словарь пришел из фрейма, то его constructor
будет ссылаться на Object
из того фрейма.
2. Конструкторы с переопределённым прототипом:
function MyClass() { } MyClass.prototype = { 'method1': function () {}, 'method2': function () {} }; |
В этом случае в переопределённом прототипе не будет свойства constructor
и по цепочке поиск пойдёт до глобального прототипа. А у него constructor === Object
. То есть, кажется, это объект, а будет определён, как словарь.
Конечно, тот, кто так делает, сам себе злобный буратино: constructor
надо восстанавливать. Но всё равно неприятный осадок остаётся.
Здесь можно выкрутиться с помощью getPrototypeOf, но в IE < 9 этой функции нет, ну и опять-таки проблема со фреймами. Кстати, jQuery.isPlainObject() этот момент вообще не отслеживает.
3. Опять-таки старые IE и host-объекты, у которых нет конструктора.
Итого
/** * Является ли значение простым словарём * * @param {String} value * @return {Boolean} */ function isDict(value) { "use strict"; if ((!value) || (typeof value !== "object")) { /* Сразу отсеить по typeof. (!value) требуется, так как typeof null === "object" */ return false; } if (value.constructor === Object) { if (Object.getPrototypeOf && (Object.getPrototypeOf(value) !== Object.prototype)) { /* Случай с переопределённым прототипом и не восстановленным constructor */ return false; } return true; } if (value instanceof Object) { /* value из нашего фрейма, значит constructor должен был быть Object */ return false; } if (Object.getPrototypeOf) { /* value не из нашего фрейма, можно выкрутиться с помощью getPrototypeOf, так как у прототипа объекта прототип - null */ value = Object.getPrototypeOf(value); if (!value) { return false; } return (Object.getPrototypeOf(value) === null); } /** * Все нормальные браузеры на этот момент уже определились. * Остались только IE<9 и значения пришедшие из фреймов. * Пытаемся определить по имени конструктора. * * В IE нет constructor.name, приходится заниматься разбором строкового представления. * toString() тоже может не быть, да ещё и могут выбрасываться исключения. * * Определить объект с уничтоженным прототипом в IE<9 не получается. */ try { return ((value.constructor + ":").indexOf("function Object()") !== -1); } catch (e) { return false; } return false; } |
Единственное, что не удалось определить без излишних извращений: объект с убитым прототипом из фрейма.
Для IE < 9, также не работает для того же объекта, но даже и в своём фрейме (Object.getPrototypeOf()
там нет).
Если кто знает, как это определить, расскажите.
Смысл басни
1. Различайте объекты и структуры.
2. IE — говно.
3. Всегда восстанавливайте constructor
при переопределении прототипа.
UPD
Ладно, объект с убитым прототипом из фрейма тоже можно определить в нормальных браузерах с помощью getPrototypeOf().
getPrototypeOf(getPrototypeOf(value)) === null
— прототип прототипа Object
=== null.
Остались только IE<9 и убитые прототипы.
Спасибо, полезная статья, пригодится!
adw0rd, 15.03.2013, 7:40
adw0rd, ты спам-бота на мне тестишь? :)
vasa_c, 15.03.2013, 11:21
А можно немного подробнее про «восстанавливайте constructor при переопределении прототипа»?
inst, 15.03.2013, 23:27
Inst, http://learn.javascript.ru/constructor
vasa_c, 16.03.2013, 12:05