пятница, 30 марта 2012 г.

Ещё одна модель наследования на примере реализации Set

В JavaScript принято великое множество разнообразных моделей наследования - практически каждая библиотека (MooTools, DOJO, JQuery, ExtJS и т.д.) предлагает свою, преследующую специфические цели и многие из них при этом игнорируют стандартную операцию instanceOf. Понимая, что вступаю на сверхконкурентное поле, всё-таки рискну вставить свои 5 копеек, потому что именно того, до чего дошёл в последнем проекте сам, я ни у кого не видел (хотя не исключаю, что просто плохо искал - так что те, кто считают, что я изобрёл велосипед, прошу меня простить великодушно за потраченное на чтение статьи время).

Одну из этих форм, не игнорирующую instanceOf, можно назвать классической для наследования в JavaScript, принятой в этом языке с момента его создания. Уже упоминавшийся мной Стоян Стефанов в своей книге "JavaScript Шаблоны" ("JavaScript Patterns") называет её "шаблоном по умолчанию" в разделе "Шаблонов повторного использования программного кода". Я долгое время использовал её в своих проектах. Не могу сказать, что она мне совсем уж во всём нравилась (об этом - ниже), по-этому я обдумывал, как её можно было бы улучшить, минимально изменив, но сделать это так, что бы она не потеряла ни своей краткости, ни своей понятности, ни красоты и что-то у меня в итоге, как мне кажется, получилось. :)

Классическую схему прототипного наследования можно продемонстрировать на следующем примере:

/** @constructor */
function A(x){
    this.x = x;
}
A.prototype.getX = function(){ return this.x };

/** @constructor
 * @extends A
 * @param y
 * @param [x=5] */
function B(y, x){
    this.y = y;
    if (typeof x !== 'undefined' && x !== this.x)
        this.x = x;
}
B.prototype = new A(5);
B.prototype.getY = function(){ return this.y; };

//Проверим:
var b1 = new B(1),
    b2 = new B(2, 1);
alert(b1.getX()); //5
alert(b1.getY()); //1
alert(b2.getX()); //1
alert(b2.getY()); //2
Здесь можно обратить внимание на то, что мы вынуждены вызывать конструктор предка лишь один раз и все его поля, установленные при этом вызове, автоматически становятся для нас как бы значениями по-умолчанию - мы проваливаемся по ссылке __proto__, считывая их, если происходит обращение к полученному объекту, а у него этого свойства нет. Т.е. в случае, если нам для них нужно иное значение, мы должны их заново устанавливать в конструкторе-потомке - помощи нам от конструктора-предка в этом нет никакой. Из этого вытекает, что в случае большого количества полей и при глубоком наследовании мы будем вынуждены каждый раз описывать все поля. И даже более того, что бы полноценно воспользоваться этим бенефитом в виде экономии на полях по-умолчанию, мы вынуждены прибегать к громоздкой конструкции проверки на наличие у нас соответствующего аргумента и проверки на его неравенство значению по-умолчанию:
typeof x !== 'undefined' && x !== this.x
По сути это означает, что наследуются только методы прототипа конструктора и значения полей по-умолчанию.

На проблему того, что нам приходится устанавливать все поля при том, что это можно было бы сделать при помощи конструктора-предка так же обратил внимание Стоян Стефанов, и для её разрешения предложил шаблон "Заимствование и установка прототипа", который приводит в той же 6-й главе той же книги. В соответствии с ним мой пример можно было бы модифицировать так:

/** @constructor
 * @extends A
 * @param x
 * @param y */
function B(x, y){
    A.call(this, x);
    this.y = y;
}
B.prototype = new A(5);
Таким образом, мы доверяем конструктору-предку сконфигурировать за нас объект, притворяясь, как будто он вызван в качестве конструктора, и даже если он в процессе этого задействует какие-то методы своего прототипа - они будут на месте, потому что прототип мы унаследовали. Хорошо, Стоян помог нам решить одну из проблем - мы добились повторного использования кода родительского конструктора и теперь не обязаны прописывать создание и инициализацию полей во всех потомках. Но осталась вторая проблема - в результате мы получаем объект, имеющий 3 набора свойств:
  • набор свойств базового объекта, применённых к нашему основному объекту (в нашем случае - поле this.x),
  • набор свойств, присвоенных нашему объекту его основным конструктором (в нашем случае - поле this.y)
  • набор свойств, аналогичный первому, но присвоенный прототипу (в нашем примере - это поле this.__proto__.x или, что более правильно с точки зрения ECMAScript 5, Object.getPrototypeOf(this).x)
