пятница, 4 октября 2013 г.

Создание собственных событий в JavaScript

Иногда требуется организовать бизнес-логику, основываясь на событийном механизме (шаблон Observer). Обычно это делается разово, но последнее время такие ситуации стали появляться всё чаще, и я решил создать универсальное решение, которое позволит расширять или создавать список событий для любого JavaScript-объекта.

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

//Сначала добавляем нужные методы к объекту
var x = {};
setAddEvent(x);

//Затем добавляем событие. Называем его 'event1'. На выходе получаем функцию, которую нужно будет вызывать, когда событие произойдёт
var f = x.addEvent('event1');

// Теперь добавляем обработчики события - способ стандартный (такой же, как принятый в DOM)
var y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);

// Вызываем событие
f(); // "Событие 1 произошло!" и "Событие 1 произошло! (Второй обработчик)"

// Удаляем один из обработчиков
removeEventListener('event1', y, false)

// Снова вызываем событие
f(); // "Событие 1 произошло! (Второй обработчик)"

// Удаляем последний обработчик
removeEventListener('event1', z, false);

// Вызываем событие
f(); // Ничего не выводит

// Удаляем событие
removeEvent('event1');

// Пробуем вызвать событие
try { f(); } catch(/** @type Error */ e) { console.error(e.message); } // Выведет в консоль сообщение об ошибке: "Событие 'event1' было удалено!"
Примерно так. Добавлю ещё, что функция removeEventListener возвращает значение типа boolean, означающее наличие, либо отсутствие иных обработчиков данного события в настоящий момент.

Без лишних предисловий, вот основной код функции:
/** Функция делает переданный ей объект Observer`ом, т.е. генератором событий, на которые можно подписываться и
 * отписываться. После её вызова у переданного ей объекта появляются 3 метода: <code>addEvent(name)</code> для
 * добавления событий и пара функций <code>{@link #addEventListener}</code> и
 * <code>{@link #removeEventListener}</code> для добавления и удаления обработчиков событий.
 * @param {Object} that любой объект JavaScript, который нужно сделать Observer`ом.
 * @returns {Object} переданный в качестве аргумента, обогащённый методами <code>{@link #addEvent}</code>,
 * <code>{@link #addEventListener}</code> и <code>{@link #removeEventListener}</code> объект
 * <code><em>that</em></code>. */
function setAddEvent(that) {

    var /** @type Object<Object<Function[]>>*/ events = {};

    /** Цепляет к объекту событие, на которое после этого можно будет вешать обработчики при помощи метода
     * <code>{@link #addEventListener}</code>.
     * @param {string[]} name имя события (которое потом нужно будет передавать первым параметром - type - в метод
     * <code>addEventListener</code>).
     * @returns function(string, boolean) функция, которую нужно вызывать в ответ на событие, передавая ей при
     * вызове объект события. Функция будет вызывать все слушатели по порядку, передавая им этот объект. */
    that.addEvent = function(name) {

        if (name in events)
            throw new Error('Cобытие ' + name + ' уже есть!');

        /** @type Function[] */ (
            /** @type Object<Function[]> */ events[name] = {}
        )[true] = [];
        /** @type Function[] */ events[name][false] = [];

        return function(event, isPropagation) {
            if (!(name in events)) throw new Error('Событие \'' + name + '\' было удалено!');
            if (typeof isPropagation === 'undefined') isPropagation = false;
            for (var /** @type Function[] */ listeners = events[name][isPropagation],
                     /** @type number     */ i = 0,
                     /** @type Function   */ listener;
                 listener = listeners[i++];)
                listener(event);
        };
    };

    /** Удаляет событие и всех его слушателей из объекта.
     * @param {string} name Имя события, назначенное ему ранее при вызове метода <code>{@link #addEvent}</code>.*/
    that.removeEvent = function(name) {
        if (!(name in events))
            throw new Error(
                'События ' + name + ' уже итак нет, либо оно не было создано при помощи метода \'addEvent\'!');

        delete events[name];
    };

    /** Добавляет к объекту слушателя события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>addEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {function(Event)} Параметр, переданный функции вторым - handler. */
    that.addEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            if (eventName in events)
                events[eventName][isPropagation].push(handler);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return handler;
        };
    }(that.addEventListener);

    /** Удаляет из объекта обработчик события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>removeEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {boolean} Остались ли ещё обработчики для данного события? Возврат значения <code>false</code>
     * указывает на бессмысленность дальнейшего вызова функции, возвращённой методом <code>{@link #addEvent}</code>
     * при создании данного события - пока при помощи метода <code>{@link #addEventListener}</code> не будет
     * прикреплён хотя бы один новый обработчик. */
    that.removeEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            var /** @type Function[] */ listeners,
                /** @type number     */ index;
            if (eventName in events &&
                (index = (listeners = events[eventName][isPropagation]).indexOf(handler)) !== -1)
                listeners.splice(index, 1);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return listeners.length > 0 || events[eventName][!isPropagation].length > 0;
        };
    }(that.removeEventListener);

    return that;
}

