Пользователь
0,0
рейтинг
8 апреля 2011 в 17:23

Разработка → Модульный подход в JavaScript перевод

Модульный подход довольно распространённая техника программирования в JavaScript. Обычно его понимают довольно хорошо, но продвинутые техники описаны недостаточно. В этой статье я рассмотрю основы и затрону некоторые сложные приёмы, включая один, по моему мнению, оригинальный.

Основы



Мы начнём с несложного обзора модульного подхода, хорошо известного с тех пор, как Эрик Миралья (Eric Miraglia) из YUI впервые об этом написал. Если вам уже знаком модульный подход, переходите сразу к «Продвинутым техникам».

Анонимные замыкания



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

(function () { 
    // ... все var-ы и функции только внутри этого контекста
    // по-прежнему имеется доступ к глобальным переменным
}());
 

Обратите внимание на () вокруг анонимной функции. Этого требует язык, поскольку операторы, начинающиеся со слова function, всегда интерпретируются как объявления функций. Добавление () создаёт вместо этого функциональное выражение.

Глобальный импорт



JavaScript поддерживает так называемые умолчательные глобальные. Встретив имя переменной, интерпретатор проходит по цепочке контекстов назад в поисках оператора var для этого имени. Если таковой не находится, переменная полагается глобальной. Если она используется в присваивании, создаётся глобальная, если её ещё не было. Это означает, что использовать или создавать глобальные переменные в анонимных замыканиях очень просто. К сожалению, это ведёт к плохо поддерживаемому коду, так так (людям) не очевидно, какие переменные глобальны в данном файле

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

(function ($, YAHOO) { 
    // теперь в коде есть доступ к переменным jQuery (как $) и YAHOO
}(jQuery, YAHOO));
 

Экспорт модуля



Иногда вы хотите не просто использовать глобальные, вы хотите их объявить. Мы можем это легко сделать, экспортируя их через возвращаемое значение анонимной функции. Этот приём завершает основной модульный подход, вот полный пример:

var MODULE = (function () { 
    var my = {}, 
        privateVariable = 1; 
 
    function privateMethod() { 
        // ... 
    } 
 
    my.moduleProperty = 1; 
    my.moduleMethod = function () { 
        // ... 
    }; 
 
    return my; 
}());

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

Продвинутые подходы



Несмотря на то, что во многих случаях хватит вышеизложенных приёмов, мы можем их улучшить и создать весьма мощные, расширяемые конструкции. Рассмотрим их по очереди, начиная с нашего модуля по имени MODULE.

Пополнение



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

var MODULE = (function (my) { 
    my.anotherMethod = function () { 
        // added method... 
    }; 
 
    return my; 
}(MODULE));

Мы здесь снова используем var для единообразия, хотя это и не обязательно. После того, как это код выполнится, наш модуль будет иметь новый публичный метод под названием MODULE.anotherMethod. Файл пополнения также будет хранить своё собственное состояние и импортированные переменные.

Свободное пополнение



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

var MODULE = (function (my) { 
    // add capabilities... 
 
    return my; 
}(MODULE || {}));

В этой схеме оператор var нужен всегда. Заметьте, что импорт создаст модуль, если его ещё не было. Это означает, что вы можете использовать утилиты наподобие LABjs и загружать все свои файлы с модулями параллельно, без блокирования.

Ограниченное пополнение



Свободное пополнение это хорошо, но оно накладывает ограничения, главное из которых в том, что вы не можете безопасно переопределить члены модуля. Кроме того, вы не можете использовать члены модуля из других файлов во время инициализации (но можете после её) завершения. Ограниченное пополнение задаёт порядок загрузки, но позволяет переопределение. Вот простой пример (пополнение нашего старого MODULE):

var MODULE = (function (my) { 
    var old_moduleMethod = my.moduleMethod; 
 
    my.moduleMethod = function () { 
        // method override, has access to old through old_moduleMethod... 
    }; 
 
    return my; 
}(MODULE));

Здесь мы переопределили MODULE.moduleMethod, но сохранили ссылку на исходный метод, если она нужна.

Клонирование и наследование



var MODULE_TWO = (function (old) { 
    var my = {}, 
        key; 
 
    for (key in old) { 
        if (old.hasOwnProperty(key)) { 
            my[key] = old[key]; 
        } 
    } 
 
    var super_moduleMethod = old.moduleMethod; 
    my.moduleMethod = function () { 
        // override method on the clone, access to super through super_moduleMethod 
    }; 
 
    return my; 
}(MODULE));

Такой подход вносит некоторую элегантность, но за счёт гибкости. Члены, являющиеся объектами или функциями, не дублируются, они продолжают существовать как один объект с двумя именами. Изменение одного меняет и второе. Для объектов это можно исправить через рекурсивное клонирование, но функциям, похоже, ничем не помочь, разве что через eval. Так или иначе, я включил это для полноты картины.