В нужности первых двух наборов убедиться не сложно, но вот зачем нам третий?

Единственное, что обычно удаётся придумать - третий набор свойств может интересовать нас разве что как некоторый набор значений свойств по-умолчанию. Т.е. мы могли бы записать в эти поля наиболее типичные значения полей и не присваивать их в конструкторе-потомке, если они совпадают. Т.е. в базовом варианте "шаблона по умолчанию" по Стефану это можно было бы реализивать так, как я и показал в первом примере данной статьи, при помощи следующей конструкции:

typeof x !== 'undefined' && x !== this.x
А в варианте "заимствования и установки прототипа" - следующим, возможно ещё более неудобным образом:
/** @constructor
 * @extends A
 * @param y
 * @param [x=5] */
function B(y, x){
    A.call(this, typeof x === 'undefined' ? 5: x);
    if (this.x === Object.getPrototypeOf(this).x)
        delete this.x;
    this.y = y;
}
B.prototype = new A(5);
И, спрашивается, ради чего такие сложности?

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

А теперь задумайтесь - много ли в Вашей практике программирования на JavaScript было случаев, когда Вам были нужны значения по-умолчанию? Я сам для себя могу таких случаев припомнить немного. Но и в тех достаточно редких случаях, когда нам действительно удобно воспользоваться этим бенефитом в виде поля по-умолчанию, наш объект этого поля не будет содержать, а за его операцией this.x, фактически, будет стоять следующее действие интерпретатора JavaScript: он будет сначала искать это поле в объекте this и только потом - в объекте this.__proto__, и лишь там его, наконец, найдёт. Давайте теперь представим многоступенчатое наследование внушительного размера конструкторов с кучей полей в каждом, реализованное по такой вот модели - и совершенно точно получим нечто трудновообразимое...

Для того, что бы как-то оптимизировать такого рода структуру я несколько раз встречал у коллег костыльный код, подобный следующему:

for (var i in B.prototype)
    if (!this.hasOwnProperty(i) && typeof B.prototype[i] !== 'function')
        this[i] = B.prototype[i];
Не трудно увидеть, что смысл этой конструкции в том, что бы притянуть все установленные по умолчанию поля из всех прототипов объекта в сам этот объект, что бы их поиск по цепочке прототипов происходил быстрее - по мнению многих разработчиков, такой тюнинг вполне оправдан (иногда даже функции из прототипа подтягиваются так же). В итоге мы видим, как игнорируется и то призрачное преимущество полей по умолчанию, которое мы имели - даже тогда, когда они востребованы.

Кроме того, нельзя забывать о проблеме со ссылкой constructor, которая в случае этой модели на нижнем уровне просто теряется - вызов b1.constructor приведёт нас к функции A, а не к B.

По всему выходит, что все эти значения по-умолчанию в общем случае нужны нам обычно чуть меньше, чем собаке - пятая нога. В частном же случае, когда они действительно оказываются нужны, их, мне кажется, проще захарткодить. Но, используя такую модель, мы вынуждены везде втыкать эти значения, фактически отказываясь от их и так довольно призрачной использования при тюнинге. Так, может быть, "не стоит овчинка выделки"? :) Может, лучше будет, если мы будем сразу писать код, который будет оттюнен сразу?

Я предлагаю внести в эту схему изменение, направленные на её большую экономность и практичность: сэкономить на значении полей по-умолчанию.

