Иногда Java-программистам в JavaScript`е не хватает коллекций. Давайте разберёмся - как можно в JavaScript без написания громоздких тяжеловесных решений обойти ситуацию, для которой нам могла бы понадобиться коллекция?
Итак, в Java есть три типа коллекций - List, Set и Map.
Эмулируем List
Строго говоря, как таковая эмуляция List`а в JavaScript нам в общем-то и не нужна - по сути ей и является массив (Array). Собственно, List`ы и в Java-то нам были нужны лишь исключительно по той причине, что массивы в Java имеют чёткий заранее определённый размер, который не может быть изменён, а по задаче мы не всегда можем сказать, массив какого размера нам понадобится. Т.к. в JavaScript такой проблемы нет - нет и необходимости создавать этот тип коллекций специально.
Эмулируем Set
Множество (Set) уникальных значений в JavaScript можно составить на основании того же массива (Array) с добавлением метода, который проверял бы уникальность добавляемого значения:
/** Определяет индекс элемента в массиве.
* @param {Object} value
* @return {number} индекс переданного объекта, если тот содержится в массиве.
* Если он в нём отсутствует, возарвщается -1. */
Array.prototype.indexOf = function(value)
{
var /** @type {number} */ length = this.length;
for (var /** @type {number} */ i = 0; i < length; i++)
if (this[i] == value)
return i;
return -1;
};
/** Определяет, содержит лимассив переданный объект.
* @param {Object} value
* @return {boolean} */
Array.prototype.contains = function(value)
{
return this.indexOf(value) === -1;
};
Теперь достаточно просто перед добавлением элемента проверять его методом contains
- для простых зазачь этого вполне хватит.
Эмуляция Map
С коллекциями типа map будет немного сложнее. В принципе, для наиболее частого случая, когда в качестве ключей нас устроят строки, решение тривиально - это Object
. Т.е. создаём объект - он и есть наша карта. Его свойства - это ключи, а их значения - это значения полей коллекции.
Но вот что делать, если ключами по логике задачи не должны быть строки, а ими должны быть другие объекты? Для того, что бы действовать в этой ситуации, нужен эффективный механизм преобразования объектов в строку. В качестве такого метода в общем случае может выступать какое-либо специальное поле-идентификатор, либо метод, возвращающий уникальное строковое значение для каждого объекта, который будет выполнять роль ключа.
Пример эмуляции Map
Например, в одной из недавних статей, где я рассматривал шаблон "Заплатка", я писал о методе создать на основе механизма навешивания слушателей в IE6-8 (attachEvent
) стандартный механизм навешивания слушателей (addEventListener
). Решение, напомню, было таким:
function setAddEventListener()
{
if ('attachEvent' in this && !('addEventListener' in this))
this.addEventListener = function(eventName, handler, isCapturing) {
if (isCapturing)
throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
attachEvent("on" + eventName, handler);
}
}
setAddEventListener.call(document);//Для document`а, например, вызываем так
Однако, если подходить серьёзно, то с этим решением в реальном проекте будет некоторое количество проблем. Первая из них - неполная совместимость IE`шного объекта Event
и стандартного. Путь для решения этой проблемы я уже демонстрировал в статье про pattern "Enrichment". Напомню, что там получилось:/** Конструктор, служащий для придания не совместимым с W3C DOM level 3 объектам
* Event стандартного поведения.
* @constructor
* @extends Event */
function EventW3C() {
/** Отменяет поведение браузера для данного события по-умолчанию. */
this.preventDefault = function() {
this.returnValue = false;
};
// Выставление других стандартных свойств
return this;
}
function setAddEventListener()
{
if ('attachEvent' in this && !('addEventListener' in this))
this.addEventListener = function(eventName, handler, isCapturing) {
if (isCapturing)
throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
attachEvent("on" + eventName, function() {
handler(EventW3C.call(event) //Здесь используется pattern "Enrichment"
});
}
}
Вторая проблема - ссылка this
. В обработчик IE6-8 по ней не передаётся элемент, с которым связано событие. Решение простое - запомнить ссылку на элемент в отдельной переменной в замыкании и передать её в качестве ссылки this
обработчику при помощи метода call
объекта Function
:function setAddEventListener()
{
if ('attachEvent' in this && !('addEventListener' in this))
this.addEventListener = function(eventName, handler, isCapturing) {
if (isCapturing)
throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
//Запоминаем элемент
var /** @type Element */ that = this;
this.attachEvent(
'on' + eventName, function() {
handler.call(that, EventW3C.call(event));//Правильно вызываем обработчик
}
);
}
}
При этом приёме, правда, как утверждает Д.Флэнаган, есть проблема с утечкой памяти в ранних версиях IE, но это сейчас out of scope. И, наконец, третья проблема - всплывает, когда мы попытаемся рассмотреть применение механизма удаления обработчика IE6-8 detachEvent
для эмуляции стандартного removeEventListener
. Дело в том, что т.к. мы используем не сам handler, а функцию, которая его вызывает, то и detach`ить нам нужно её, а не изначальный handler. Соответственно, для удаления нам где-то нужно её найти, так? Но в стандартный метод removeEventListener
будет передаваться лишь сам handler. Значит, нам нужно по handler`у найти настраивающую его функцию. Тут-то нам и понадобится коллекция типа Map.
Итак, есть соответствие двух функций и по ключу - одной функции мы должны найти значение - другую функцию, что бы её удалить из обработчиков данного события. Т.к. Map мы эмулируем на основе Object`а, у которого в качестве имён полей, т.е. ключей, традиционно выступают строки, то нам, очевидно, нужен метод, который преобразует объект в уникальную для него строку. В общем случае эта задача не является тривиальной, но в нашем конкретном - какая удача! - для функций, т.е. для объектов Function, этот метод уже есть! И называется он - ни за что не догадаетесь! - toString()
! :)))) Этот метод по сути возвращает представление данной функции в виде многострочного текста её определения. Очевидно, что текст определения функции не меняется никогда и он соответствует критерию уникальности для каждой функции, а для одной и той же функции всегда будет возвращать одинаковый результат.
Так что вот какое компактное решение получаем в данном случае:
/** Конструктор, служащий для придания не совместимым с W3C DOM level 3 объектам Event стандартного поведения.
* @constructor
* @extends Event */
function EventW3C() {
/** Отменяет поведение браузера для данного события по-умолчанию. */
if (!('preventDefault' in this))
this.preventDefault = function() {
this.returnValue = false;
};
if (!('stopPropagation' in this))
this.stopPropagation = function(){
this.cancelBubble = true;
};
// Выставление других стандартных свойств
return this;
}
(/** IE patch for addEventListener and removeEventListener methods. */
function setAddEventListener() {
if (!('addEventListener' in this))
if ('attachEvent' in this) {
var /** Объект-карта всех слушателей данного объекта
* @type Object<Object<Function>> */
fnMap = {};
this.addEventListener = function(eventName, handler) {
if (!(eventName in fnMap)) //Если для данного события ещё нет коллекции обработчиков...
/**@type Object<Function> */ fnMap[eventName] = {}; //...создаём её
var /** @type Element */ that = this; //Запоминаем элемент
this.attachEvent('on' + eventName,
fnMap[eventName][handler.toString()] = function() {
handler.call(that, EventW3C.call(event));//Правильно вызываем обработчик
}
);
};
this.removeEventListener = function(eventName, handler) {
if (eventName in fnMap && handler.toString() in fnMap[eventName]) {
this.detachEvent(
'on' + eventName,
fnMap[eventName][handler.toString()]
);
delete fnMap[eventName][handler.toString()];
}
};
}
})(); // Вызываем для window сразу
setAddEventListener.call(document);//Для document`а, например, вызываем так
Комментариев нет:
Отправить комментарий