Pattern-ul Modul
Pattern-ul "Modul" se bazează pe închideri (closures) și constă din două componente: o funcție externă, care definește mediul lexical, și un set returnat de funcții interne, care au acces la acest mediu.
Să definim cel mai simplu modul:
const printer = (function(){
const messages = {greeting: "hello"};
return {
print: function(){
console.log(messages.greeting);
}
}
})();
printer.print(); // hello
Aici este definită constanta printer, care reprezintă rezultatul unei funcții anonime. În interiorul acestei funcții este definit un obiect messages cu unele date.
Funcția anonimă returnează un obiect, care definește funcția print. Obiectul returnat definește API-ul public, prin intermediul căruia putem accesa datele definite în interiorul modulului.
return {
print: function(){
console.log(messages.greeting);
}
}
Această construcție permite să izolăm un anumit set de date în cadrul funcției-modul și să mediere accesul la acestea printr-un API definit - funcțiile interne returnate.
Funcțiile returnate pot fi definite în alt loc, nu doar în interiorul funcției anonime:
const printer = (function(){
const messages = {greeting: "Hello FDC.COM"};
const printMessage = function(){
console.log(messages.greeting);
};
return {
print: printMessage // funcția printMessage este definită în afara obiectului
}
})();
printer.print(); // Hello FDC.COM
Dacă există posibilitatea ca modulul să fie deja definit undeva mai devreme în cod sau în fișiere externe incluse, atunci putem folosi următoarea construcție:
var printer = printer || (function(){
const messages = {greeting: "Hello World"};
return {
print: function(){
console.log(messages.greeting);
}
}
})();
printer.print(); // Hello World
Definirea var printer = printer || (function(){ ... }) atribuie variabilei valoarea unui anumit obiect printer, dacă există, sau atribuie rezultatul apelării funcției anonime IIFE. Dar cu această definiție nu putem folosi cuvintele cheie let sau const pentru a defini obiectul. Prin urmare, în acest caz, obiectul este definit cu ajutorul var.
Să examinăm un exemplu puțin mai complex:
const calculator = (function(){
const data = { number: 0};
return {
sum: function(n){
data.number += n;
},
subtract: function(n){
data.number -= n;
},
print: function(){
console.log("Result: ", data.number);
}
}
})();
calculator.sum(10);
calculator.sum(3);
calculator.display(); // Result: 13
calculator.subtract(4);
calculator.display(); // Result: 9
Acest modul reprezintă un calculator primitiv, care efectuează trei operații: adunare, scădere și afișarea rezultatului.
Toate datele sunt încapsulate în obiectul data, care stochează rezultatul operației. Toate operațiile sunt reprezentate de trei funcții returnate: sum, subtract și print. Prin aceste funcții putem controla rezultatul calculatorului din exterior.
Introducerea modulelor externe
Prin parametrii funcțiilor IIFE în module se pot introduce unele date, de exemplu, alte module:
var moduleA = moduleA || (function () {
const message = "Hello World";
return {
printMessage: function() {
console.log(message);
}
}
})();
var moduleB = moduleB || (function (moduleA) {
return {
print: function() {
moduleA.printMessage();
}
}
})(moduleA);
moduleB.print();
În acest caz, modulul moduleB așteaptă să primească modulul moduleA. În interiorul modulului moduleB se face referire la funcția moduleA.printMessage. Similar, se pot transmite și seturi de module.
Extinderea unui modul
Când lucrați cu module, poate apărea necesitatea de a extinde funcționalitatea acestuia - adăugând funcții sau variabile noi. În acest caz, putem folosi o serie de tehnici.
// prima tehnică
var localeModule = localeModule || (function(locale){
const enMessage = "Hello World";
locale.printEn = function(){console.log(enMessage);};
return locale;
})(localeModule || {});
// a doua tehnică
var localeModule = (function(locale){
const roMessage = "Salut lume";
locale.printRo = function(){console.log(roMessage);};
return locale;
})(localeModule);
localeModule.printEn(); // Hello World
localeModule.printRo(); // Salut lume
Pentru a extinde un modul, se pot aplica două tehnici. Prima tehnică presupune că, dacă modulul nu a fost încă creat, atunci ca parametru se transmite un obiect gol:
var localeModule = localeModule || (function(locale){
const enMessage = "Hello World";
locale.printEn = function(){console.log(enMessage);};
return locale;
})(localeModule || {});
În acest caz, dacă modulul localeModule nu există încă, va fi creat un obiect nou, la care va fi adăugată funcția printEn pentru a afișa un anumit mesaj.
Avantajul acestei tehnici este că scripturile care fac parte din modul pot fi încărcate asincron. Nu contează care script este încărcat primul, deoarece, în caz de îndoială, modulul va fi creat din nou.
A doua tehnică presupune că modulul există deja:
var localeModule = (function(locale){
const roMessage = "Salut lume";
locale.printRo = function(){console.log(roMessage);};
return locale;
})(localeModule);
Aici suntem siguri că există deja obiectul localeModule, și de asemenea adăugăm o nouă funcție -printRo. În ambele cazuri, modulul returnează ca rezultat un obiect extins cu noua funcționalitate din parametru.
Totuși, indiferent de tipul de extindere a modulului aplicat, ambele au un dezavantaj comun: funcțiile definite într-un fișier de cod sursă pentru modul nu au acces la variabilele și constantele private definite într-un alt fișier de cod sursă pentru același modul. De exemplu, metoda printRo nu are acces la constanta enMessage.