Давайте подумаем, что нам для этого нужно? На мой взгляд, здесь важно понять, что в классической схеме ("шаблон по умолчанию" Стефана) мы вызывали конструктор предка с целью установки ссылки __proto__ и лишь побочным эффектом этого стала для нас установка в нагрузку ещё и значений полей в прототипе. Теперь давайте пойдём немного дальше и зададимся следующим, вытекающим отсюда вопросом - а существует ли иной, кроме создания экземпляра объекта, способ создать объект с ссылкой __proto__, ведущей на нужный нам прототип? Оказывается - да, существует! В ECMAScript5 у нас есть прекрасный метод Object.create, второй параметр которого необязательно указывать, а первый как раз-таки и содержит прототип вновь создаваемого объекта. Ссылку же 'çonstructor' можно прописать и вручную тут же, не отходя от кассы, воспользовавшись тем, что любое равенство в JavbaScript возвращает то, что в нём присваивается. Т.е. получаем такой компактный код:

function B(){/*...*/}
(B.prototype = Object.create(A.prototype)).constructor = B;
Всё - разобрались с проблемой: и объект прототипа у нас чистенький и ссылается он ссылками __proto__ и constructor куда надо - на прототип предка и на собственный конструктор соответственно, а если нам понадобится получить доступ к конструктору предка - это можно будет осуществить вызовом
Object.getPrototypeOf(Object.getPrototypeOf(b1)).constructor
, что, хоть и более громоздко, чем привычное и лаконичное b1.__proto__.__proto__.constructor, тем не менее является более правильным, чем прямое использование нестандартной ссылки __proto__, относительно которой даже на сайте Mozilla Developer Network уже чёрным по синему написано: "Deprecated", то бишь со временем это, по-видимому, будет убрано... Да, я понимаю, у меня у самого в связи с этим до сих пор некоторая ломка, но "надо, Федя, надо..."(с) :)

Единственная проблема, которая здесь возникает - это проблема с поддержкой старых браузеров, из которых актуальными остаются на сегодняшний момент IE6-8, не поддерживающие ECMAScript5. Для них придётся писать заплатки. Вот они для использующихся методов Object.create и Object.getPrototypeOf:

Object.create = function(proto, descriptors){
    if (typeof proto !== 'object' && typeof proto !== 'function')
        throw TypeError(proto + ' is not an object or null');
    var /** @private @type Object */ result,
        /** @private @type Object */ objPrototype = Object.prototype;
    Object.prototype = proto;
    result = new Object();
    Object.prototype = objPrototype;
    if (typeof descriptors !== 'undefined')
        Object.defineProperties(result, descriptors);
    return result;
};
Object.defineProperties = function(object, descriptors){
    for (var /** @private @type string */ i in descriptors)
        Object.defineProperty(object, i, descriptors[i]);
    return object;
};
Object.defineProperty = function(o, name, desc){
    o[name] = ('value' in desc) ? desc.value: null;
    return o;
};
Object.getPrototypeOf = function(o){ return o.__proto__; };
Увы, одним patch`ем отвертеться не удастся - для того, что бы последний метод работал, нужно в каждый конструктор вставлять ссылку "__proto__" выражением, которое я уже недавно демонстрировал в статье "__proto__ во всех браузерах":
if (!this.hasOwnProperty('__proto__'))
    this.__proto__ = B.prototype;
Почему Стоян Стефанов не обратил своё внимание на эту проблему - я не знаю. Может быть, счёл её не достаточно серьёзной, может - просто не придумал достаточно элегантного на его взгляд решения...

Вот что примерно получается, если сложить всё вместе (кроме patch`а для IE6-8):

/** @constructor */
function A(x){
    this.x = x;
}
A.prototype.getX = function(){ return this.x };

/** @constructor
 * @extends A
 * @param x 
 * @param y */
function B(x, y){
    A.call(this, x)
    this.y = y;
}
((B.prototype = Object.create(A.prototype)
    ).constructor = B
        ).prototype.getY = function(){ return this.y; };

//Проверим:
var b = new B(1, 2);
alert(b.getX()); //1
alert(b.getY()); //2
Этот метод хотелось бы продемонстрировать на примере недавно обсуждавшейся в статье "Коллекции в JavaScript - допиливаем setAddEventListener" JavaScript-аналоге коллекций типа множество (Set). Этот пример демонстрирует наследование от конструктора Array с переопределением метода push из несколько сомнительного расчёта на то, что множество будет наполняться исключительно посредством этого метода, хотя на самом деле, конечно же, может наполняться и прямой записью элементов по какому-либо индексу, но с этим уж ничего не поделаешь. А вот с функцией push кое-что поделать мы всё-таки можем:
/** Множество. При добавлении элемента массив проверяется на его наличие и добавление происходит лишь в
 * случае отсутствия в нём этого элемента.
 * @constructor
 * @extends Array */