Кросс-файловое состояние



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

var MODULE = (function (my) { 
    var _private = my._private = my._private || {}, 
        _seal = my._seal = my._seal || function () { 
            delete my._private; 
            delete my._seal; 
            delete my._unseal; 
        }, 
        _unseal = my._unseal = my._unseal || function () { 
            my._private = _private; 
            my._seal = _seal; 
            my._unseal = _unseal; 
        }; 
 
    // permanent access to _private, _seal, and _unseal 
 
    return my; 
}(MODULE || {}));

Любой файл может задать члены на локальной переменной _private, и они будут немедленно доступны снаружи. Как только это модуль полностью загрузится, приложение должно вызввать MODULE.seal(), что предотвратит доступ извне к внутренней _private. Если мы хотим пополнить модуль ещё за время жизни приложения, один из внутренних методов может вызвать _unseal() перед загрузкой нового файла, а потом _seal() после его выполнения.

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

Подмодули



Наш последний продвинутый подход самый простой. Для создания подмодулей есть много причин. Это как создать обычный модуль:

MODULE.sub = (function () { 
    var my = {}; 
    // ... 
 
    return my; 
}());

Сколь бы очевидно это ни было, мне показалось это стоящим упоминания. Подмодули имеют все свойства обычных модулей, включая пополнение и сохранения состояния.

Выводы



Большинство продвинутых подходов могут сочетаться. При создании сложного приложения я бы лично избрал свободное пополнение, приватные состояния и подмодули.

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

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

var UTIL = (function (parent, $) { 
    var my = parent.ajax = parent.ajax || {}; 
 
    my.get = function (url, params, callback) { 
        // ok, so I'm cheating a bit :) 
        return $.getJSON(url, params, callback); 
    }; 
 
    // etc... 
 
    return parent; 
}(UTIL || {}, jQuery));

Надеюсь, что вам понравилось, поделитесь своими мыслями. А теперь вперёд, писать на JavaScript модульно!
Перевод: Бен Черри (Ben Cherry)
Iļja Ketris @bubuq
карма
68,3
рейтинг 0,0

Самое читаемое Разработка

