Паттерны для обработки событий в Javascript. А теперь начнем изучать паттерны JavaScript вместе с углубленными знаниями проектирования. Одиночные глобальные переменные

This UML describes how a prototype interface is used to clone concrete implementations.

To clone an object, a constructor must exist to instantiate the first object. Next, by using the keyword prototype variables and methods bind to the object"s structure. Let"s look at a basic example:

Var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = "Tesla"; this.make = "Model S"; } TeslaModelS.prototype.go = function() { // Rotate wheels } TeslaModelS.prototype.stop = function() { // Apply brake pads }

The constructor allows the creation of a single TeslaModelS object. When a creating new TeslaModelS object, it will retain the states initialized in the constructor. Additionally, maintaining the function go and stop is easy since we declared them with prototype . A synonymous way to extend functions on the prototype as described below:

Var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = "Tesla"; this.make = "Model S"; } TeslaModelS.prototype = { go: function() { // Rotate wheels }, stop: function() { // Apply brake pads } }

Revealing Prototype Pattern

Similar to Module pattern, the Prototype pattern also has a revealing variation. The Revealing Prototype Pattern provides encapsulation with public and private members since it returns an object literal.

Since we are returning an object, we will prefix the prototype object with a function . By extending our example above, we can choose what we want to expose in the current prototype to preserve their access levels:

Var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = "Tesla"; this.make = "Model S"; } TeslaModelS.prototype = function() { var go = function() { // Rotate wheels }; var stop = function() { // Apply brake pads }; return { pressBrakePedal: stop, pressGasPedal: go } }();

Note how the functions stop and go will be shielded from the returning object due to being outside of returned object"s scope. Since JavaScript natively supports prototypical inheritance, there is no need to rewrite underlying features.

There are many times when one part of the application changes, other parts needs to be updated. In AngularJS, if the $scope object updates, an event can be triggered to notify another component. The observer pattern incorporates just that - if an object is modified it broadcasts to dependent objects that a change has occurred.

Another prime example is the model-view-controller (MVC) architecture; The view updates when the model changes. One benefit is decoupling the view from the model to reduce dependencies.

Wikipedia

As shown in the UML diagram, the necessary objects are the subject , observer , and concrete objects. The subject contains references to the concrete observers to notify for any changes. The Observer object is an abstract class that allows for the concrete observers to implements the notify method.

Let"s take a look at an AngularJS example that encompasses the observer pattern through event management.

// Controller 1 $scope.$on("nameChanged", function(event, args) { $scope.name = args.name; }); ... // Controller 2 $scope.userNameChanged = function(name) { $scope.$emit("nameChanged", {name: name}); };

With the observer pattern, it is important to distinguish the independent object or the subject .

It is important to note that although the observer pattern does offer many advantages, one of the disadvantages is a significant drop in performance as the number of observers increased. One of the most notorious observers is watchers . In AngularJS, we can watch variables, functions, and objects. The $$digest cycle runs and notifies each of the watchers with the new values whenever a scope object is modified.

We can create our own Subjects and Observers in JavaScript. Let"s see how this is implemented:

Var Subject = function() { this.observers = ; return { subscribeObserver: function(observer) { this.observers.push(observer); }, unsubscribeObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers.splice(index, 1); } }, notifyObserver: function(observer) { var index = this.observers.indexOf(observer); if(index > -1) { this.observers.notify(index); } }, notifyAllObservers: function() { for(var i = 0; i < this.observers.length; i++){ this.observers[i].notify(i); }; } }; }; var Observer = function() { return { notify: function(index) { console.log("Observer " + index + " is notified!"); } } } var subject = new Subject(); var observer1 = new Observer(); var observer2 = new Observer(); var observer3 = new Observer(); var observer4 = new Observer(); subject.subscribeObserver(observer1); subject.subscribeObserver(observer2); subject.subscribeObserver(observer3); subject.subscribeObserver(observer4); subject.notifyObserver(observer2); // Observer 2 is notified! subject.notifyAllObservers(); // Observer 1 is notified! // Observer 2 is notified! // Observer 3 is notified! // Observer 4 is notified!

Publish/Subscribe

The Publish/Subscribe pattern, however, uses a topic/event channel that sits between the objects wishing to receive notifications (subscribers) and the object firing the event (the publisher). This event system allows code to define application-specific events that can pass custom arguments containing values needed by the subscriber. The idea here is to avoid dependencies between the subscriber and publisher.

This differs from the Observer pattern since any subscriber implementing an appropriate event handler to register for and receive topic notifications broadcast by the publisher.

Many developers choose to aggregate the publish/subscribe design pattern with the observer though there is a distinction. Subscribers in the publish/subscribe pattern are notified through some messaging medium, but observers are notified by implementing a handler similar to the subject.

In AngularJS, a subscriber "subscribes" to an event using $on("event", callback), and a publisher "publishes" an event using $emit("event", args) or $broadcast("event", args).

Singleton

A Singleton only allows for a single instantiation, but many instances of the same object. The Singleton restricts clients from creating multiple objects, after the first object created, it will return instances of itself.

Finding use cases for Singletons is difficult for most who have not yet used it prior. One example is using an office printer. If there are ten people in an office, and they all use one printer, ten computers share one printer (instance). By sharing one printer, they share the same resources.

Var printer = (function () { var printerInstance; function create () { function print() { // underlying printer mechanics } function turnOn() { // warm up // check for paper } return { // public + private states and behaviors print: print, turnOn: turnOn }; } return { getInstance: function() { if(!printerInstance) { printerInstance = create(); } return printerInstance; } }; function Singleton () { if(!printerInstance) { printerInstance = intialize(); } }; })();

The create method is private because we do not want the client to access this, however, notice that the getInstance method is public. Each officer worker can generate a printer instance by interacting with the getInstance method, like so:

Var officePrinter = printer.getInstance();

In AngularJS, Singletons are prevalent, the most notable being services, factories, and providers. Since they maintain state and provides resource accessing, creating two instances defeats the point of a shared service/factory/provider.

Race conditions occur in multi-threaded applications when more than one thread tries to access the same resource. Singletons are susceptible to race conditions, such that if no instance were initialized first, two threads could then create two objects instead of returning and instance. This defeats the purpose of a singleton. Therefore, developers must be privy to synchronization when implementing singletons in multithreaded applications.

Conclusion

Design patterns are frequently used in larger applications, though to understand where one might be advantageous over another, comes with practice.

Before building any application, you should thoroughly think about each actor and how they interact with one another. After reviewing the Module , Prototype , Observer , and Singleton design patterns, you should be able to identify these patterns and use them in the wild.

Сегодня мы собираемся окунуться в компьютерную науку, так как узнаем о некоторых общих шаблонах проектирования. Шаблоны проектирования предлагают разработчикам способы решения технических проблем в многоразовом и элегантном стиле. Хотите стать лучшим разработчиком JavaScript? Тогда читайте дальше.

Переизданный учебник

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

Введение

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

Что такое шаблон проектирования?

Шаблон проектирования является многоразовым программным решением

Проще говоря, шаблон проектирования является многоразовым программным решением для определенного типа проблем, который часто возникает при разработке программного обеспечения. В течение многих лет практики разработки программного обеспечения эксперты выяснили способы решения подобных проблем. Эти решения были инкапсулированы в шаблоны проектирования. Так:

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

Мы рассмотрим некоторые примеры шаблонов проектирования в учебнике.

Типы шаблонов проектирования

При разработке программного обеспечения шаблоны проектирования обычно группируются по нескольким категориям. Мы рассмотрим три наиболее важных из этих уроков. Они поясняются ниже:

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

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

Заметка о классах в JavaScript

Когда вы читаете о шаблонах проектирования, вы часто увидите ссылки на классы и объекты. Это может сбивать с толку, поскольку у JavaScript действительно нет конструкции класса; более правильным термином является «тип данных».

Типы данных в JavaScript

JavaScript - это объектно-ориентированный язык, где объекты наследуются от других объектов в концепции, известной как прототипное наследование. Тип данных может быть создан путем определения функции, называемой конструктором, например:

Function Person(config) { this.name = config.name; this.age = config.age; } Person.prototype.getAge = function() { return this.age; }; var tilo = new Person({name:"Tilo", age:23 }); console.log(tilo.getAge());

Обратите внимание на использование prototype при определении методов в типе данных Person . Поскольку объекты с несколькими Person ссылаются на один и тот же прототип, это позволяет использовать метод getAge() всеми экземплярами типа данных Person , а не переопределять его для каждого экземпляра. Кроме того, любой тип данных, который наследуется от Person , будет иметь доступ к методу getAge() .

Работа с конфиденциальностью

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

Var retinaMacbook = (function() { //Private variables var RAM, addRAM; RAM = 4; //Private method addRAM = function (additionalRAM) { RAM += additionalRAM; }; return { //Public variables and methods USB: undefined, insertUSB: function (device) { this.USB = device; }, removeUSB: function () { var device = this.USB; this.USB = undefined; return device; } }; })();

В приведенном выше примере мы создали объект retinaMacbook с общедоступными и частными переменными и методами. Так мы будем использовать его:

RetinaMacbook.insertUSB("myUSB"); console.log(retinaMacbook.USB); //logs out "myUSB" console.log(retinaMacbook.RAM) //logs out undefined

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

Порождающие шаблоны проектирования

Существует много разных типов шаблонов создания, но мы рассмотрим два из них в этом уроке: Builder и Prototype. Я считаю, что они используются достаточно часто, чтобы уделить им внимание.

Шаблон Builder

Шаблон Builder часто используется в веб-разработке, и вы, вероятно, использовали его раньше, не осознавая этого. Проще говоря, этот шаблон можно определить следующим образом:

Применение шаблона построителя позволяет нам создавать объекты, указывая только тип и содержимое объекта. Нам не нужно явно создавать объект.

Например, вы, вероятно, сделали это бесчисленное количество раз в jQuery:

Var myDiv = $("

This is a div.
"); //myDiv now represents a jQuery object referencing a DOM node. var someText = $("

"); //someText is a jQuery object referencing an HTMLParagraphElement var input = $("");

Взгляните на три примера выше. В первом мы передали элемент

с некоторым контентом. Во втором мы передали пустой тег

В последнем мы передали элемент . Результат всех трех был одинаковым: нам был возвращен объект jQuery, ссылающийся на узел DOM.

Переменная $ реализует шаблон Builder в jQuery. В каждом примере нам был возвращен объект JQuery DOM и он имел доступ ко всем методам, предоставляемым библиотекой jQuery, но ни в коем случае мы явно не вызывали document.createElement . Библиотека JS обрабатывала все это под капотом.

Представьте, сколько было бы работы, если бы мы должны были явно создать элемент DOM и вставлять в него контент! Используя шаблон построителя, мы можем сосредоточиться на типе и содержании объекта, а не на его явном создании.

Шаблон прототипа

Ранее мы рассмотрели, как определить типы данных в JavaScript через функции и добавить методы к prototype объекта. Шаблон Prototype позволяет объектам наследовать от других объектов через их прототипы.

Шаблон прототипа - это шаблон, в котором объекты создаются на основе шаблона существующего объекта путем клонирования.

Это простой и естественный способ реализации наследования в JavaScript. Например:

Var Person = { numFeet: 2, numHeads: 1, numHands:2 }; //Object.create takes its first argument and applies it to the prototype of your new object. var tilo = Object.create(Person); console.log(tilo.numHeads); //outputs 1 tilo.numHeads = 2; console.log(tilo.numHeads) //outputs 2

Свойства (и методы) объекта Person применяются к прототипу объекта tilo . Мы можем переопределить свойства объекта tilo , если хотим, чтобы они были разными.

В приведенном выше примере мы использовали Object.create() . Однако Internet Explorer 8 не поддерживает новый метод. В этих случаях мы можем имитировать поведение:

Var vehiclePrototype = { init: function (carModel) { this.model = carModel; }, getModel: function () { console.log("The model of this vehicle is " + this.model); } }; function vehicle (model) { function F() {}; F.prototype = vehiclePrototype; var f = new F(); f.init(model); return f; } var car = vehicle("Ford Escort"); car.getModel();

Единственным недостатком этого метода является то, что вы не можете указывать свойства только для чтения, которые могут быть указаны при использовании Object.create() . Тем не менее, образец прототипа показывает, как объекты могут наследоваться от других объектов.

Структурные шаблоны проектирования

Структурные шаблоны проектирования действительно полезны при определении того, как система должна работать. Они позволяют нашим приложениям легко масштабироваться и оставаться обслуживаемыми. Мы рассмотрим следующие шаблоны в этой группе: Composite и Facade.

Композитный шаблон

composite шаблон - это еще один шаблон, который вы, вероятно, использовали раньше, без какой-либо реализации.

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

Так что это значит? Ну, рассмотрим этот пример в jQuery (у большинства JS-библиотек будет эквивалент этого):

$(".myList").addClass("selected"); $("#myItem").addClass("selected"); //dont do this on large tables, it"s just an example. $("#dataTable tbody tr").on("click", function(event){ alert($(this).text()); }); $("#myButton").on("click", function(event) { alert("Clicked."); });

Большинство библиотек JavaScript предоставляют согласованный API независимо от того, имеем ли мы дело с одним элементом DOM или с массивом элементов DOM. В первом примере мы можем добавить класс select ко всем элементам, выбранным селектором.myList , но мы можем использовать тот же метод при работе с единственным элементом DOM, #myItem . Аналогично, мы можем присоединить обработчики событий, используя метод on() на нескольких узлах или на одном узле через один и тот же API.

Используя шаблон Composite, jQuery (и многие другие библиотеки) предоставляют нам упрощенный API.

Шаблон composite иногда может вызывать проблемы. В свободно типизированном языке, таком как JavaScript, часто бывает полезно знать, имеем ли мы дело с одним элементом или несколькими элементами. Поскольку шаблон composite использует один и тот же API для обоих случаев, мы иногда можем ошибаться и принимать один объект за несколько и наоборот. Некоторые библиотеки, такие как YUI3, предлагают два отдельных метода получения элементов (Y.one() против Y.all()).

Фасад

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

Шаблон Facade предоставляет пользователю простой интерфейс, скрывая его сложность.

Шаблон Facade почти всегда улучшает удобство использования программного обеспечения. Используя jQuery в качестве примера снова, одним из наиболее популярных методов библиотеки является метод ready() :

$(document).ready(function() { //all your code goes here... });

Метод ready() фактически реализует фасад. Если вы посмотрите на исходный код, вот что вы найдете:

Ready: (function() { ... //Mozilla, Opera, and Webkit if (document.addEventListener) { document.addEventListener("DOMContentLoaded", idempotent_fn, false); ... } //IE event model else if (document.attachEvent) { // ensure firing before onload; maybe late but safe also for iframes document.attachEvent("onreadystatechange", idempotent_fn); // A fallback to window.onload, that will always work window.attachEvent("onload", idempotent_fn); ... } })

Под капотом метод ready() не так прост. jQuery нормализует несоответствия браузера, чтобы гарантировать, что ready() будет запущен в соответствующее время. Однако, как разработчику, вам предоставляется простой интерфейс.

Большинство примеров шаблона Фасад следуют этому принципу. При реализации одного мы обычно полагаемся на условные операторы под капотом, но представляем его как простой интерфейс для пользователя. Другие методы, реализующие этот шаблон, включают animate() и css() . Можете ли вы подумать, почему они будут использовать шаблон фасада?

Поведенческие шаблоны проектирования

Любые объектно-ориентированные программные системы будут иметь связь между объектами. Если не организовать это общение, то это может привести к ошибкам, которые трудно найти и исправить. Поведенческие шаблоны проектирования предписывают разные способы организации связи между объектами. В этом разделе мы рассмотрим шаблоны Observer и Mediator.

Шаблон наблюдатель

Шаблон Observer является первым из двух моделей поведения, которые мы собираемся пройти. Вот его определение:

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

Звучит довольно просто, не так ли? Нам нужно три метода для описания этого шаблона:

  • publish(data) : Вызывается субъектом, когда у него есть уведомление. В этот метод могут быть переданы некоторые данные.
  • subscribe(observer) : вызывается субъектом чтобы добавить наблюдателя в свой список наблюдателей.
  • unsubscribe(observer) : вызывается субъектом для удаления наблюдателя из его списка наблюдателей.

Ну, оказывается, что большинство современных библиотек JavaScript поддерживают эти три метода как часть инфраструктуры пользовательских событий. Обычно есть метод on() или attach() , метод trigger() или fire() и метод off() или detach() . Рассмотрим следующий фрагмент:

//We just create an association between the jQuery events methods //and those prescribed by the Observer Pattern but you don"t have to. var o = $({}); $.subscribe = o.on.bind(o); $.unsubscribe = o.off.bind(o); $.publish = o.trigger.bind(o); // Usage document.on("tweetsReceived", function(tweets) { //perform some actions, then fire an event $.publish("tweetsShow", tweets); }); //We can subscribe to this event and then fire our own event. $.subscribe("tweetsShow", function() { //display the tweets somehow .. //publish an action after they are shown. $.publish("tweetsDisplayed); }); $.subscribe("tweetsDisplayed, function() { ... });

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

Посредник

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

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

Хорошей аналогией в реальном мире будет вышка в аэропорте, которая обрабатывает связь между аэропортом и рейсами. В мире разработки программного обеспечения шаблон Mediator часто используется, по мере того как система становится слишком сложной. Посредством размещения медиаторов связь может обрабатываться через один объект, а не с несколькими объектами, обменивающимися друг с другом. В этом смысле шаблон медиатора может использоваться для замены системы, которая реализует шаблон наблюдателя.

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

$("#album").on("click", function(e) { e.preventDefault(); var albumId = $(this).id(); mediator.publish("playAlbum", albumId); }); var playAlbum = function(id) { … mediator.publish("albumStartedPlaying", {songList: [..], currentSong: "Without You"}); }; var logAlbumPlayed = function(id) { //Log the album in the backend }; var updateUserInterface = function(album) { //Update UI to reflect what"s being played }; //Mediator subscriptions mediator.subscribe("playAlbum", playAlbum); mediator.subscribe("playAlbum", logAlbumPlayed); mediator.subscribe("albumStartedPlaying", updateUserInterface);

Преимущество этого шаблона над шаблоном Observer заключается в том, что за коммуникацию отвечает один объект, тогда как в шаблоне наблюдателя несколько объектов могут прослушиваться и подписываться друг на друга.

В шаблоне Observer нет единого объекта, который инкапсулирует ограничение. Вместо этого наблюдатель и субъект должны сотрудничать для поддержания ограничения. Шаблоны комуникации определяются тем, как наблюдатели и субъекты взаимосвязаны: у одного субъекта обычно есть много наблюдателей, и иногда наблюдатель одного субъекта является субъектом другого наблюдателя.

Заключение

Кто-то уже успешно применял это в прошлом.

Самое замечательное в шаблонах проектирования - то, что кто-то уже успешно применял его в прошлом. Существует множество открытых исходных кодов, которые реализуют различные шаблоны в JavaScript. Как разработчикам, нам нужно знать, какие шаблоны есть и когда их применять. Я надеюсь, что этот урок помог вам сделать еще один шаг к ответу на эти вопросы.

Для дополнительного чтения

Большая часть контента из этой статьи находится в отличной книге « » Адди Османи. Это онлайн-книга, которая была выпущена бесплатно под лицензией Creative Commons. Книга широко охватывает теорию и реализацию множества различных шаблонов, как в ванильном JavaScript, так и в различных JS-библиотеках. Я призываю вас заглянуть в нее, когда вы начнете свой следующий проект.

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

Зачем нужны паттерны проектирования?

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

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

Например, если вы используете паттерн «Декоратор», это тут же сообщит новому программисту, пришедшему в проект, о том, какие именно задачи решает некий фрагмент кода и зачем он нужен. Благодаря этому такой программист сможет больше времени уделить практическим задачам, которые решает программа, а не попыткам понять её внутреннее устройство.

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

Паттерн «Модуль»

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

Модули - это составная часть любого современного JavaScript-приложения. Они помогают поддерживать чистоту кода, способствуют разделению кода на осмысленные фрагменты и помогают его организовывать. В JavaScript существует множество способов создания модулей, одним из которых является паттерн «Модуль» (Module).

В отличие от других языков программирования, JavaScript не имеет модификаторов доступа. То есть, переменные нельзя объявлять как приватные или публичные. В результате паттерн «Модуль» используется ещё и для эмуляции концепции инкапсуляции.

Этот паттерн использует IIFE (Immediately-Invoked Functional Expression, немедленно вызываемое функциональное выражение), замыкания и области видимости функций для имитации этой концепции. Например:

Const myModule = (function() { const privateVariable = "Hello World"; function privateMethod() { console.log(privateVariable); } return { publicMethod: function() { privateMethod(); } } })(); myModule.publicMethod();

Так как перед нами IIFE, код выполняется немедленно и возвращаемый выражением объект назначается константе myModule . Благодаря тому, что тут имеется замыкание, у возвращённого объекта есть доступ к функциям и переменным, объявленных внутри IIFE, даже после завершения работы IIFE.

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

После того, как этот код будет выполнен, myModule будет выглядеть следующим образом:

Const myModule = { publicMethod: function() { privateMethod(); }};

То есть, обращаясь к этой константе, можно вызвать общедоступный метод объекта publicMethod() , который, в свою очередь, вызовет приватный метод privateMethod() . Например:

// Выводит "Hello World" module.publicMethod();

Паттерн «Открытый модуль»

Паттерн «Открытый модуль» (Revealing Module) представляет собой немного улучшенную версию паттерна «Модуль», которую предложил Кристиан Хейльманн. Проблема паттерна «Модуль» заключается в том, что нам приходится создавать публичные функции только для того, чтобы обращаться к приватным функциям и переменным.

В рассматриваемом паттерне мы назначаем свойствам возвращаемого объекта приватные функции, которые хотим сделать общедоступными. Именно поэтому данный паттерн и называют «Открытый модуль». Рассмотрим пример:

Const myRevealingModule = (function() { let privateVar = "Peter"; const publicVar = "Hello World"; function privateFunction() { console.log("Name: "+ privateVar); } function publicSetName(name) { privateVar = name; } function publicGetName() { privateFunction(); } /** открываем функции и переменные, назначая их свойствам объекта */ return { setName: publicSetName, greeting: publicVar, getName: publicGetName }; })(); myRevealingModule.setName("Mark"); // Выводит Name: Mark myRevealingModule.getName();

Применение этого паттерна упрощает понимание того, какие функции и переменные модуля общедоступны, что способствует улучшению читабельности кода.

После выполнения IIFE myRevealingModule выглядит так:

Const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName };

Мы можем, например, вызвать метод myRevealingModule.setName("Mark") , который представляет собой ссылку на внутреннюю функцию publicSetName . Метод myRevealingModule.getName() ссылается на внутреннюю функцию publicGetName . Например:

MyRevealingModule.setName("Mark"); // выводит Name: Mark myRevealingModule.getName();

Рассмотрим преимущества паттерна «Открытый модуль» перед паттерном «Модуль»:

  • «Открытый модуль» позволяет делать общедоступными скрытые сущности модуля (и снова скрывать их, если нужно), модифицируя, для каждой из них, лишь одну строку в объекте, возвращаемом после выполнения IIFE.
  • Возвращаемый объект не содержит определения функций. Всё, что находится справа от имён его свойств, определено в IIFE. Это способствует чистоте кода и упрощает его чтение.

Модули в ES6

До выхода стандарта ES6 в JavaScript не было стандартного средства для работы с модулями, в результате разработчикам приходилось использовать сторонние библиотеки или паттерн «Модуль» для реализации соответствующих механизмов. Но с приходом ES6 в JavaScript появилась стандартная система модулей.

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

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

Есть два способа экспорта функции или переменной, объявленной в модуле:

  • Экспорт выполняется путём добавления ключевого слова export перед объявлением функции или переменной. Например:
// utils.js export const greeting = "Hello World"; export function sum(num1, num2) { console.log("Sum:", num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log("Subtract:", num1, num2); return num1 - num2; } // Это - приватная функция function privateLog() { console.log("Private Function"); }
  • Экспорт выполняется путём добавления ключевого слова export в конец кода с перечислением имён функций и переменных, которые нужно экспортировать. Например:
// utils.js function multiply(num1, num2) { console.log("Multiply:", num1, num2); return num1 * num2; } function divide(num1, num2) { console.log("Divide:", num1, num2); return num1 / num2; } // Это приватная функция function privateLog() { console.log("Private Function"); } export {multiply, divide};

Импорт модуля

Так же, как существуют два способа экспорта, есть и два способа импорта модулей. Делается это с использованием ключевого слова import:

  • Импорт нескольких избранных элементов. Например:
// main.js // Импорт нескольких избранных элементов import { sum, multiply } from "./utils.js"; console.log(sum(3, 7)); console.log(multiply(3, 7));
  • Импорт всего, что экспортирует модуль. Например:
// main.js // импорт всего, что экспортирует модуль import * as utils from "./utils.js"; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7));

Псевдонимы для экспортируемых и импортируемых сущностей

Если имена экспортируемых в код функций или переменных могут вызвать коллизию, их можно изменить при экспорте или при импорте.

Для переименования сущностей при экспорте можно поступить так:

// utils.js function sum(num1, num2) { console.log("Sum:", num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log("Multiply:", num1, num2); return num1 * num2; } export {sum as add, multiply};

Для переименования сущностей при импорте используется такая конструкция:

// main.js import { add, multiply as mult } from "./utils.js"; console.log(add(3, 7)); console.log(mult(3, 7));

Паттерн «Синглтон»

Паттерн «Синглтон» или «Одиночка» (Singleton) представляет собой объект, который может существовать лишь в единственном экземпляре. В рамках применения этого паттерна новый экземпляр некоего класса создаётся в том случае, если он пока не создан. Если же экземпляр класса уже существует, то, при попытке обращения к конструктору, возвращается ссылка на соответствующий объект. Последующие вызовы конструктора всегда будут возвращать тот же самый объект.

Фактически, то, что мы называем паттерном «Синглтон», имелось в JavaScript всегда, но называют это не «Синглтоном», а «объектным литералом». Рассмотрим пример:

Const user = { name: "Peter", age: 25, job: "Teacher", greet: function() { console.log("Hello!"); } };

Так как каждый объект в JavaScript занимает собственную область памяти и не делит её с другими объектами, всякий раз, когда мы обращаемся к переменной user , мы получаем ссылку на один и тот же объект.

Паттерн «Синглтон» можно реализовать с использованием функции-конструктора. Выглядит это так:

Let instance = null; function User(name, age) { if(instance) { return instance; } instance = this; this.name = name; this.age = age; return instance; } const user1 = new User("Peter", 25); const user2 = new User("Mark", 24); // выводит true console.log(user1 === user2);

Когда вызывается функция-конструктор, она, в первую очередь, проверяет, существует ли объект instance . Если соответствующая переменная не инициализирована, в instance записывают this . Если же в переменной уже есть ссылка на объект, конструктор просто возвращает instance , то есть - ссылку на уже существующий объект.

Паттерн «Синглтон» можно реализовать с использованием паттерна «Модуль». Например:

Const singleton = (function() { let instance; function User(name, age) { this.name = name; this.age = age; } return { getInstance: function(name, age) { if(!instance) { instance = new User(name, age); } return instance; } } })(); const user1 = singleton.getInstance("Peter", 24); const user2 = singleton.getInstance("Mark", 26); // prints true console.log(user1 === user2);

Здесь мы создаём новый экземпляр user , вызывая метод singleton.getInstance() . Если экземпляр объекта уже существует, то этот метод просто возвратит его. Если же такого объекта пока нет, метод создаёт его новый экземпляр, вызывая функцию-конструктор User .

Паттерн «Фабрика»

Паттерн «Фабрика» (Factory) использует для создания объектов так называемые «фабричные методы». При этом не требуется указывать классы или функции-конструкторы, которые применяются для создания объектов.

Этот паттерн используется для создания объектов в случаях, когда не нужно делать общедоступной логику их создания. Паттерн «Фабрика» может быть использован в том случае, если нужно создавать различные объекты в зависимости от специфических условий. Например:

Class Car{ constructor(options) { this.doors = options.doors || 4; this.state = options.state || "brand new"; this.color = options.color || "white"; } } class Truck { constructor(options) { this.doors = options.doors || 4; this.state = options.state || "used"; this.color = options.color || "black"; } } class VehicleFactory { createVehicle(options) { if(options.vehicleType === "car") { return new Car(options); } else if(options.vehicleType === "truck") { return new Truck(options); } } }

Здесь созданы классы Car и Truck , которые предусматривают использование неких стандартных значений. Они применяются для создания объектов car и truck . Также здесь объявлен класс VehicleFactory , который используется для создания новых объектов на основе анализа свойства vehicleType , передаваемого соответствующему методу возвращаемого им объекта в объекте с параметрами options . Вот как со всем этим работать:

Const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: "car", doors: 4, color: "silver", state: "Brand New" }); const truck= factory.createVehicle({ vehicleType: "truck", doors: 2, color: "white", state: "used" }); // Выводит Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); // Выводит Truck {doors: 2, state: "used", color: "white"} console.log(truck);

Здесь создан объект factory класса VehicleFactory . После этого можно создавать объекты классов Car или Truck , вызывая метод factory.createVehicle() и передавая ему объект options со свойством vehicleType , установленным в значение car или truck .

Паттерн «Декоратор»

Паттерн «Декоратор» (Decorator) используется для расширения функционала объектов без модификации существующих классов или функций-конструкторов. Этот паттерн можно использовать для добавления к объектам неких возможностей без модификации кода, который ответственен за их создание.

Вот простой пример использования этого паттерна:

Function Car(name) { this.name = name; // Значение по умолчанию this.color = "White"; } // Создание нового объекта, который планируется декорировать const tesla= new Car("Tesla Model 3"); // Декорирование объекта - добавление нового функционала tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor("black"); tesla.setPrice(49000); // Выводит black console.log(tesla.color);

Рассмотрим теперь практический пример применения этого паттерна. Предположим, стоимость автомобилей зависит от их особенностей, от имеющихся у них дополнительных функций. Без использования паттерна «Декоратор» нам, для описания этих автомобилей, пришлось бы создавать разные классы для разных комбинаций этих дополнительных функций, в каждом из которых присутствовал бы метод для нахождения стоимости автомобиля. Например, это может выглядеть так:

Class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { }

Благодаря рассматриваемому паттерну можно создать базовый класс Car , описывающий, скажем, автомобиль в базовой комплектации, стоимость которого выражается некоей фиксированной суммой. После этого стандартный объект, создаваемый на основе этого класса, можно расширять с использованием функций-декораторов. Стандартный «автомобиль», обработанный такой функцией, получает новые возможности, что, кроме того, влияет на его цену. Например, эту схему можно реализовать так:

Class Car { constructor() { // Базовая стоимость this.cost = function() { return 20000; } } } // Функция-декоратор function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } } // Функция-декоратор function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 2000; } } // Функция-декоратор function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } }

Здесь мы сначала создаём базовый класс Car , используемый для создания объектов, представляющих автомобили в стандартной комплектации. Затем создаём несколько функций-декораторов, которые позволяют расширять объекты базового класса Car дополнительными свойствами. Эти функции принимают соответствующие объекты в качестве параметров. После этого мы добавляем в объект новое свойство, указывающее на то, какой новой возможностью будет оснащён автомобиль, и переопределяем функцию cost объекта, которая теперь возвращает новую стоимость автомобиля. В результате, для того, чтобы «оснастить» автомобиль стандартной конфигурации чем-то новым, мы можем воспользоваться следующей конструкцией:

Эта статья раскрывает варианты применения паттернов среднего и сложного уровня к пространствам имён(Namespace) в JavaScript.

  • Паттерн (англ. "pattern - образец, шаблон, система) - Смысл термина «паттерн» больше уже чем просто «образец», и варьируется в зависимости от области знаний, в которой используется. Паттерн (информатика) - эффективный способ решения характерных задач проектирования, в частности проектирования компьютерных программ.

Что такое Namespacing в JavaScript

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

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

Не смотря на то, что JavaScript не имеет встроенной поддержки Namespace, как другие языки программирования, в нём есть объекты и closeure(замыкания), которые позволяют достичь нужного эффекта.

Продвинутые паттерны организации Namespace

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

Автоматизация вложенных(nested) Namespace

Возможно, вам известно, что nested Namespace представляет собой иерархическую организацию структур. Рассмотрим, как структура вида application.utilities.drawing.canvas.2d может выглядеть на практике. Применительно к JavaScript это литерал-объект:

Var application = { utilities:{ drawing:{ canvas:{ 2d:{ /*...*/ } } } } };

Ух, это уныло.

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

Как решить эту проблему изящнее? В книге JavaScript Patterns Стоян Стефанов предлагает весьма грамотный подход для организации вложенных пространств имён при существующей глобальной переменной. Метод принимает однострочный аргумент для гнезда, парсит его и автоматически заполняет Namespace вместе с требуемыми объектами.

Предлагаемый им метод я изменил до кондиции общей функции, которую можно повторно использовать при работе с несколькими пространствами имён:

// верхний уровень namespace объявлен как литерал-объект var myApp = myApp || {}; // функция для разборки строки Namespace и автоматической генерации вложенной иерархии function extend(ns, ns_string) { var parts = ns_string.split("."), parent = ns, pl, i; if (parts == "myApp") { parts = parts.slice(1); } pl = parts.length; for (i = 0; i < pl; i++) { //create a property if it doesnt exist if (typeof parent] == "undefined") { parent] = {}; } parent = parent]; } return parent; } // пример применения: расширение myApp с глубокой вложенностью namespace var mod = extend(myApp, "myApp.modules.module2"); // на выходе: корректно вложенная в объект иерархия console.log(mod); // мы можем проверить это используя экземпляр объекта за пределами myApp namesapce, // как клон, включающий в себя расширения console.log(mod == myApp.modules.module2); //true // следом, более простая демонстрация объявления с использованием extend extend(myApp, "moduleA.moduleB.moduleC.moduleD"); extend(myApp, "longer.version.looks.like.this"); console.log(myApp);

Вот что мы увидим в web-инспекторе:

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

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

Шаблон зависимых объявлений

В этом разделе попробуем взглянуть на несколько "раздутый" вариант предыдущего паттерна, который вы наверняка привыкли видеть в некоторых приложениях. Все мы знаем, что локальные ссылки на объекты могут порядочно ускорить время поиска. Давайте попробуем применить это к Namespace:

// общий подход к доступу пространств имён

MyApp.utilities.math.fibonacci(25); myApp.utilities.math.sin(56); myApp.utilities.drawing.plot(98,50,60);

// объявление локальных(кешируемых) ссылок

Var utils = myApp.utilities, maths = utils.math, drawing = utils.drawing;

// более короткий путь к namespace

Maths.fibonacci(25); maths.sin(56); drawing.plot(98, 50,60);

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

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

Стоян рекомендует объявлять локальные Namespace требуемых функций или модулей в верху иерархии приложения(с использованием шаблона единичной переменной) и называет такую модель «шаблон зависимых объявлений». Одним из достоинств данного метода является снижение числа возможных зависимостей и поиска решений по их устранению, если у вас расширяемая архитектура, которая загружает модули динамически, по мере их надобности.

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

Глубинные расширения объектов

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

Это как раз то, что стало легко выполнимо с помощью JavaScript фреймворков(например, метод jQuery $.extend). Тем не менее, если вам нужно повторить это в стиле «vanila js», следующие инструкции могут быть полезны.

// extend.js // written by andrew dupont, optimized by addy osmani

Function extend(destination, source) { var toString = Object.prototype.toString, objTest = toString.call({}); for (var property in source) { if (source && objTest == toString.call(source)) { destination = destination || {}; extend(destination, source); } else { destination = source; } } return destination; }; console.group("objExtend namespacing tests");

// определение top-level namespace

Var myNS = myNS || {};

// 1. расширение namespace объектом "utils"

Extend(myNS, { utils:{ } }); console.log("test 1", myNS);

// myNS.utils теперь существует // 2. расширение объекта несколькими значениями (namespace.hello.world.wave)

Extend(myNS, { hello:{ world:{ wave:{ test: function(){ /*...*/ } } } } });

// проверка ссылок на работоспособность

MyNS.hello.test1 = "this is a test"; myNS.hello.world.test2 = "this is another test"; console.log("test 2", myNS);

// 3. а что если myNS уже имеет namespace (например, "library")?

MyNS.library = { foo:function(){} }; extend(myNS, { library:{ bar:function(){ /*...*/ } } });

// проверим работает ли extend так как ожидалось myNS сейчас содержит library.foo и library.bar

Console.log("test 3", myNS);

// 4. а что, если захочется иметь более простой доступ к отдельному пространству имён без необходимости каждый раз указывать полный путь

Var shorterAccess1 = myNS.hello.world; shorterAccess1.test3 = "hello again"; console.log("test 4", myNS);

// победа, myApp.hello.world.test3 теперь доступно через "hello again"

Console.groupEnd();

Если же ваше приложение использует jQuery, то расширения namespace объекта можно достичь с помощью $.extend.

// top-level namespace

Var myApp = myApp || {};

// явное расширение namespace вглубь

MyApp.library = { foo:function(){ /*..*/} };

// расширим наш namespace другим объектом, но // чтобы было интереснее сделаем вложенное пространство имён функцией // $.extend(deep, target, object1, object2)

$.extend(true, myApp, { library:{ bar:function(){ /*..*/ } } }); console.log("test", myApp);

// myApp теперь содержит методы library.foo() и library.bar() // ни одно из namespace не подверглось корреляции, на что мы и рассчитывали.

Для большей наглядности посмотрите другие примеры $.extend и поэкспериментируйте с ними.

Основы построения пространств имён

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

  1. Одиночные глобальные переменные (Single global variables)
  2. Объектно-буквенное обозначение (Object literal или литерал-объект)
  3. Глубинное именование (Nested namespacing)
  4. Объявление самовызывающейся функции (Immediately-invoked Function Expressions)
  5. Внедрение в Namespace (Namespace injection)

Одиночные глобальные переменные

Одним из популярных шаблонов для организации Namespace в JavaScript является выбор одной глобальной переменной в качестве основного объекта для ссылки. Каркас такого паттерна возвращает объект с функцией и свойствами:

Var myApplication = (function(){ function(){ /*...*/ }, return{ /*...*/ } })();

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

Возможный вариант решения упоминался Петер Мичаукс. Решение заключается в довольно простой идее использовать префиксы для имён. Сначала вы именуете своё приложение, а затем объекты, методы, функции или свойства, например:

Var myApplication_propertyA = {}; var myApplication_propertyB = {}; funcion myApplication_myMethod(){ /*..*/ }

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

Если вас интересует мнение Петера в отношении этого вопроса, можете прочитать статью на эту тему.

Object literal notation

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

Var myApplication = { getInfo:function(){ /**/ }, // мы также можем организовать литерал-объект для надёжности models: {}, views: { pages: {} }, collections: {} };

Естественно, мы можем определить какие-то свойства для Namespace напрямую.

MyApplication.foo = function(){ return "bar"; } myApplication.utils = { toString:function(){ /*..*/ }, export: function(){ /*..*/ } }

Шаблон «Object literal notation» имеет достоинства не загрязнять глобальное пространство имён, помогать в логической организации кода и параметров. Это очень полезно, если вы хотите создавать легко читаемые структуры кода, которые могут быть в последующем расширяться вглубь. В отличие от обычных глобальных переменных объектно-буквенный namespace также учитывают тесты на существование переменной, поэтому шансы на путаницу значительно сокращаются.

Следующий фрагмент кода демонстрирует способы, которыми вы можете проверить наличие переменной(namespace) в глобальном пространстве, перед тем, как определить её. Вы будете чаще встречать вариант 1, однако, варианты 3 и 5 являются более тщательными, в то время, как вариант 4 считается лучшей практикой.

// Плохо: // этот вариант не предусматривает проверки глобального namespace на наличие "myApplication".

Var myApplication = {};

/* Следующие методы предназначены для проверки на существование переменной.

Если переменная существует, используется экземпляр. В противном случае создаётся новый литерал-объект для myApplication

Var myApplication = myApplication || {};

If(!MyApplication) MyApplication = {};

Var myApplication = myApplication = myApplication || {}

MyApplication || (myApplication = {});

Var myApplication = myApplication === undefined ? {} : myApplication;

Существует огромное количество мнений, как именно использовать литерал-объект для проектирования структуры приложений. Для организации вложенных API применимо к отдельно взятым модулям вы можете поискать свой способ возвращать интерфейс для удобства других разработчиков. Это вариация модульного шаблона на основе паттерна «IIFE» с применением интерфейса на базе литреал-объекта.

Var namespace = (function () { // объявление в локальной области var privateMethod1 = function () { /* ... */ } var privateMethod2 = function () { /* ... */ } var privateProperty1 = "foobar"; return { // здесь мы возвращаем литерал-объект, который может иметь // столько уровней в глубину, сколько вы пожелаете. // но, как упоминалось выше, этот вариант лучше всего подходит // небольших приложений с ограниченной областью видимости publicMethod1: privateMethod1, //вложенные пространства имён с публичными свойствами properties:{ publicProperty1: privateProperty1 }, //ещё одно адресное пространство utils:{ publicMethod2: privateMethod2 } ... } })();

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

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

Var myConfig = { language: "english", defaults: { enableGeolocation: true, enableSharing: false, maxPhotos: 20 }, theme: { skin: "a", toolbars: { index: "ui-navigation-toolbar", pages: "ui-custom-toolbar" } } }

Стоит обратить внимание на то, что существуют незначительные отличия в синтаксисе при определении литерал-объекта и стандартного набора данных в формате JSON. Если вы по какой либо причине склоняетесь к JSON для организации параметров(например, для более удобного обмена с back-end частью приложения), не стесняйтесь. Более подробно о литерал-объекте можно узнать в статье Реббека Мерфи.

Вложенные именования

«Nested namespace» является расширением шаблона литерал-объекта. Это ещё одна распространённая модель обеспечивающая снижение рисков путаницы также и в локальной области видимости.

Возможно вы уже где-то видели это?

YAHOO.util.Dom.getElementsByClassName("test");

Фрэймворк Yahoo YUI использует именование вложенное именование объектов на регулярной основе. Кроме того, мы в AOL используем этот паттерн для большого числа наших приложений. Пример такой организации может выглядеть следующим образом:

Var myApp = myApp || {};

// проверки на существование в т.ч. при определении потомков

MyApp.routers = myApp.routers || {}; myApp.model = myApp.model || {}; myApp.model.special = myApp.model.special || {};

// вложенное пространство имён может быть таким сложным, как потребуется // myApp.utilities.charting.html5.plotGraph(/*..*/); // myApp.modules.financePlanner.getSummary(); // myApp.services.social.facebook.realtimeStream.getLatest();

Для определения новых вложенных namespace вы можете обратиться к свойствам по индексу.

MyApp["routers"] = myApp["routers"] || {}; myApp["models"] = myApp["models"] || {}; myApp["controllers"] = myApp["controllers"] || {};

Оба варианта вполне читабельны, организованы и позволяют относительно безопасно спроектировать адресное пространство, подобно тому, как это сделано в других языках. Единственный момент, который нужно учесть: это заставит логику движка JavaScript вашего браузера сначала рассчитать объект myApp, а затем «копать вглубь» вплоть до функции, которую вы желаете использовать.

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

Объявление самовызывающейся функции (IIFE)

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

Простейшая реализация IIFE может выглядеть следующим образом:

// immediately-invoked function expression (анонимная)

(function(){ /*...*/})();

// immediately-invoked function expression (именованная)

(function foobar(){ /*..*/}());

// техника для реализации самовызова функции из внутреннего контекста

Function foobar(){ foobar(); }

Чуть более развёрнутый вариант будет таким:

Var namespace = namespace || {};

// здесь объект namespace представлен, как параметр функции

// с объявлением публичных методов и их свойств

(function(o){ o.foo = "foo"; o.bar = function(){ return "bar"; }; })(namespace); console.log(namespace);

Для большей наглядности этот пример может быть развёрнут до уровня видимости различных уровней приватности(public/private функций и переменных) и удобного обозрения namespace-расширений. Взглянем на следующий пример:

// 1. namespace может быть изменено локально и не может быть перезаписано вне своего контекста

// 2.значение undefined гарантирует, что параметры действительно не определены.

// это обезопасит приложение от подмены входных данных(mutable pre-ES5).

;(function (namespace, undefined) { // private properties var foo = "foo", bar = "bar"; // public methods and properties namespace.foobar = "foobar"; namespace.sayHello = function () { speak("hello world"); }; // private method function speak(msg) { console.log("You said: " + msg); }; // проверим есть ли "namespace" в глобальном контексте; если нет, то объявим window.namespace литерал-объектом. }(window.namespace = window.namespace || {});

// проверим наши свойства

Console.log(namespace.foobar); // foobar namescpace.sayHello(); // hello world

// попробуем определить новые

Namespace.foobar2 = "foobar"; console.log(namespace.foobar2);

Расширения(или extensions) несомненно являются ключом к любой масштабируемой модели namespace и IIFE может быть весьма эффективно использованы для этих целей. В следующем примере, "namespace" вновь передаётся в качестве аргумента в анонимную функцию и далее расширяется.

// давайте расширим namespace новой функциональностью

(function(namespace, undefined){ // public method namespace.sayGoodbye = function(){ console.log(namespace.foo); console.log(namespace.bar); speak("goodbye"); } }(window.namespace = window.namespace || {});

// в результате

Namespace.sayGoodbye(); //goodbye

Если вам интересно чуточку больше, то вы можете почитать пару заметок о анонимных функциях Бена и о namespace-паттернах в C# Элии Мэнора.

Внедрение в namespace (Namespace injection)

«Namespace injection» является ещё одним из вариантов IIFE, когда мы вводим методы или свойства в определённое пространство имён из функции обёртки, используя это в качестве мини прокси-сервера. Преимущество данного паттерна заключается в лёгкости применения определённого поведения к нескольким объектам или namespace. Также это может быть полезно при определении базовых методов, которые могут быть в последующем использованы(getters и setters).

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

Следующий пример показывает, как мы можем использовать Namespace injection для заполнения параллельно двух namespace. Одно из них utils, а второе его часть подключаемая динамически в пространстве имён tools.

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

// объявим namespace, котрый используем позже

Var ns = ns || {}, ns2 = ns2 || {};

// формирователь

Var creator = function(val){ var val = val || 0; this.next = function(){ return val++ }; this.reset = function(){ val = 0; } } creator.call(ns);

// ns.next, ns.reset теперь созадны

Creator.call(ns2, 5000);

// ns2 содержит некий набор методов, но с перезаписанным значением 5000 Как отмечалось выше, данный шаблон полезен для назначения базового функционала нескольких модулей или namespace. Однако, я предлагаю вам использовать его только тогда, когда использовать замыкания для прямого доступа не имеет смысла.

В заключение

Из всех представленных выше шаблонов организации namespace, которые мне доводилось использовать в большинстве сложных приложений, лидируют паттерны с глубинным расширением и литрал-объект.

IIFEs и «single global variable» могут неплохо работать как для построения малых и средних приложений, так и для больших объемов кода с глубокой вложенностью. Данная модель достигает своих целей читабельности и масшабируемости достаточно хорошо.

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

Шаблоны проектирования помогут сделать разработку комфортной и понятной. Давайте начнем изучать паттерны JavaScript вместе.

Модуль и Синглтон

Автор на примерах показывает паттерны JavaScript – Module и Singleton. Очень полезно инкапсулировать состояние, структуру и любую приватную информацию объекта. Для решения этой задачи используют шаблон “Модуль”, который реализует все задуманное через замыкания. Другим полезным и часто используемым шаблоном, является “Синглтон”. Этот шаблон предоставляет одну глобальную точку доступа к экземпляру и гарантирует наличие только одного такого объекта в приложении.

Observer

В этой лекции рассматривается самый простой для понимания паттерн – наблюдатель. Он основан на зависимости один ко многим: при изменении состояния одного объекта (observer), все зависимые объекты (observable) оповещаются об этом. Самое главное в общении этих двух объектов, что не нужна прямая связь между объектом и субъектом. Это позволяет добиться модульности клиентской части кода.

Strategy

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

Decorator или обертка

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

А теперь начнем изучать паттерны JavaScript вместе с углубленными знаниями проектирования.

Контекст и шаблоны вызова функций, call/apply

Здесь автор начинает серию уроков по расширенному JS, а точнее по углубленной базе для новичков. Рассмотрению подвергается поведение this в зависимости от способа вызова функции. Также объясняется на примерах ценность паттерна apply, относительно привязки this к obj и паттерна call, который получает не параметры, а список аргументов при вызове.

Атрибуты свойств, get/set

Для более гибкого управления абсолютно всеми свойствами объекта и даже видимостью в цикле, изменяемостью и прочими, есть специальные возможности. Define property – метод, позволяющий настроить деликатный доступ к свойствам объекта, используя флаги configurable, enumerable и т. д.

Асинхронность, стек вызовов, промисы

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

  • Сергей Савенков

    какой то “куцый” обзор… как будто спешили куда то