Заметки в категории «JavaScript» (страница 17)

Замыкания в JavaScript

Механизм, который позволяет скрывать переменные и методы реализуется благодаря замыканиям (сlosures). Внутренняя функция всегда имеет доступ к переменным и параметрам своей внешней функции, даже после того как внешняя функция закончила выполняться. Это чрезвычайно мощное свойство языка.

var Module = function () {
    var private = 100;
    this.getPrivate = function () {
        return private;
    };
};

Единственным способом получить доступ к переменной private — это вызвать привилегированный метод getPrivate . В этом примере замыканием служит первая функция, которая ограничивает область видимости.

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

Другое применение замыкание находит в обработчиках событий.

Допустим, мы хотим отобразить список из названий предметов, а по клику на названии получить детальную информацию о предмете.


function drawList(items) {
    var i, li;
    for (i = 0; i < items.length; i++) {
        li = $('<li class="item">' + items[i].name + '</li>').appendTo(root);
        li.click(function () {
            alert(items[i].data);
        });
    }
}

Этот пример не будет работать, как мы ожидаем потому, что переменная i внутри обработчика события click не соответствует порядковому номеру элемента в списке. Значение этой переменной всегда будет равно items.length.

Чтобы пример заработал, обернем обработчик в замыкание.

(function (i) {
    li.click(function () {
        alert(items[i].data);
    });
})(i);

Или можно сразу получить требуемый элемент.


(function (item) {
    li.click(function () {
        alert(item.data);
    });
})(items[i]);

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

Оставте свой комментарий

Конструкторы в JavaScript

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

Конструктор представляет собой обычную функцию.

var Module = function () {
};

или

function Module() {
}

Эти две записи тождественны. Я все-таки предпочитаю использовать первый вариант, так как он позволяет объявить функцию с достаточно сложным названием. Например, App.Package1.Module2 .

Для создания нового объекта конструктор вызывается через оператор new.

var instance = new Module();

Оператор new меняет значение переменной this внутри конструктора. В отличие от его обычного значения, this будет новым объектом. Тело конструктора обычно инициализирует поля объекта. По завершению конструктор вернет этот новый объект, если точка выхода не будет явно переопределена через оператор return.

Некоторый смущающий момент заключается в том, что если конструктор будет вызван без оператора new, то this уже не будет новым объектом, а будет ссылаться на window!

Чтобы обезопасить себя от различных ошибок, в конструкторе можно принудительно создавать новый объект и возвращать его.


var Module = function () {
    var me = {};
    return me;
};

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

Оставте свой комментарий

Система оповещения модулей

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

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


/**
 *  Observable Object
 *  Just call it in constructor of your class (delegate pattern)
 *  Ex: var me = Observable(); // create new object
 *      or
 *      Observable(me);        // extend existing object
 */
var Observable = function (target) {
    // callback holder
    var observers = {};
    // create new object if needed
    target = target || {};

    target.attachObserver = function (eventType, callback) {
        if (!observers[eventType]) {
            observers[eventType] = [];
        }
        observers[eventType].push(callback);
        return this;
    };

    target.detachObserver = function (eventType, callback) {
        var a, i;
        if (eventType) {
            if (observers[eventType] && callback) {
                a = observers[eventType];
                for (i = 0; i < a.length; i++) {
                    if (a[i] === callback) {
                        a.splice(i, 1);
                        break;
                    }
                }
            } else {
                delete observers[eventType];
            }
        } else {
            observers = {};
        }
        return this;
    };

    target.notify = function () {
        var a, i, handler, data = [];
        // copy notification arguments
        Array.prototype.push.apply(data, arguments);
        a = observers[data.shift()];
        if (a && a.length > 0) {
            for (i = 0; i < a.length; i++) {
                handler = a[i];
                if (typeof handler === 'string') {
                    handler = this[handler];
                }
                if (handler instanceof Function) {
                    handler.apply(this, data);
                }
            }
        }
        return this;
    };

    return target;
};

Этот компонент при инициализации создает в указанном объекте 3 метода:

  • attachObserver — добавляет колбек для определенного события
  • detachObserver — удаляет один или все колбеки для определенного события
  • notify — инициализирует событие, при этом вызываются все зарегистрированные колбеки с указанными аргументами

Все указанные методы поддерживают цепочный вызов.


var FieldObject = function (root) {
    var me = Observable(this);

    $(root).append('<div class="field"><input type="text" value="" /></div>');
    $('div.field input', root).change(function () {
        me.notify('valueDidChange', this.value);
    });

    return me;
};

