Namespace w JavaScript

Bywa, że ciężko zorganizować nam kod, mając do czynienia z projektem składającym się z kilkudziesięciu plików. Kolejne moduły przypisujemy do różnych miejsc, często do globalnego obiektu, w którym znajduje się nasza aplikacja, czasem tworzymy po prostu kolejne globalne zmienne. Jak można ugryźć ten temat?

Załóżmy, że mamy folder App, w którym mieści się nasza aplikacja, a w nim kolejne dwa katalogi: views i models. Następnie uznajmy, że w widokach (views) i modelach (models) mamy po jednym pliku. Niech App/views/add.js wygląda tak:

App.views.add = function() {
	return {};
};

Z kolei App/models/add.js może wyglądać tak:

App.models.add = function() {
	return {};
};

Widać więc jak na dłoni, że używamy jednego, globalnego obiektu o nazwie App, który zawiera wszystkie moduły naszej aplikacji. Oczywiście posiadając same pliki w views i models mielibyśmy błąd – nigdzie nie stworzyliśmy przecież obiektów App i App.views oraz App.models. Zróbmy to w App/init.js:

(function() {
	this.App = {
		views: {},
		models: {}
	};
})();

Do this, który reprezentuje globalny obiekt w ECMAScript 3, jeśli wywołamy anonimową funkcję, przypisujemy obiekt App, a do niego views oraz models. Zupełnie podobnie, gdybyśmy zrobili tak:

window.App = {};
window.App.views = {};
window.App.models = {};

Możliwe jest więc używanie konstrukcji, jakie mamy w naszych plikach – w końcu najpierw zainicjowaliśmy te obiekty, a potem możemy coś do nich przypisać.

Używając podobnego patternu napotykamy potencjalne zagrożenia, a mianowicie nie możemy być pewni, że rzeczywiście App.views i App.models istnieją – ok, stworzyliśmy je poprzez dodanie App/init.js, ale istnieją bardziej uniwersalne rozwiązania. Mam tu na myśli funkcję, która na podstawie stringa, będącym namespace’m – np. App.views.articles.edit – stworzyłaby takie drzewo obiektów:

App = {
	views: {
		articles: {
			edit: {}
		}
	}
};

I to niezależnie od tego, czy App został już zdefiniowany w globalnej przestrzeni czy nie. Do tego celu możemy napisać globalną funkcję namespace:

(function() {
	var global = this;
	global.namespace = function(ns) {
	};
})();

Na razie nic ona sobą nie reprezentuje. Możemy jednak omówić, jak będziemy ją wywoływać. Otóż przypominając sobie plik App/views/add.js z nową funkcją będzie on wyglądał tak:

namespace('App.views');

(function() {
	App.views.add = function() {
		return {};
	};
})();

Dla App/models/add.js będzie wyglądać to tak:

namespace('App.models');

(function() {
	App.models.add = function() {
		return {};
	};
})();

Widać jak na dłoni, że namespace przyjmuje stringa i potem rozdziela go po kropce, najpierw przypisując do window App, potem do window.App views lub models.

Zaczynamy!

(function() {
	var global = this;
	global.namespace = function(ns) {
		var parent;
		if (typeof ns === "string") {
		}
	};
})();

Nic trudnego, na tym etapie sprawdzamy tylko, czy zmienna ns jest stringiem, inaczej bez sensu byłoby robić jakiekolwiek operacje, w tym dzielenie stringa na wyrazy, które oddziela kropka.

(function() {
	var global = this;
	global.namespace = function(ns) {
		var parent;
		if (typeof ns === "string") {
			parent = global;
			ns = ns.split('.');
			// ...
		}
	};
})();

Dwie najważniejsze rzeczy to przypisanie do zmiennej parent globalnego obiektu, od którego zaczniemy naszą wędrówkę z przypisywaniem kolejnych obiektów do siebie. W końcu App musi być najpierw dodany do globalnej przestrzeni, od niego wszystkiego się zacznie. ns = ns.split(‘.’); to podzielenie stringa na wyrazy, które oddzielone są kropką. Teraz ns to tablica składająca się – w wypadku App/views/add.js z “App” i “views” – [“App”, “views”].

Pora przeiterować przez tę tablicę, po kolei dodając nowe obiekty App i views.

