Strona główna - poczytaj o JavaScript, jQuery, CSS i HTML5

Front-Trends 2010

Singleton w JavaScript

Ostatnio, wspólnie z Markiem Stasikowskim, myśleliśmy intensywnie – w ramach zabawy z JavaScript – nad jak najlepszą implementacją wzorca projektowego Singleton. Wspólnymi siłami doszliśmy do dość fajnego kodu, który pozwolę sobie przedstawić i nieco omówić.

Przypomnijmy, o co tak naprawdę chodzi we wzorcu Singleton. Ujmując rzecz najprościej, Singleton ma za zadanie udostępniać tylko jedną instancję danej klasy w obrębie całej aplikacji. Używając tego wzorca mamy pewność, że zawsze operujemy na tym samym obiekcie, a pamięć nie zostanie zbyt zaśmiecona. Praktyczną implementację Singletonu można zastosować na przykład do klasy XHR (XMLHttpRequest) czy też rozpoznającej wersję przeglądarki internetowej.

Co jeszcze charakterystyczne dla tego wzorca, to prywatny konstruktor. W efekcie wywołanie new Singleton() jest niemożliwe. Jedyną instancję klasy Singleton pobieramy specjalnie do tego stworzoną metodą statyczną, np. Singleton.getInstance().

Spójrzmy więc na najprostszą implementację wzorca. Zakładamy, ze klasą typu Singleton będzie tak naprawdę klasa (funkcja) Constructor.

var Singleton = (function(){
	var Constructor = function() {
		this.data = 'some data';
}	

	return {
		getInstance: function (){
			return this.instance || (this.instance = new Constructor);
		}
	}
})();

Co tutaj najważniejsze, to natychmiastowe utworzenie obiektu z funkcji anonimowej1:

var Singleton = (function() { ... })();

Uniemożliwia to stworzenie czegoś na wzór new Singleton(), ponieważ do zmiennej Singleton zostanie przypisany rezultat natychmiastowego uruchomienia (ściślej mówiąc: skonkretyzowania obiektu) powyższej funkcji, a nie jej ciało.

Następnie tworzymy prywatny konstruktor:

var Constructor = function(){
	this.data = 'some data';
}

Ostatnie, co zostało do zrobienia, to zwrócenie jakieś konkretnej wartości wywoływanej anonimowej funkcji, tak by zmienna Singleton mogła używać metody getInstance (Singleton.getInstance()).

return {
	getInstance: function (){
		return this.instance || (this.instance = new Constructor);
	}
}

W ciele funkcji getInstance zwracamy instancję klasy Constructor (która, jak założyliśmy, ma być Singletonem), jeśli już jest stworzona lub tworzony ją, zapisując ją przy okazji jako składową, którą będziemy zwracać przy kolejnych wywołaniach getInstance. Wypada zrobić testy:

var foo = Singleton.getInstance();
var bar = Singleton.getInstance();

console.log(foo === bar); // true

foo.data = 'foobar';
console.log(bar.data); // foobar

Dzięki === dokonujemy sprawdzenia, czy wybrane obiektu są tego samego typu, a potem, czy zmiana dokonana na jednym obiekcie typu Singleton, powoduje zmiany w pozostałych obiektach. Działa!

Update

Podany w powyższej postaci Singleton można niestety nadpisać. Nie mamy więc pewności, że obiekt Singletonu będzie cały czas dostępny. Jednym z prostszych rozwiązań jest użycie const, która definiuje w JavaScript stałe:

const Singleton = (function(){
	var Constructor = function() {
		this.data = 'some data';
}	

	return {
		getInstance: function (){
			return this.instance || (this.instance = new Constructor);
		}
	}
})();

W ten sposób próba przypisania Singleton = “cos tam” jest niemożliwa. Niestety rozwiązanie to nie działa na IE, ale programujemy na poważne przeglądarki, czyż nie?

Warto też mieć świadomość, że w przypadku obecnej implementacji, zmienna instance jest publiczna (czyli możemy użyć Singleton.instance). Możemy ją ukryć w prosty sposób, używając closures:

const Singleton = (function(){
	var Constructor = function() {
		this.data = 'some data';
}, instance = null;	

	return {
		getInstance: function (){
			return instance || (instance = new Constructor);
		}
	}
})();

  1. (function() {})() można też zapisać jako new function() {}

Komentarze

1

Taka drobnostka, ale czy nie publiczną własność instance zamienić na zmienną lokalną?

W obecnej formie można wykonać sobie

console.log(Singleton.instance);
Singleton.instance = new Date;

a do takiej sytuacji chyba nie powinniśmy dopuszczać.

const Singleton = (function(){
   var Constructor = function(){
      this.data = 'some data';
   }, instance = null;
   return {
      getInstance:function(){
         return instance || (instance == new Constructor);
      }
   }
})();
Rafał
2