function ArraySet(baseType){
    Array.apply(this, arguments);
}
/** Добавить элементы к множеству.
 * @param {...Object} arguuments */
((ArraySet.prototype = Object.create(Array.prototype)
    ).constructor = ArraySet
       ).prototype.push = function(){
    var /** @type Function */ push = Object.getPrototypeOf(Object.getPrototypeOf(this)).push,
        /** @type number */ length = arguments.length;
    for (var i = -1; ++i < length;)
        if (this.indexOf(arguments[i]) === -1)
            push.call(this, arguments[i]);
    return this.length;
};
А вот и необходимая заплатка для Array.indexOf в IE6-8:
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;
};

среда, 29 февраля 2012 г.

Встраивание поддержки стандартов в DOM-дерево

В предыдущих статьях я приводил код, позволяющий добиться частичной поддержки стандартных методов в IE6-8, используя минимум ресурсов. Но встраивание его в DOM-модель производилось пользователем вручную. В данной статье я намерен показать, как этого можно было бы избежать, сделав стандартизацию IE прозрачной.

В одном из предыдущих постов я описал функцию, которая добавляет метод addEventListener к любому DOM-объекту при помощи нестандартного IE`шного метода attachEvent (в последующих постах я привёл для неё ряд улучшений - 1, 2). И всё бы хорошо, но сама по себе эта функция, очевидно, проблемы не решает, ведь теперь её нужно вызывать для всех объектов DOM-дерева, с которыми работает сценарий.

Можно было бы, конечно, вставлять эти вызовы в основной код, корректно отрабатывающий во всех браузерах, кроме IE6-8, но это его замусорит. Я уже не раз писал, что в современных проектах код часто итак достаточно сложен, так что отвлекаться при его написании ещё и на нестандартные браузеры часто слишком накладно как с позиции трудоёмкости написания, так и с позиции трудоёмкости его последующего изменения при отладке и расширении.

Так что я решил пойти несколько другим путём.

Я написал основной код на стандартном JavaScript`е строго в рамках стандартов, отладил всё в стандартных браузерах - IE9, а так же в последних версиях Chrome, Safari, Opera и FireFox, и лишь потом взялся за IE6-8, создав для них отдельный файл сценария в проекте. Добавление этого сценария можно произвести очень удобно - IE умеет понимать комментарии специального вида, которые диктуют ему различное поведение в отличие от его различных версий. Я написал так:

<!--[if lt IE 9]><script type="text/javascript" src="/_scripts/_lib/patch.js"></script><![endif]-->
Очень удобно, что все остальные браузеры считают этот код простым комментарием и не вставляют его, а IE 8`й и более ранних версий этот код вставляет и выполняет. Этот "комментарий" я вставил до объявления всех остальных сценариев, добившись, таким образом, что бы у меня была возможность в этом сценарии именно нужным образом подготовить браузер к выполнению стандартного кода, следующего дальше.

Итак, теперь осталось решить, как ранее уже описанную функцию setAddEventListener вызывать для всех DOM-объектов, что бы в нужный момент у них можно было вызвать стандартный метод addEventListener.

Легко понять, что попытка решить эту задачу в общем слишком глобальна, ведь модель DOM по сути представляет собой в большей степени коллекцию интерфейсов, а не конструкторов, как многие ошибочно полагают. В действительности у нас нет, например, конструктора Node и конструктора HTMLElement в браузерной среде - у нас есть только фабрика document, имеющая методы, которые возвращают объекты, соответствующие этим интерфейсам. Так что бесполезно было бы пытаться добавлять функционал к прототипам этих интерфейсов - они ни на что не влияют, а зачастую и вообще не существуют в браузерном окружении. Если мы попытаемся добавить стандартный, но отсутствующий в IE6-8 метод к Node.prototype или HTMLElement.prototype, надеясь, что данный метод появится у всех HTMLElement`ов, мы неминуемо будем разочарованы их отсутствием, а то и вообще получим ошибку при обращении к несуществующему объекту. Так что добавлением стандартного функционала надо заниматься на уровне фабрики - т.е. объекта document.