//Test
var f = setAddEvent(this).addEvent('event1'),
    y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);
f();
addEventListener('load', function(){ console.log('loaded'); }, false);
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', y, false)
        ? 'Да': 'Нет'
    ));
f();
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', z, false)
        ? 'Да': 'Нет'
    ));
f();
removeEvent('event1');
try { f(); } catch(/** @type Error */ e) { console.error(e.message); }

среда, 10 июля 2013 г.

XSLT 2.0 -> 1.0

Известна проблема, связанная с реализацией XSLT 2.0 (и, соответственно, XPath 2.0) - как признаёт Дуг Тидуэлл (автор бестселлера по этой теме), фактически полноценных реализации всего две:
  • Altova
  • Saxon
- и обе платные. Все остальные - частичные. Что-что? Кто сказал, что Saxon - это типа OpenSource и, значит, по определению бесплатный? Платной является именно полноценная версия, в которой можно использовать Schema-типы данных. Бесплатные же реализации для XSLT есть только для версии 1.0 . При чём последних - как собак нерезаных! Они даже включены в наши с Вами браузеры. Действительно, можете это проверить - зайдите на страницу XML, для которой при помощи декларации
<?xml-stylesheet type="text/xsl" href="trans.xslt"?>
прописана таблица преобразования, совместимая с XSLT 1.0 - и наслаждайтесь результатом! (Тут, правда, следует учесть, что локальный файл не все браузеры корректно откроют - лучше разместить его на каком-то из ваших сайтов, либо поднять сервер локально и загрузить с него - применяет XSLT-стили к локальным файлам разве что FireFox последних версий...)

Так вот, в ситуации, когда XSLT 1.0 процессоров - хоть попой ешь, а XSLT 2.0 - в высшей степени скудно, Вашему покорному слуге недавно подумалось - а почему бы не использовать здесь примерно тот же паттерн, что я уже довольно давно и активно применяю по отношению к стеку технологий JavaScript/CSS/HTML, решая проблему кроссбраузерности без потери легковесности решений?

Тем более, что технология к этому располагает. XSLT-документы, фактически, представляют собой XML-документы, а потому к ним, в свою очередь, так же применим любой инструментарий работы с XML. А значит, можно написать XSLT-документ, который будет применяться к другому XSLT-документу, что бы преобразовать его. Зачем же может понадобиться преобразование одного XSLT-документа в другой? Для того, что бы в первом без особых заморочек использовать элементы XSLT 2.0 и XPath 2.0 запросы, а в результате преобразования получать пусть и более громоздкий, но корректный и работающий XSLT 1.0 документ.

Давайте прикинем, что нам для этого нужно?

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

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="xslt2to1.xslt"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
В принципе, это, конечно, необязательно, но лучше это сделать, что бы не забыть, при помощи какого именно файла это преобразование будет осуществляться. Опять же - Altova XMLSpy последней, 13-й версии это дело игнорирует, не разрешая по кнопке "F10" трансформировать документ, который считает именно XSLT-документом, в отличие от того, как она позволяет поступать по отношению к обычным XML-документам, так что приходится осуществлять это преобразование, перейдя на вкладку преобразующего документа и там указывать исходный документ.