Przepraszam, ale zjadłem kilka słów podczas edycji. Pierwsze zdanie miało brzmieć: “Taka drobnostka, ale czy nie lepiej zamienić publiczną własność instance na zmienną lokalną?”

Rafał
3

@Rafał
this w this.instance nie będzie na pewno wskazywać na obiekt Singleton, więc ciężko będzie się dobrać do tej publicznej zmiennej przypadkiem, aczkolwiek Twoje rozwiązanie mi się podoba i można potraktować to jako dalsze udoskonalenie przedstawionej tu implementacji tego wzorca projektowego

4

@Marek: nie jestem pewien, czy zrozumiałem Twoją myśl. Ja miałem na myśli to, że wiersz this.instance = wartość przypisze nową, publiczną własność do obiektu, do którego referencję posiada zmienna/stała Singleton. Z tego powodu w Singleton.instance mamy dostęp do instancji obiektu utworzonego z konstruktora Constructor. Możemy sobie dowolnie podmieniać instancję naszego singletona na dowolny inny obiekt. Dla przykładu:

var foo = Singleton.getInstance();
console.log(foo.constructor); // function(){ this.data = 'some data'; }
console.log(Singleton.instance.constructor); // function(){ this.data = 'some data'; }
Singleton.instance = new Date;
var bar = Singleton.getInstance();
console.log(bar.constructor); // function Date(){ [native code] }

zamiana publicznej własności na zmienną lokalną nie pozwoli na taką operację. Pisałem, że to drobnostka, ale dla niektórych może być istotna.

Rafał
5

Rafał ma rację – tak to jest, kiedy pisze się coś późno w nocy, choć w sumie ta drobnostka wyszła na dobre – przy okazji można było pokazać, jak zrobić zmienną prywatną czy też jak działają closures.

Zaupdatowałem stosowną część tekstu, dziękuje za feedback!

Pozdrawiam

6

jo, racja, odszczekuję

7

świetna robota chłopaki!

8

> ale programujemy na poważne przeglądarki, czyż nie?

Ale programujemy tak, żeby nam programistom było łatwiej, czyż nie?

Myślałem, że jeśli 42% ludzi używa IE 6/7/8, to trzeba brać ich pod uwagę.

9

Jeśli ma nam być łatwiej, to IE lepiej ignorować. Osobiście coraz bardziej jestem zwolennikiem splashy informujących, że user używa gównianej przeglądarki (mowa o IE6, zastanawiałbyś się też nad IE7) i warto wybrać coś lepszego.

10

W wielu miejscach nie można zaktualizować przeglądarki – i co wtedy zrobisz? Ja, stawiając się w roli użytkownika strony, zwyzywał ich autorów.

I największy absurd – jakim kosztem. Zyskując gwarancję niezmienności (zakaz użycia “Singleton = cośtam”), odcinamy 40 ze 100 osób funkcjonalność strony.

11

I taki stan rzeczy jest zły. To tak, jakby dziwić się, że fiat 126p nie wyciąga 200 na godzinę, skoro BMW już tak. Jasne, w wielu korporacjach, bibliotekach i innych tego typu lokacjach nie ma sposobu na zmianę przeglądarki, aczkolwiek kto jak nie my, developerzy, ma obowiązek uświadomić ludzi, ze stosowanie IE niesie za sobą poważne braki. Z czasem, dobrze zakrojona kampania odciśnie piętno na większych strukturach – od nitki do kłębka! Już prawie 10 lat idziemy na kompromis i używamy hacków dla IE, a nie informujemy ludzi o wysiłku, który musieliśmy ponieść i o, przede wszystkim, wadach ich przeglądarek. Ja w swoich projektach olewam IE6, z czasem pewnie IE7 i staram się informować ludzi o tym.

Pozdrawiam

12

Targetem tej kampanii chyba jednak nie powinni byc laicy, ktorzy nic nie zmienia, po prostu nie wejda na strone napisana w sposob nieczytelny dla IE…

Dobrze zakrojona kampania powinna pokazywac korzysci np dla firm komputerowych skladajacych maszyny i instalujacych na nich soft dla zwyklych userow. Wtedy na systemie windows znajdzie sie rowniez np firefox czy inna opera ustawione jako przegladarka domyslna…

Inna sprawa to ta, ze np firma dla ktorej teraz pisze soft pracuje tylko na IE, poniewaz wczesniejsze wersje softu dzialaly tylko pod IE… Odgornie pojawil sie zakaz instalowania innych przegladarek, zeby aplikacja zawsze dzialala poprawnie. Tak wiec ilestamset osob ma TYLKO IE. I zadna kampania wycelowana w uzytkownika tego nie zmieni.

Mysle, ze warto jednak miec nastawienie frontem do usera, ktory najczesciej ma niewiele do powiedzenia…

Kania
13

jakiś czas temu czytałem artykuł o wzorcach tutaj:
http://www.klauskomenda.com/archives/2007/07/07/javascript-programming-patterns/