Однако сказать это проще, чем сделать. Ведь способов получить DOM-объекты в JavaScript масса и учесть все возможные их комбинации не представляется возможным! Вот, допустим, мы переопределим стандартный и наиболее часто встречающийся метод document.getElementById примерно так:

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result);
        return result;
    }
}();
Вроде бы, всё замечательно, но это, ведь, не единственный способ получения элемента. Вот вернули мы этим нашим методом в стандартный сценарий element, а в этом стандартном сценарии производится вызов свойства parentNode или nextSibling, и на него уже производится попытка повесить обработчик события стандартным образом - с этим как? Ведь, это же свойства, а не функции, и в IE6-8 у нас нет методов определения их в качестве properties - ни ECMAScript5`овских Object.defineProperties c Object.defineProperty, ни даже FireFox`овских нестандартных (и кстати уже deprecated) __defineGetter__ с __defineSetter__`ом. Так что мы не можем обработать их вызовы. Можем, конечно, написать так:
setAddEventListener(result)(result.parentNode)(result.nextSibling);
, но если в стандартном коде свойство parentNode будет вызвано несколько раз - и этот объект прописывать, а остальные?.. В общем, реальных комбинаций может быть очень много - это, конечно, конечное множество, но полноценный качественный алгоритм, который переберёт все возможные варианты, неминуемо серьёзно затормозит работу браузера, так что это решение не приемлемо.

Так что, думаю, будет не рентабельно вершить мир во всём мире, создавая универсальное решение - гораздо продуктивнее отталкиваться от конкретики стандартного сценария, которому нужна поддержка ограниченного стандартного функционала и обеспечить её здесь и сейчас. В моём случае мне вполне хватало одноразового использования ссылки parentNode для возвращаемого методом document.getElementById элемента, так что я остановился на вполне достаточном для меня варианте:

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result)(result.parentNode);
        return result;
    }
}();
Кроме того, мне был нужен слушатель для элемента, возвращаемого конструктором Image, который по аналогии был реализован так же сходным образом, лишь с незначительными изменениями:
Image = function (){
    var src = Image;
    return function(){
        var /** @type HTMLImageElement */ result = new src;
        setAddEventListener(result);
        return result;
    }
}();

Улучшение метода setAddEventListener

Активное применение описанноего в одном из предыдущих постов метода setAddEventListener для придания браузерам IE6-8 стандартного W3C DOM`овского метода addEventListener позволило выявить у первого некоторые недостатки. О крупных недостатках я напишу отдельно, но о паре мелких улучшений давайте поговорим сейчас.

Во-первых, я решил избавиться от необходимости вызывать метод setAddEventListener при помощи метода call. Во-вторых, я так же решил не вызывать его каждый раз для каждого объекта с указанием имени этого метода, сделав эту же функцию возвращаемым значением - это позволило вызывать её подряд для цепочки объектов, что я нахожу очень удобным.

Вот как теперь можно объявив эту функцию, сразу же в том же выражении элегантно вызвать её и для объекта window и для объекта document:

var setAddEventListener = function f(that) {
    if (!('addEventListener' in that) && 'attachEvent' in that) {
        //реализация
        that.addEventListener = function(eventName, handler) {
            //реализация
            return handler;
        };
        that.removeEventListener = function(eventName, handler) {
            //реализация
            return handler;
        };
    }
    return f;
}(window)(document);

Усовершенствование шаблона enrichment