(function() {
	var global = this;
	global.namespace = function(ns) {
		var parent;
		if (typeof ns === "string") {
			parent = global;
			ns = ns.split('.');
			for (var i = 0, ilen = ns.length; i < ilen; i++) {
			// ...
			}
		}
	};
})();

Proste, prawda? Jedynie łatwy for - zabawa zacznie się wewnątrz pętli.

(function() {
	var global = this;
	global.namespace = function(ns) {
		var parent;
		if (typeof ns === "string") {
			parent = global;
			ns = ns.split('.');
			for (var i = 0, ilen = ns.length; i < ilen; i++) {
				parent[ns[i]] = parent = (typeof parent[ns[i]] === "object" ? parent[ns[i]] : {});		
			}
		}
	};
})();

Wyobraźmy sobie, że ns[i] to string "App". Na początku też parent to window/this. A więc do window['App'] przypisujemy

(parent[ns[i]])

albo

{}

Na początku nie ma window['App'] więc typeof parent[ns[i]] da undefined. W związku z tym, że już pierwszy warunek nie jest prawdziwy, przechodzimy dalej do pustego obiektu {} i to on zostanie przypisany do window['App'] oraz zmiennej parent.

Podczas następnej iteracji zmienną parent będzie obiekt:

App = {
};

Z kolei ns[i] da "views". Sprawdzenie będzie podobne. Jeśli jednak drugi raz mielibyśmy do czynienia z App.views, funkcja nie przechodziłaby w warunku dalej i przypisała do parent istniejący już obiekt.

W ten sposób wygląda podstawowa wersja funkcji namespace. Pozwala ona nam usystematyzować dodawanie plików i modułów do aplikacji. Jeśli używamy jakiegoś build systemu, może on sprawdzać, czy dany plik, pobierając jego pierwszą linijkę z definicją namespace'a, znajduje się w dobrym katalogu. W naszym wypadku na przykład czy add.js mieści się w App/views.

Komentarze

1

Bardzo fajny art, jak przeczytalem o tym patternie w ksiazce Stoyana to miałem taki okres w zyciu że pchałem to wszedzie gdzie popadnie :).

2

Damian, przygotowałeś serię tego typu artykułów czy piszesz je na bieżąco? [czysta ciekawość :)]

Poza tym: kawał świetnej roboty!

Marcin Maćkowiak
3

Na bieżąco, mam mnóstwo tego materiału ze szkoleń.

4

Fajne podejście i ładnie, maksymalnie prosto, wręcz łopatologiczne opisane – chcę więcej! :)

5

hmm… myślę, że teraz się już odchodzi od budowania namespace’ów na globalu, i nie szedłbym w tym kierunku. Mamy AMD, moduły CommonJS/NodeJS (które ja preferuje).. można pisać duże, złożone aplikacje nie dotykając globala w ogóle, i taki też będzie “oficjalny” standard, jak tylko moduły z ES6 wejdą w życie.

6

Mariusz, nadal warto znać takie techniki – być może przydadzą się przy developowaniu jakiegoś toola czy innego rozwiązania. AMD (require.js) też korzysta z globala siłą rzeczy, tylko dla toola. ;)

7

Wlasnie opisales jak dziala funkcja namespace w YUI :)
http://www.zachleat.com/web/yui-code-review-yahoonamespace/

gustaff_weldon
8

Krotkie pytanie – co sadzisz o Backbone.js?
Przegladam Twojego bloga raz na jakis czas, ale nie widzialem wzmianki o tym frameworku, wyszukiwarka tez nic nie zdradza.

Ostatnio wscieklem sie sam na siebie za brak porzadku w kodzie js, zaczalem uzywac, spelnia swoja funkcje bardzo dobrze poki co.

klon
9

Przydałby się rss do bloga jakiś :) bo tak mogę nie trafić z powrotem… a szkoda by było…

yoltz
10
11

Fajny artykuł, ale dobrze byłoby, gdyby przykłady nie polegały na fakcie, że this zwraca w funkcji globalny obiekt. Dzisiaj się to za problem i dlatego taka konstrukcja nie zadziała w strict mode.

12

> Dzisiaj się to za problem
->
Dzisiaj uważa się to za problem

Dodaj komentarz

Dozwolone tagi: <blockquote>, <code>, <strong>