i co ciekawe częściej trafiam na określenie module niż singleton dla zapisu zbliżnego do powyższego. wynika to zapewne z innego założenia do zastosowania tego rozwiązania, bo tam zapis ten wykorzystuje się do budowania modułów, które nie będą ze sobą kolidowały.

Kamil
14

bardziej chodzi o “computer science in javascript” niż używanie tego w przeglądarkach
mam głęboko w butach, że nie działa w IE6, ot co

15

Targetem tej kampanii chyba jednak nie powinni byc laicy, ktorzy nic nie zmienia, po prostu nie wejda na strone napisana w sposob nieczytelny dla IE…

Dobrze zakrojona kampania powinna pokazywac korzysci np dla firm komputerowych skladajacych maszyny i instalujacych na nich soft dla zwyklych userow. Wtedy na systemie windows znajdzie sie rowniez np firefox czy inna opera ustawione jako przegladarka domyslna…

Inna sprawa to ta, ze np firma dla ktorej teraz pisze soft pracuje tylko na IE, poniewaz wczesniejsze wersje softu dzialaly tylko pod IE… Odgornie pojawil sie zakaz instalowania innych przegladarek, zeby aplikacja zawsze dzialala poprawnie. Tak wiec ilestamset osob ma TYLKO IE. I zadna kampania wycelowana w uzytkownika tego nie zmieni.

Mysle, ze warto jednak miec nastawienie frontem do usera, ktory najczesciej ma niewiele do powiedzenia…

DN
16

Możesz polecić jakąś skuteczną aplikację stworzoną do pisania JavaScript oraz debugowania tego języka?

Karol
17

@Karol: http://www.aptana.org/ imho najlepsza

shfx
18

Ekhm… A czy singletonem nie jest przypadkiem:

singleton={}?

Do tego występują “singletony prototypowe”:

A.prototype={
singleton=[] // lub {} lub new Konstruktor()
}

W każdej instancji A this.singleton będzie wskazywał na ten sam obiekt (musi być [] lub {} – zasadniczo wskaźniki).

JavaScript NIE JEST językiem klasowym i żadne wzorce projektowe dla języków obiektowo-klasowych nie powinny być tu używane. JS jest językiem funkcyjnym z zaimplementowaną prototypowością dla słowników (w JS nazywają się one obiektami). Nie utrudniajcie sobie życia i używajcie tego języka tak jak się powinno bo potem wychodzą takie kwiatki jak w tym poście. Poza tym każda książka traktuje o tym dlaczego w JS nie należy używać wzorca fabryki.

Ukrywanie parametrów funkcji się wykonuje za pomocą var a nie this i zmienna=null.

Aczkolwiek rozumiem, że autorzy są zafascynowani tym co można w JS napisać to jednak uważam, że należy używać jak najprostszych konstrukcji do osiągania celów. Coś na kształt wzorca fabryki jest czasem używany w JS po to aby dać userowi możliwość używania fn() i new fn() zarazem. Nie sądzę jednak aby komukolwiek poza programistami bibliotek takie konstrukcje były potrzebne. Dzisiaj wystarczy wziąć YUI, zapoznać się z konstrukcjami wymyślonymi przez Crockforda i tam zaimplementowanymi i używać NAPRAWDĘ przemyślanych, działających wszędzie konstrukcji. Tam nad każdą jedną funkcją programiści spędzają miesiące najpierw wymyślając, wdrażając i debugując. Nikt inny na świecie nie powinien odkrywać ameryki na nowo – i tak jest mała szansa, że wymyśli coś lepszego niż ci ludzie.

Mam nadzieję, że się sam gdzieś nie pomyliłem. Najgorsze jest to, że nie istnieje dobra książka do nauki tego języka. Najlepiej nauczyć się OCamla, a potem wszystkie jego konstrukcje stosować tutaj. Można też zaopatrzyć się w książkę Crockforda “JavaScript – the good parts”. Sam jej nie czytałem ale ten człowiek kiczu nie mógł wydać. Książkę Zacasa zdecydowanie odradzam.

Adrian

PS. IE poprawnie interpretuje ECMAScript. To, że inne przeglądarki obsługują nieustandaryzowane rozszerzenia nie oznacza, że IE jest gorszy.

19

Adrianie, zalozeniem tego artykulu bylo pokazanie, jak mozna zaimplementowac klasyczny wzorzec Singleton w JavaScript. Oczywiscie, ze pusty object literal mozna traktowac jako singleton, ale nie o tym mial byc ten artykul.

Moglbys rowniez, polecam na przyszlosc, powstrzymac sie od zbyt ofensywnego tonu, gdyz nic nowego – przynajmniej dla mnie – tutaj nie powiedziales, natomiast mam wrazenie, ze mialo zabrzmiec niczym prawda objawiona.

PS.

A propos implementacji JScript mentorze, polecam wyprobowac sobie w IE cos takiego:

if (false) {
function g() {};
};

typeof g;

Dodaj komentarz

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