Стоян Стефанов (Stoyan Stefanov) описывает в своей книге "JavaScript Шаблоны" ("JavaScript Patterns") в гл.6 шаблон "Заимствование конструктора" (к сожалению, в русском издании английские названия шаблонов были опущены и по-этому каноническое название этого шаблона мне пока не удалось найти, в том чиле и на сайте http://www.jspatterns.com/ этот шаблон не упомянут).

Этот шаблон очень напоминает то, что я в одном из предыдущих своих постов назвал "Enrichment", единственное отличие заключается в том, что он предлагает его использование в контексте наследования (в том числе предлагает на его основе реализацию множественного наследования), а не в контексте придания дополнительной функциональности уже сформированным объектам. Читатель, конечно, может считать это одним и тем же шаблоном, однако специфика применения в данном случае, на мой взгляд, имеет важное значение.

Немного попрактиковавшись, я пришёл к выводу о том, что шаблон enrichment луше будет всё-таки описывать именно простой функцией, не пытаясь делать вид, что это конструктор, начиная её не с заглавной буквы, а с приставки "to", что бы не путать её с настящим конструктором. Конструктора же из неё не выйдет просто потому, что, как справедливо указывает Стоян Стефанов в разделе недостатков этого шаблона, она не может прицепить к передаваемому ей объекту ссылку "__proto__" на прототип, а по-этому не может встроить его в модель наследования.

Кроме того, что бы не плодить большого количества функций, которые имеют характер постоянных при многократном использовании данного шаблона, будет гораздо лучше, если мы будем создавать их один раз и потом просто присваивать. Т.е. вместо кода, который я приводил ранее:

function EventW3C() {
    if (!('preventDefault' in this))
        this.preventDefault = function() {
            this.returnValue = false;
        };
    if (!('stopPropagation' in this))
        this.stopPropagation = function(){
            this.cancelBubble = true;
        };
    //Прочие реализации стандартных свойств и методов
    return this;
}
, теперь я пишу так, что, с моей точки зрения, более корректно:
function toW3CEvent(that) {
    for (var p in toW3CEvent)
        if (!(p in that) && toW3CEvent.hasOwnProperty(p) && p !== 'prototype')
            that[p] = toW3CEvent[p];
    return that;
}
toW3CEvent.preventDefault = function() { this.returnValue = false; };
toW3CEvent.stopPropagation = function() { this.cancelBubble = true; };
//Прочие реализации стандартных свойств и методов
Так, теперь для придания объекту event в IE6-8 функциональности стандартного W3C DOM объекта Event, можно вызвать эту функцию следующим образом:
handler.call(that, toW3CEvent(event));

суббота, 21 января 2012 г.

__proto__ для стандартных конструкторов

Сегодня слушателями курса по JavaScript сподвигли меня на написание очередной заплатки для IE. В статье "__proto__ во всех браузерах" я уже приводил код для вставки ссылки __proto__ в любой объект. Теперь настала пора вспомнить о стандартных конструкторах JavaScript - у них соответствующие ссылки так же должны вести в правильные места:
if (!Object.hasOwnProperty('__proto__')) {
 Object.prototype.__proto__ = null;
 Object.__proto__ =
 Function.__proto__ =
 Date.__proto__ =
 Number.__proto__ =
 String.__proto__
 Boolean.__proto__ = Function.prototype;
}
Только не спрашивайте, зачем это может быть нужно в реальном проекте - не знаю. :)

вторник, 22 ноября 2011 г.

Задержка выхода второй версии библиотеки Drag&Drop

Извиняюсь за задержку с выходом второй версии библиотеки - знаю, что обещал, знаю, что моя вина. Но поймите и Вы меня - дело в том, что когда я уже вносил в неё последние штришки, готовясь сегодня-завтра опубликовать проект, случайно услышал о HTML5 Drag&Drop`е. В итоге я встал перед дилеммой - добивать эту версию со своим собственным интерфейсом, или переделывать её для того, что бы она поддерживала стандартный, но пока ещё мало где (по утверждению W3Schools, пока только в Chrom`е и Safari) реализованный интерфейс HTML5 Drag&Drop. После некоторых колебаний я решил, что теперь, после появления этой спеки, библиотека с собственным интерфейсом для Drag&Drop мало кого заинтересует, так что я решил выбрать последнее.

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

Так же для пользователей первой версии библиотеки, возможно, будет нужна обратная совместимость - поддержка интерфейса первой версии, благо реализовать её не сложно, а так же с помощью Google Closure Compiler`а выкинуть её в случае необходимости не представляется сложным, просто подложив другие экстерны.

К сожалению, работа отнимает довольно много времени. Похоже, успею выпустить библиотечку не раньше февраля. За то она будет HTML5-совместима! :)

четверг, 10 ноября 2011 г.

Коллекции в JavaScript - допиливаем setAddEventListener

Иногда 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`а, например, вызываем так