FieldObject($('body'))
    .attachObserver('valueDidChange', function (data) {
        alert(data);
    });

В этом примере я создал простой программный компонент FieldObject, которой добавляет на страницу текстовое поле, а затем подписался на событие valueDidChange , которое случается при обновлении этого текстового поля. Весь фокус в том, что я могу не знать, как устроен этот объект, и откуда получить значение его текстового поля. Все что мне нужно – это отреагировать на изменение поля.

Оставте свой комментарий

Привязка данных к объектам страницы

Мне почти каждый день приходится писать различные виджеты для манипуляции с данными — динамические таблицы, списки из красиво оформленных однотипных элементов и т.п. Конечно же, в этих элементах были кнопки или поля ввода, которые влияли на данные, связанные с этим элементом.

Для небольших виджетов я нашел оптимальным отделять этап создания представления и подключения контролера.


var obj = [], i;
obj.push('<div class="wrapper">');
for (i = 0; i < model.lenght; i++) {
    obj.push('<div class="item">');
    obj.push('<h1>', model[i].title, '</h1>');
    obj.push('<div class="input"><input type="text" value="', model[i].value, '" /></div>');
    obj.push('</div>');
}
obj.push('</div>');
$(root).html(obj.join(''));
$('div.input input', root).change(function () {
    // нужно сохранить значение в модель
});

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


(function () {
    var uidx = 1, storage = {};

    /**
     * Generate an id that is unique among application
     * @method guid
     * @param pre {String} optional guid prefix
     * @return {String} the guid
     */
    APP.guid = function (pre) {
        var p = (pre) || 'app';
        return p + '-' + uidx++;
    };

    /**
     * Make a save point for linking DOMNodes and Objects via IDs
     * @param obj {Object}
     * @param pre {String} optional guid prefix
     * @return {String} the guid
     */
    APP.savepoint = function (obj, pre) {
        var guid = APP.guid(pre);
        storage[guid] = obj;
        return guid;
    };

    /**
     * Recall data for save point
     * @param guid {String} id of save point
     * @return {Object} stored data
     */
    APP.recall = function (guid) {
        return (guid in storage) ? storage[guid] : null;
    };
})();

Теперь поле ввода я создаю с уникальным id.


obj.push('<div class="input"><input type="text" value="', model[i].value, '" id="', APP.savepoint(model[i]), '" /></div>');

А реализация обработчика выглядит не менее сложной.

function () {
    var o = APP.recall(this.id);
    o.value = this.value;
}

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

Оставте свой комментарий

Динамическая загрузка JavaScript файлов

Техника динамической загрузки данных через XHR (в простонародии, AJAX ) очень популярна в современном WEB-е. Но вот динамическую загрузку программных компонент используют пока крайне редко. Компактный модуль ScriptLoader наглядно демонстрирует то, как это может быть просто и функционально.


/**
 * JavaScript loader
 * @param modules {Array} массив имен js файлов
 */
var ScriptLoader = function (modules) {
    this.modules = modules.slice();
    this.queue = new Queue();
    this._init();
};

ScriptLoader.prototype = {
    run: function () {
        // test for header ready
        var head = document.getElementsByTagName("head");
        if (head.length > 0) {
            this.queue.iterate();
        } else {
            setTimeout(arguments.callee, 500);
        }
    },

    _init: function () {
        var i, me = this;
        for (i = 0; i < me.modules.length; i++) {
            (function (name) {
                me.queue.add(function () {
                    var head, script;
                    head = document.getElementsByTagName("head");
                    if (head.length > 0) {
                        head = head[0];
                        script = document.createElement("script");
                        script.src = name;
                        script.type = "text/javascript";
                        script.onload = script.onreadystatechange = function () {
                            if ((!this.readyState || this.readyState == "loaded" || this.readyState == "complete") ) {
                                me.queue.iterate();
                                script.onload = script.onreadystatechange = null;
                                head.removeChild(script);
                            }
                        };
                        head.appendChild(script);
                    }
                });
            })(me.modules[i]);
        }
    }
};

Обратите внимание, что в этом модуле используется Queue.

Пример использования.


var loader = new ScriptLoader(["file1.js", "file2.js", "http://server.org/js/framework.js"]);
// на этом этапе в очередь могут быть добавлены дополнительные вызовы
loader.queue.add(function () {
    Module1.init();
});
loader.run();

Комментарии к заметке: 2