Moduły

Pisząc kod tak jak do tej pory, daleko nie zajedziemy. Jeżeli założylibyśmy, że na naszej stronie zawsze będziemy używać tylko i wyłączenie naszego skryptu to problemu raczej nie będzie. Bylibyśmy jednak naiwni, gdybyśmy tak zakładali.

Jeżeli do naszej strony chcielibyśmy dodać inny skrypt (np. plugin), istnieje bardzo duże ryzyko, że autor tego plugina używa zmiennych o takich samych nazwach jak w naszym skrypcie. Cyk, pryk - mamy konflikt. Na szczęście są sposoby by się przed tym zabezpieczyć.

Organizujemy kod

Szybka powtórka. W javascript istnieje wzorzec funkcji, która sama się wywołuje: Powiedzmy, że mamy kod w postaci:


var animals = ['dog', 'cat', 'bird'];
        
document.querySelector('#number').addEventListener('click', function(e) {
    var newAnimal = document.querySelector('#animal').value();
    if (typeof newAnimal !== 'string') {
        if (newAnimal != '') {
            animals.push(newAnimal);
        }
    }
});
        
document.querySelector('#show').addEventListener('click', function(e) {
    var list = document.querySelector('#list');
    if (list == null) {
        list = document.createElement('ul');
        list.id = list;
        document.querySelector('body').appendChild(list);
    }
    list.innerHTML = '';
    _animals.forEach(function(el, i) {
        var element = document.createElement('li');
        element.innerHTML = el;
        list.appendChild(element);
    });
});

W tej postaci nasz kod jest zwyczajnie kiepski. Zmienne globalne animals, age można nieumyślnie nadpisać inną zmienną o tej samej nazwie. Dodatkowo kod jest średnio czytelny i mało użyteczny, bo dodawanie nowych wartości i wyświetlanie ich jest wrzucone bezpośrednio w event przycisków.

Zacznijmy więc porządkowanie naszego kodu poprzez zamianę go na prosty moduł, który jest obiektem:


var myModule = {
    animals : ['dog', 'cat', 'bird'],
    
    addNewElement : function(newAnimal) {
        var elementToAdd;

        if (typeof newAnimal !== 'string') {
            elementToAdd = document.querySelector('#animal').value();
        } else {
            elementToAdd = newAnimal;
        }

        if (elementToAdd != '') {
        this.animals.push(elementToAdd);
        }
    },
        
    displayList : function() {
        var list = document.querySelector('#list');
        if (list == null) {
            list = document.createElement('ul');
            list.id = list;
            document.querySelector('body').appendChild(list);
        }
        list.innerHTML = '';
        animals.forEach(function(el, i) {
            var 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();
    }
}

Nie tylko pozbyliśmy się globalnych zmiennych, ale nasz kod stał się o wiele bardziej czytelny. Dzięki powyższemu zapisowi od razu wiadomo co do czego służy.
Od tej pory możemy wywoływać jego nie tylko poprzez klikanie na przyciski, ale także za pomocą oddzielnych metod:


myModule.addNewElement('Karol');
myModule.displayList();
myModule.init();

Problem z powyższym kodem jest taki, że mimo ładnego zapisu nasz kod udostępnia innym za dużo zmiennych.
Powiedzmy, że złośliwy programista gdzieś w swoim kodzie ręcznie zmodyfikuje nasza tablicę zwierząt:


animals = 123;

Od tej pory nasz kod przestanie działać, bo przecież displayList 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 wzorca 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 tego wzorca:


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

        addNewElement : function(newAnimal) {
            var elementToAdd;

            if (typeof newAnimal !== 'string') {
                elementToAdd = document.querySelector('#animal').value();
            } else {
                elementToAdd = newAnimal;
            }

            if (elementToAdd != '') {
                animals.push(elementToAdd);
            }
        },

        displayList : function() {
            var list = document.querySelector('#list');
            if (list == null) {
                list = document.createElement('ul');
                list.id = list;
                document.querySelector('body').appendChild(list);
            }
            list.innerHTML = '';
            animals.forEach(function(el, i) {
                var 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 juz 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. 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ć:


var ourModule = (function() {
    var _hiddenVar = 'tekst';
    var _visibleVar = 'tekst2';

    var _hiddenMethod = function() {
        console.log(_var + _var2);
    }

    var _print = function() {
        console.log(_var);
    }

    //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ć:


var ourModule = (function() {
    var _animals = ['dog', 'cat', 'bird'];

    var _addNewElement = function(newAnimal) {
        var elementToAdd;

        if (typeof newAnimal !== 'string') {
            elementToAdd = document.querySelector('#animal').value();
        } else {
            elementToAdd = newAnimal;
        }

        if (elementToAdd != '') {
            _animals.push(elementToAdd);
        }
    };

    var _displayList = function() {
        var list = document.querySelector('#list');
        if (list == null) {
            list = document.createElement('ul');
            list.id = list;
            document.querySelector('body').appendChild(list);
        }
        list.innerHTML = '';
        animals.forEach(function(el, i) {
            var element = document.createElement('li');
            element.innerHTML = el;
            list.appendChild(element);
        });
    };

    var _bindEvents = function() {
        document.querySelector('#number').addEventListener('click', _addNewElement.bind(this));
        document.querySelector('#show').addEventListener('click', _displayList.bind(this));
    };

    var _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; //nie mamy dostępu do tej właściwości, więc nie możemy jej ręcznie nadpisać
ourModule.displayList(); //wypisze oryginalne [dog, cat, bird]
ourModule.addNewElement('Świnka');
ourModule.displayList(); //wypisze [dog, cat, bird, świnka]
ourModule._bindEvents(); //błąd! - nie mamy dostępu do tej metody