Комментарии (32)

  • –1
    Давно хотелось почитать нечто подобное.
  • 0
    наконец то я понял назначение замыканий в js :) спасибо за перевод и ссылку!
  • +2
    Далеко не все можно описать этими анонимными замыканиями. Не везде они удобны.

    var MODULE = (function (my) { 
        my.anotherMethod = function () { 
            // added method... 
        }; 
     
        return my; 
    }(MODULE));


    Мне кажется, что проще так:
    var MODULE = (function (my) { 
        this.anotherMethod = function () { 
            // added method... 
        }; 
     
        return my; 
    }).call(MODULE);

    • +1
      В чем преимущества вашей альтернативы?
      • +4
        Чисто визуально мне лично так больше понятно, что происходит.

        Ну и правильнее как-то создавать отдельный контекст (ну или использовать существующий) — потому что в исходном варианте this — это window (точнее, глобальный объект).
    • +2
      Вы хотели написать:

      var MODULE = (function () {
      this.anotherMethod = function () {
      // added method…
      };

      return this;
      }).call(MODULE);

      ?
      • 0
        Ох, да, конечно.
    • 0
      var MODULE = new function () {
          this.anotherMethod = function () {
              // added method...
          };
      
          return this;
      };
      


      Конечно, так модуль нельзя будет расширять где-то в другом месте, но мне такой подход вообще не нравится. Есть ведь отличный подход с прототипами, который намного ближе к идеологии JS
      • +1
        Хотя можно даже так:
        var MODULE = new function () {
            this.anotherMethod = function () {
                // added method...
            };
        };
        
        • 0
          var MODULE = new function () {
          Object( MODULE ).constructor.apply( this )
          this.anotherMethod = function () {
          // added method…
          };
          };
        • 0
          Имхо через конструктор — идеальный вариант.
      • 0
        А я и не говорил, что я в восторге. Но так иногда делают, и я иногда так делаю, когда это кажется уместным.
        Я всего лишь сделал более явным тот факт, что происходит расширение объекта.

        Да и вообще — будь это готовый фреймворк, было бы клево, а так — просто подход.

        Кроме того, если уж автор оригинала использует jQuery, то есть, имхо, более удачные способы инкапсуляции.
        • 0
          Я не делаю вам замечание. Просто дополнил)
          • 0
            Аналогично:)
  • –7
    Не дочитав, захотел сразу добавить в избранное. Пришлось проматывать вниз и искать звёздочку. Досадная недоработка в интерфейсе, неплохо бы добавить её и около заголовка.
    • +5
      Не дочитав хотел сразу откоментировать, пришлось проматывать и искать форму. Досадная недоработка в интерфейсе… Неплохобы форму коментариев добавить в хабраленту.
  • +2
    Самое хитрое в модульном подходе это моменты модульной сборки и возможности в модуле определять различные классы работающие именно в пространстве данного модуля( что тянет за собой различные implements\provide\register\require и другие exports).
    А описание переменой в локальном замыкании — это разве модульность?
  • +1
    Вообще YUI3 занятная штука, крайне рекомендую
  • +7
    Я больше предпочитаю «активный» экспорт. Потому, что он больше вяжется с CommonJS Modules и отлично подходит как для Node.js так и для браузера.
    (function (exports/* остальные объекты и алиасы по вкусу */) {
        var MyObject = {};
    
        exports.MyObject = MyObject;
    }(typeof exports === 'undefined' ? window : exports));
    
    // Если надо расширить, то
    (function (MyObject) {
        MyObject.smth = function () {};
    }(typeof require === 'undefined' ? MyObject : require('MyObject'));
    

    Если использовать такой вид модуля, не экспортировать в глобалы и написать хороший сборщик (1 .js файл), то мы можем избавить window от лишних глобальных объектов, что является самой сложной техникой JavaScript Ninja ;)
    • 0
      Мне нравится ваше кунг-фу:)
    • 0
      может отдельную статью запостишь по этому поводу?
      возможно не до конца понял, но видимо это самый верный путь.
      поделишься подходом в развёрнутом виде??
    • 0
      CommonJS modules это вещь. Если хотите поэксперементировать с ними, советую попробовать прямо из браузера на Akshelle
      • 0
        ага, вещь. из-за их баловства с эвалом фиг найдёшь место ошибки.
        а в акшелле нет банальной консоли ошибок, из-за чего полезность всех их свистелок стремится к нулю.
        • 0
          С каким эвалом? Он вообще здесь ни причем.

          А в акшеле stack trace видно в HTML в Preview и в Eval консоле тоже. Добавь строку 'throw new Error();' и убедись сам(а).
          • 0
            docs.google.com/present/view?hl=en&id=dcd8d5dk_0cs639jg8 9 слайд

            днём не выводил. хотя, может я писал просто throw 123, не помню уже.

            • 0
              Ну это только в их имплементации, вообще-то эвал в других местах не используется для этого.
  • +8
    Никогда не понимал, зачем тащить всякие private/protected из языков вроде Java в динамические языки вроде JavaScript или Perl. Да, с помощью клозур и такой-то матери можно что-то спрятать, но нафига это нужно?! Если кто-то захочет залезть внутрь и отстреляться себе по ногам — всё-равно залезет и отстреляется, так или иначе. А потом начинают изобретать извращения чтобы добраться к тому, к чему сами себе закрыли доступ.

    Практика показывает, что для разделения на public и private более чем достаточно соглашения об именах: private методы и свойства начинаются на подчёркивание. Этого более чем достаточно, чтобы сохранить наглядность интерфейса, и чтобы легко обнаруживать нарушения инкапсуляции во время code review (а то и автоматически, просто запустив grep по исходникам).
    • +1
      Статья повествует о том, как вынести детали реализации из глобальной области видимости по модулям.
      Вы же не отрицаете тот факт, что разные программисты в разных частях одной программы могут случайно использовать одинаковые глобальные идентификаторы, что может привести к достаточно противным ошибкам?
      • +1
        Не отрицаю. Но я об этом ничего и не говорил. Просто я для реализации модулей предпочитаю более нативный подход через prototype, а не клозуры:
        
        var MODULE = function(){
            this.public = 'some';
            this._private = 'some';
        };
        
        MODULE.prototype.public_method = function(){
           this._private_method();
        };
        
        MODULE.prototype._private_method = function(){
           …
        };
        
        obj = new MODULE();
        

        На мой взгляд это значительно проще и читабельнее, чем вариант на клозурах.
    • –1
      приватные поля предназначены для того, чтобы можно было объявлять их в родителе не боясь, что потомок их _случайно_ испоганит решив воспользоваться тем же именем. никакие подчёркивания тут не помогут.
      • 0
        Я стараюсь не злоупотреблять наследованием. Даже, я бы сказал, не употреблять вообще. :) Это сильно упрощает жизнь.

        А в тех редких случаях, когда наследование действительно удобно — как правило либо классы родителя и потомков пишутся одним разработчиком, контролирующим такие конфликты, либо в потомке для гарантии отсутствия конфликтов его личные приватные свойства и методы содержат префикс — имя потомка. Да, удобным и элегантным подходом это никак не назовёшь, и при активном использовании наследования описанные в статье подходы будут предпочтительнее. Но если наследование используется в качестве исключения, а не правила, то этих простых подходов более чем достаточно.
  • 0
    Спасибо за статью, давно хотел разобраться как проще всего организовать модули на JavaScript без внешних библиотек.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.