Во-вторых, нужно, собственно, создать преобразующий документ. Понятное дело, что этот документ крайне желательно так же иметь в виде XSLT 1.0, иначе полностью уйти от XSLT 2.0 движков не получится. Для начала давайте сделаем таблицу, которая будет полностью копировать исходный документ, а потом будем постепенно вносить в неё template`ы, что бы получить на выходе то, что надо:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
Если применить эту таблицу к любому XML-документу - получим на выходе его полную копию! Хорошо, но нам же не нужна полная копия, так? Нам нужно преобразование. Для начала, нам нужно убрать, собственно, инструкцию процессору
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
, говорящую о том, что этот документ перед тем, как использоваться, должен быть преобразован согласно указанной XSLT-таблице стилей - выходной же документ, являющийся результатом преобразования, в свою очередь уже не нуждается в преобразовании и по-этому эту инструкцию нужно убрать.

Что бы сделать это, достаточно воспользоваться приёмом создания template`а, который имеет более специфическое XPath-выражение в атрибуте matсh, чем "node()" (поскольку селектор node() является максимально общим, практически любой XPath-запрос к тому же элементу будет более специфичным, так что сделать это не сложно ;) ). Тогда, согласно правилам XSLT, процессор для данного узла, либо атрибута выберет именно новый template, а не старый, именно из-за большей специфичности его XPath-запроса. Каким же должен быть этот template? Пустым, поскольку именно пустой template позволит убрать из конечного документа найденный узел. Т.е. убирается ненужная конструкция добавлением в таблицу преобразования следующего элемента:

<xsl:template match="processing-instruction()[local-name()='xml-stylesheet']"/>
Хорошо. Следующий шаг - изменение версии XSLT в документе, ведь мы преобразуем XSLT 1.0 в 2.0 . Это может быть сделано примерно так:
<xsl:template match="/xsl:*[local-name()='stylesheet' or local-name()='transform']/@version[.='2.0']">
    <xsl:attribute name="version">1.0</xsl:attribute>
</xsl:template>
Ну а дальше - непаханное поле, вполне достойное отдельного проекта на GitHub`е, который я собираюсь создать и потихоньку разрабатывать (когда найду на это время, конечно). В качестве примера можно рассмотреть реализацию довольно удобного атрибута "separator" в элементе value-of, который появляется в XSLT 2.0 и отсутствует в XSLT 1.0 . Этот атрибут задаёт символы, которые необходимо вставить между результатами выборки, проставленными в XPath-выражении атрибута "select". Например, выражение
<xsl:value-of select="'1'|'2'|'3'" separator=", "/>
выведет строчку:

"1, 2, 3"

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

<xsl:for-each select="'1'|'2'|'3'">
    <xsl:value-of select="."/>
    <xsl:if test="not(position()=last())">
        <xsl:text>, </xsl:text>
    </xsl:if>
</xsl:for-each>
Соответственно, преобразование может выглядеть примерно так:
<xsl:template match="xsl:value-of[@separator]">
    <xsl:element name="xsl:for-each">
        <xsl:attribute name="select">
            <xsl:value-of select="@select"/>
        </xsl:attribute>
        <xsl:element name="xsl:value-of">
            <xsl:attribute name="select">.</xsl:attribute>
        </xsl:element>
        <xsl:element name="xsl:if">
            <xsl:attribute name="test">not(position()=last())</xsl:attribute>
            <xsl:element name="xsl:text">
                <xsl:value-of select="@separator"/>
            </xsl:element>
        </xsl:element>
    </xsl:element>
</xsl:template>
Вот примерно как-то так...

P.S. Остался наиболее хитрый вопрос - о том, что делать с спецификой именно XPath 2.0 - запросов. Дело в том, что напрашивающиеся тут RegExp`ы в XSLT 1.0 (читай - XPath 1.0) не поддерживаются, что создаёт, мягко говоря, некоторые трудности. Что тут можно сделать? Либо отказаться от идеи использования в качестве процессора преобразования XSLT 1.0 процессор и по крайней мере для преобразования таблиц юзать процессор XSLT 2.0, либо использовать механизм расширений. Например, для Xalan`а пример работы можно подсмотреть тут.

пятница, 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;
}
Только не спрашивайте, зачем это может быть нужно в реальном проекте - не знаю. :)