Moduły

Jeżeli w naszych skryptach nadużywamy zmiennych globalnych, istnieje duże ryzyko, że prędzej przy później natrafimy na problemy. Wyobraź sobie, że dołączasz do strony dodatkowy plugin. Autor tego plugina używa zmiennych o takich samych nazwach jak w naszym skrypcie. Fiku miku - mamy konflikt. Na szczęście są sposoby by się przed tym zabezpieczyć.

Wyobraź sobie, że napisaliśmy kod jak poniżej:


const ourModule = {
    animals : ['dog', 'cat', 'bird'],

    addNewElement : function(newAnimal) {
        if (elementToAdd != '') {
            this.animals.push(elementToAdd);
        }
        this.displayList();
    },

    displayList : function() {
        const list = document.querySelector('#list');
        list.innerHTML = '';

        animals.forEach(function(el) {
            const element = document.createElement('li');
            element.innerHTML = el;
            list.appendChild(element);
        });
    },

    bindEvents : function() {
        document.querySelector('#number').addEventListener('click', addNewElement.bind(this));
        document.querySelector('#show').addEventListener('click', displayList.bind(this));
    }

    init : function() {
        bindEvents();
        displayList();
    }
}

Problem z powyższym kodem jest taki, że mimo ładnego zapisu nasz kod udostępnia innym za dużo zmiennych i metod. Nic nie stoi na przeszkodzie, by inny programista nadpisał nam tablicę animals zamieniając ją w numer:


ourModule.animals = 123;

Od tej pory nasz kod przestanie działać, bo przecież metoda displayList zakłada, że robi pętlę po tablicy zwierząt, a nie po numerze.

Wstępne zabezpieczenie kodu

Aby zabezpieczyć nasz kod przed innymi skryptami, zamkniemy go w zamkniętym środowisku. Aby to osiągnąć, skorzystamy z samo wywołującej się funkcji:


(function() {
    console.log('Jakiś tekst'); //wywoła się od razu
})();

Zamieńmy nasz poprzedni kod na kod z wykorzystaniem takiej funkcji:


(function() {
    const myModule = {
        animals : ['dog', 'cat', 'bird'],

        addNewElement : function(newAnimal) {
            if (elementToAdd != '') {
                this.animals.push(elementToAdd);
            }
            this.displayList();
        },

        displayList : function() {
            const list = document.querySelector('#list');
            list.innerHTML = '';

            animals.forEach(function(el) {
                const element = document.createElement('li');
                element.innerHTML = el;
                list.appendChild(element);
            });
        },

        bindEvents : function() {
            document.querySelector('#number').addEventListener('click', addNewElement.bind(this));
            document.querySelector('#show').addEventListener('click', displayList.bind(this));
        }

        init : function() {
            bindEvents();
            displayList();
        }
    }

    myModule.init();
})();

Powyższy kod jest już zabezpieczony przed zewnętrznym środowiskiem, ale wciąż ma braki. Jego głównym minusem jest to, że z zewnątrz nie mamy dostępu do żadnej jego części w tym poprzednio dostępnych funkcji displayList, addNewElement i init. Tworząc prawdziwe API, częstokroć chcielibyśmy na zewnątrz wystawiać konkretne metody i właściwości, a ukrywać tylko te metody, których inni nie powinni ruszać.

Udostępnianie zmiennych i metod

Aby zrealizować to zadanie, skorzystamy z wzorca modułu, który pozwala udostępniać na zewnątrz tylko wybrane przez nas rzeczy. Wzorzec ten ma postać:


const ourModule = (function() {
    const hiddenVar = 'tekst';
    const visibleVar = 'tekst2';

    const hiddenMethod = function() {
        console.log(hiddenVar);
    };

    const print = function() {
        console.log(visibleVar);
    };

    //wszystko co zwracamy staje się dostępne na zewnątrz, cała reszta będzie ukryta dla zewnętrznego środowiska
    return {
        print : print,
        ourVar : visibleVar
    }
})();

Wszystkie właściwości i metody, które zwrócimy w instrukcji return automatycznie staną się publiczne, czyli będzie je można wykorzystywać w zewnętrznym środowisku. Cała reszta metod i właściwości staje się prywatna, niedostępna na zewnątrz.

Nasz poprzedni moduł po zamianie na ten wzorzec będzie miał postać:


const ourModule = (function() {
    const animals = ['dog', 'cat', 'bird'],

    const addNewElement = function(elementToAdd) {
        if (elementToAdd != '') {
            this.animals.push(elementToAdd);
        }
        this.displayList();
    };

    const displayList = function() {
        const list = document.querySelector('#list');
        list.innerHTML = '';

        animals.forEach(function(el) {
            const element = document.createElement('li');
            element.innerHTML = el;
            list.appendChild(element);
        });
    };

    const bindEvents = function() {
        document.querySelector('#number').addEventListener('click', addNewElement.bind(this));
        document.querySelector('#show').addEventListener('click', displayList.bind(this));
    };

    const init = function() {
        bindEvents();
        displayList();
    };

    return {
        init : init,
        addNewElement : addNewElement,
        displayList : displayList
    }
})();

Jak widzisz, nasz moduł nie udostępnia ani bezpośredniego dostępu do tablicy animals, ani do metody bindEvents. Sprawdźmy:


ourModule.animals = 123; //błąd! - niby możemy ustawić taką zmienną, ale to nie nadpisze tablicy animals którą mamy w module
ourModule.bindEvents(); //błąd! - nie mamy dostępu do tej metody

ourModule.displayList(); //wypisze oryginalne [dog, cat, bird]
ourModule.addNewElement('Świnka');
ourModule.displayList(); //wypisze [dog, cat, bird, świnka]

Pokazane powyżej rozwiązanie to jedno z licznych. Jeżeli zainteresuje cię ten temat i czujesz się na siłach polecam książkę online pod adresem https://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript.

Do tematu modułów wrócimy jeszcze w dziale o modułach w ES6.