Czy są jakieś zasady OO, które są praktycznie stosowane dla Javascript?

79

Javascript jest językiem obiektowym opartym na prototypach, ale może zostać oparty na klasach na różne sposoby, poprzez:

  • Samodzielne pisanie funkcji, które mają być używane jako klasy
  • Użyj fajnego systemu klas w ramach (takich jak mootools Class.Class )
  • Wygeneruj go z Coffeescript

Na początku pisałem kod oparty na klasach w JavaScript i mocno na nim polegałem. Ostatnio jednak używałem frameworków JavaScript i NodeJS , które odchodzą od tego pojęcia klas i polegają bardziej na dynamicznej naturze kodu, takiego jak:

  • Programowanie asynchroniczne, używanie i pisanie kodu używającego wywołań zwrotnych / zdarzeń
  • Ładowanie modułów za pomocą RequireJS (aby nie wyciekły do ​​globalnej przestrzeni nazw)
  • Koncepcje programowania funkcjonalnego, takie jak opisy list (mapa, filtr itp.)
  • Między innymi

Do tej pory zebrałem informacje, że większość zasad i wzorców OO, które przeczytałem (takich jak wzorce SOLID i GoF) zostały napisane dla klasowych języków OO, takich jak Smalltalk i C ++. Ale czy jakieś z nich dotyczą języka opartego na prototypach, takiego jak Javascript?

Czy są jakieś zasady lub wzorce specyficzne dla Javascript? Zasady unikania piekła zwrotnego , ewangelii zła lub innych anty-wzorów itp.

Łup
źródło

Odpowiedzi:

116

Po wielu edycjach ta odpowiedź stała się długa. Z góry przepraszam.

Przede wszystkim eval()nie zawsze jest zły i może przynieść korzyści w zakresie wydajności, na przykład w przypadku leniwej oceny. Leniwa ocena jest podobna do leniwego ładowania, ale zasadniczo przechowujesz swój kod w ciągach znaków, a następnie używasz evallub oceniasz new Functionkod. Jeśli użyjesz sztuczek, stanie się znacznie bardziej użyteczny niż zło, ale jeśli tego nie zrobisz, może to prowadzić do złych rzeczy. Możesz spojrzeć na mój system modułów, który używa tego wzorca: https://github.com/TheHydroImpulse/resolve.js . Resolve.js używa eval zamiast new Functionprzede wszystkim do modelowania CommonJS exportsi modulezmiennych dostępnych w każdym module, i new Functionzawija twój kod w funkcję anonimową, jednak ostatecznie pakuję każdy moduł w funkcję, robię to ręcznie w połączeniu z eval.

Więcej na ten temat przeczytasz w dwóch poniższych artykułach, później również w pierwszym.

Generatory harmonii

Teraz, gdy generatory wreszcie wylądowały w V8, a więc w Node.js, pod flagą ( --harmonylub --harmony-generators). To znacznie zmniejsza ilość piekła zwrotnego, które masz. To sprawia, że ​​pisanie kodu asynchronicznego jest naprawdę świetne.

Najlepszym sposobem na wykorzystanie generatorów jest zastosowanie biblioteki kontroli przepływu. Umożliwi to przepływ, aby kontynuować pracę w miarę generowania zysków w generatorach.

Podsumowanie / przegląd:

Jeśli nie znasz generatorów, są to praktyki polegające na wstrzymywaniu wykonywania funkcji specjalnych (zwanych generatorami). Ta praktyka nazywana jest plonem za pomocą yieldsłowa kluczowego.

Przykład:

function* someGenerator() {
  yield []; // Pause the function and pass an empty array.
}

Dlatego za każdym razem, gdy wywołasz tę funkcję po raz pierwszy, zwróci ona nową instancję generatora. Umożliwia to wywołanie next()tego obiektu w celu uruchomienia lub wznowienia działania generatora.

var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }

Dzwoniłbyś nextdo momentu donepowrotu true. Oznacza to, że generator całkowicie zakończył wykonywanie i nie ma już żadnych yieldinstrukcji.

Kontrola przepływu:

Jak widać, kontrolowanie generatorów nie jest automatyczne. Musisz ręcznie kontynuować każdy z nich. Dlatego używane są biblioteki kontroli przepływu, takie jak co .

Przykład:

var co = require('co');

co(function*() {
  yield query();
  yield query2();
  yield query3();
  render();
});

Pozwala to na możliwość napisania wszystkiego w Node (i przeglądarce z Regeneratorem Facebooka, który pobiera, jako dane wejściowe, kod źródłowy, który wykorzystuje generatory harmonii i dzieli w pełni kompatybilny kod ES5) w stylu synchronicznym.

Generatory są wciąż całkiem nowe, dlatego wymagają Node.js> = v11.2. Gdy piszę to, wersja 0..11.x jest nadal niestabilna, dlatego wiele natywnych modułów jest uszkodzonych i będzie działać do wersji v.12, gdzie natywny interfejs API się uspokoi.


Aby dodać do mojej oryginalnej odpowiedzi:

Ostatnio wolę bardziej funkcjonalny interfejs API w JavaScript. W razie potrzeby konwencja korzysta z funkcji OOP, ale upraszcza wszystko.

Weźmy na przykład system widoku (klient lub serwer).

view('home.welcome');

Jest o wiele łatwiejszy do odczytania lub śledzenia niż:

var views = {};
views['home.welcome'] = new View('home.welcome');

viewFunkcja po prostu sprawdza, czy ten sam widok już istnieje w lokalnej mapie. Jeśli widok nie istnieje, utworzy nowy widok i doda nowy wpis do mapy.

function view(name) {
  if (!name) // Throw an error

  if (view.views[name]) return view.views[name];

  return view.views[name] = new View({
    name: name
  });
}

// Local Map
view.views = {};

Niezwykle podstawowy, prawda? Uważam, że radykalnie upraszcza publiczny interfejs i ułatwia korzystanie z niego. Używam również umiejętności łańcuchowych ...

view('home.welcome')
   .child('menus')
   .child('auth')

Tower, framework, który rozwijam (z kimś innym) lub opracowuję następną wersję (0.5.0), będzie wykorzystywać to funkcjonalne podejście w większości swoich interfejsów.

Niektóre osoby wykorzystują włókna jako sposób na uniknięcie „piekła zwrotnego”. Jest to zupełnie inne podejście do JavaScript i nie jestem jego wielkim fanem, ale korzysta z niego wiele platform / platform; w tym Meteor, ponieważ traktują Node.js jako platformę wątek / na połączenie.

Wolę użyć abstrakcyjnej metody, aby uniknąć piekła zwrotnego. Może to stać się kłopotliwe, ale znacznie upraszcza rzeczywisty kod aplikacji. Pomagając w budowie frameworka TowerJS , rozwiązaliśmy wiele naszych problemów, oczywiście nadal będziesz mieć pewien poziom wywołań zwrotnych, ale zagnieżdżanie nie jest głębokie.

// app/config/server/routes.js
App.Router = Tower.Router.extend({
  root: Tower.Route.extend({
    route: '/',
    enter: function(context, next) {
      context.postsController.page(1).all(function(error, posts) {
        context.bootstrapData = {posts: posts};
        next();
      });
    },
    action: function(context, next) {
      context.response.render('index', context);
      next();
    },
    postRoutes: App.PostRoutes
  })
});

Przykład naszego, obecnie rozwijanego, systemu routingu i „kontrolerów”, choć dość różniących się od tradycyjnych „podobnych do szyn”. Ale przykład jest niezwykle mocny i minimalizuje liczbę wywołań zwrotnych i sprawia, że ​​wszystko jest dość widoczne.

Problem z tym podejściem polega na tym, że wszystko jest abstrakcyjne. Nic nie działa tak, jak jest i wymaga za tym „frameworka”. Ale jeśli tego rodzaju funkcje i styl kodowania są zaimplementowane w ramach, to jest to ogromna wygrana.

W przypadku wzorców w JavaScript zależy to szczerze. Dziedziczenie jest naprawdę przydatne tylko podczas korzystania z CoffeeScript, Ember lub dowolnego frameworka / infrastruktury „klasowej”. Gdy znajdujesz się w „czystym” środowisku JavaScript, korzystanie z tradycyjnego prototypowego interfejsu działa jak urok:

function Controller() {
    this.resource = get('resource');
}

Controller.prototype.index = function(req, res, next) {
    next();
};

Ember.js zaczął, przynajmniej dla mnie, stosować inne podejście do konstruowania obiektów. Zamiast konstruować każdą prototypową metodę niezależnie, użyłbyś interfejsu podobnego do modułu.

Ember.Controller.extend({
   index: function() {
      this.hello = 123;
   },
   constructor: function() {
      console.log(123);
   }
});

Wszystkie są różnymi stylami „kodowania”, ale dodają do bazy kodu.

Wielopostaciowość

Polimorfizm nie jest szeroko stosowany w czystym JavaScript, gdzie praca z dziedziczeniem i kopiowanie modelu podobnego do „klasy” wymaga dużej ilości kodu źródłowego.

Projektowanie oparte na zdarzeniach / komponentach

Modele oparte na zdarzeniach i oparte na komponentach to zwycięskie IMO lub najłatwiejsze do pracy, szczególnie podczas pracy z Node.js, który ma wbudowany komponent EventEmitter, jednak implementacja takich emiterów jest trywialna, to tylko miły dodatek .

event.on("update", function(){
    this.component.ship.velocity = 0;
    event.emit("change.ship.velocity");
});

To tylko przykład, ale jest to przyjemny model do pracy. Zwłaszcza w projekcie zorientowanym na grę / komponent.

Projektowanie komponentów jest odrębną koncepcją samą w sobie, ale myślę, że działa wyjątkowo dobrze w połączeniu z systemami zdarzeń. Gry są tradycyjnie znane z projektowania opartego na komponentach, gdzie programowanie obiektowe zabiera cię tylko do tej pory.

Projekt oparty na komponentach ma swoje zastosowania. To zależy od rodzaju systemu twojego budynku. Jestem pewien, że działałby z aplikacjami internetowymi, ale działałby wyjątkowo dobrze w środowisku gier, ze względu na liczbę obiektów i oddzielnych systemów, ale z pewnością istnieją inne przykłady.

Wzór Pub / Sub

Wiązanie zdarzeń i pub / sub jest podobny. Wzorzec pub / sub naprawdę świeci w aplikacjach Node.js ze względu na język ujednolicający, ale może działać w dowolnym języku. Działa bardzo dobrze w aplikacjach, grach itp. W czasie rzeczywistym

model.subscribe("message", function(event){
    console.log(event.params.message);
});

model.publish("message", {message: "Hello, World"});

Obserwator

Może to być subiektywne, ponieważ niektórzy ludzie myślą o schemacie Observer jako pub / sub, ale mają swoje różnice.

„Obserwator to wzorzec projektowy, w którym obiekt (znany jako przedmiot) utrzymuje listę obiektów w zależności od niego (obserwatorów), automatycznie powiadamiając ich o wszelkich zmianach stanu”. - Wzór obserwatora

Wzorzec obserwatora jest o krok dalej niż typowe systemy pub / sub. Obiekty mają ze sobą ścisłe relacje lub metody komunikacji. Obiekt „Temat” prowadziłby listę osób zależnych „Obserwatorów”. Osoba badana będzie na bieżąco aktualizować swoich obserwatorów.

Programowanie reaktywne

Programowanie reaktywne jest mniejszą, bardziej nieznaną koncepcją, szczególnie w JavaScript. Istnieje jeden framework / biblioteka (o których wiem), który udostępnia łatwą do pracy z API funkcję „reaktywnego programowania”.

Zasoby dotyczące programowania reaktywnego:

Zasadniczo ma zestaw danych synchronizujących (zmiennych, funkcji itp.).

 var a = 1;
 var b = 2;
 var c = a + b;

 a = 2;

 console.log(c); // should output 4

Uważam, że programowanie reaktywne jest znacznie ukryte, szczególnie w imperatywnych językach. To niezwykle potężny paradygmat programowania, szczególnie w Node.js. Meteor stworzył własny silnik reaktywny, na którym w zasadzie oparty jest szkielet. Jak reaguje Meteor za kulisami? to świetny przegląd tego, jak działa wewnętrznie.

Meteor.autosubscribe(function() {
   console.log("Hello " + Session.get("name"));
});

Wykona się to normalnie, wyświetlając wartość name, ale jeśli ją zmienimy

Session.set („name”, „Bob”);

Ponownie wyświetli wyświetlanie pliku console.log Hello Bob. Podstawowy przykład, ale możesz zastosować tę technikę do modeli danych i transakcji w czasie rzeczywistym. Za pomocą tego protokołu możesz tworzyć niezwykle wydajne systemy.

Meteor's ...

Wzorzec reaktywny i wzorzec obserwatora są dość podobne. Główna różnica polega na tym, że wzorzec obserwatora często opisuje przepływ danych z całymi obiektami / klasami w porównaniu z programowaniem reaktywnym zamiast tego opisuje przepływ danych do określonych właściwości.

Meteor jest doskonałym przykładem programowania reaktywnego. To środowisko wykonawcze jest nieco skomplikowane z powodu braku JavaScript w natywnych zdarzeniach zmiany wartości (zmieniają to serwery proxy Harmony). Inne frameworki po stronie klienta, Ember.js i AngularJS również wykorzystują programowanie reaktywne (do pewnego stopnia).

Dwie późniejsze struktury wykorzystują wzorzec reaktywny przede wszystkim w swoich szablonach (czyli automatycznych aktualizacjach). Angular.js używa prostej techniki „brudnego sprawdzania”. Nie nazwałbym tego dokładnie reaktywnym programowaniem, ale jest blisko, ponieważ brudne sprawdzanie nie odbywa się w czasie rzeczywistym. Ember.js stosuje inne podejście. Ember użycie set()i get()metody, które pozwalają im natychmiast aktualizować wartości. Z ich runloop jest niezwykle wydajny i pozwala na bardziej zależne wartości, gdzie kąt ma teoretyczną granicę.

Obietnice

Nie jest to poprawka do wywołań zwrotnych, ale usuwa wcięcia i ogranicza zagnieżdżone funkcje do minimum. Dodaje także ładną składnię do problemu.

fs.open("fs-promise.js", process.O_RDONLY).then(function(fd){
  return fs.read(fd, 4096);
}).then(function(args){
  util.puts(args[0]); // print the contents of the file
});

Możesz także rozłożyć funkcje wywołania zwrotnego, aby nie były wbudowane, ale to kolejna decyzja projektowa.

Innym podejściem byłoby połączenie zdarzeń i obietnic, w których miałbyś funkcję, aby odpowiednio wywołać zdarzenia, wtedy rzeczywiste funkcje funkcjonalne (te, które mają w sobie prawdziwą logikę) wiązałyby się z określonym zdarzeniem. Następnie przekazałbyś metodę dyspozytora wewnątrz każdej pozycji wywołania zwrotnego, musiałbyś jednak opracować pewne zagięcia, które przychodzą na myśl, takie jak parametry, wiedząc, do której funkcji wysłać itp.

Funkcja jednofunkcyjna

Zamiast mieć ogromny bałagan wywołany piekłem zwrotnym, zachowaj jedną funkcję dla jednego zadania i wykonaj to zadanie dobrze. Czasami możesz wyprzedzić siebie i dodać więcej funkcji w ramach każdej funkcji, ale zadaj sobie pytanie: czy może to stać się niezależną funkcją? Nazwij funkcję, a to wyczyści wcięcie, w wyniku czego rozwiąże problem z piekłem zwrotnym.

Na koniec sugeruję opracowanie lub użycie małego „frameworka”, po prostu szkieletu aplikacji i poświęcenie czasu na tworzenie abstrakcji, wybór systemu opartego na zdarzeniu lub „mnóstwo małych modułów, które są niezależny system. Pracowałem z kilkoma projektami Node.js, w których kod był wyjątkowo niechlujny, w szczególności z piekłem zwrotnym, ale także brakiem przemyśleń, zanim zaczęły kodować. Poświęć trochę czasu na przemyślenie różnych możliwości w zakresie API i składni.

Ben Nadel napisał kilka naprawdę dobrych postów na blogu o JavaScript oraz kilka dość surowych i zaawansowanych wzorców, które mogą działać w twojej sytuacji. Kilka dobrych postów, które podkreślę:

Odwrócenie sterowania

Chociaż nie jest to dokładnie związane z piekłem zwrotnym, może pomóc w ogólnej architekturze, szczególnie w testach jednostkowych.

Dwie główne pod-wersje odwrócenia kontroli to Dependency Injection i Service Locator. Uważam, że Service Locator jest najłatwiejszy w JavaScript, w przeciwieństwie do wstrzykiwania zależności. Dlaczego? Głównie dlatego, że JavaScript jest językiem dynamicznym i nie istnieje żadne statyczne pisanie. Java i C # są, między innymi, „znane” z wstrzykiwania zależności, ponieważ są w stanie wykryć typy i mają wbudowane interfejsy, klasy itp. To sprawia, że ​​jest to dość łatwe. Możesz jednak odtworzyć tę funkcję w JavaScript, jednak nie będzie ona identyczna i nieco zhackowana, wolę używać lokalizatora usług w moich systemach.

Każdy rodzaj inwersji kontroli radykalnie rozdzieli kod na osobne moduły, które można w każdej chwili wyśmiewać lub sfałszować. Zaprojektowałeś drugą wersję swojego silnika renderującego? Wspaniale, po prostu zastąp stary interfejs nowym. Lokalizatory usług są szczególnie interesujące z nowymi serwerami Harmony Proxies, jednak tylko efektywnie użytecznymi w Node.js, zapewniają ładniejsze API, zamiast używać Service.get('render');i zamiast tego Service.render. Obecnie pracuję nad tego rodzaju systemem: https://github.com/TheHydroImpulse/Ettore .

Chociaż brak pisania statycznego (pisanie statyczne jest możliwym powodem efektywnych zastosowań we wstrzykiwaniu zależności w Javie, C #, PHP - nie jest to pisanie statyczne, ale ma podpowiedzi). Można postrzegać jako punkt ujemny, zdecydowanie zamień to w mocną stronę. Ponieważ wszystko jest dynamiczne, możesz zaprojektować „fałszywy” układ statyczny. W połączeniu z lokalizatorem usług można powiązać każdy komponent / moduł / klasę / instancję z typem.

var Service, componentA;

function Manager() {
  this.instances = {};
}

Manager.prototype.get = function(name) {
  return this.instances[name];
};

Manager.prototype.set = function(name, value) {
  this.instances[name] = value;
};

Service = new Manager();
componentA = {
  type: "ship",
  value: new Ship()
};

Service.set('componentA', componentA);

// DI
function World(ship) {
  if (ship === Service.matchType('ship', ship))
    this.ship = new ship();
  else
    throw Error("Wrong type passed.");
}

// Use Case:
var worldInstance = new World(Service.get('componentA'));

Prosty przykład. W rzeczywistym świecie, efektywne wykorzystanie, musisz rozwinąć tę koncepcję, ale może pomóc oddzielić system, jeśli naprawdę chcesz tradycyjnego wstrzykiwania zależności. Być może będziesz musiał trochę pogrzebać przy tej koncepcji. Nie zastanawiałem się nad poprzednim przykładem.

Model-View-Controller

Najbardziej oczywisty wzór i najczęściej używany w sieci. Kilka lat temu JQuery był cały czas na topie, więc narodziły się wtyczki JQuery. Po stronie klienta nie potrzebowałeś pełnego frameworka, po prostu użyj jquery i kilku wtyczek.

Teraz jest ogromna wojna ramowa JavaScript po stronie klienta. Większość z nich używa wzorca MVC i wszyscy używają go inaczej. MVC nie zawsze jest implementowane tak samo.

Jeśli korzystasz z tradycyjnych interfejsów prototypowych, możesz mieć trudności z uzyskaniem cukru syntaktycznego lub dobrego API podczas pracy z MVC, chyba że chcesz wykonać pracę ręczną. Ember.js rozwiązuje ten problem, tworząc system „klasy” / obiektu. Kontroler może wyglądać następująco:

 var Controller = Ember.Controller.extend({
      index: function() {
        // Do something....
      }
 });

Większość bibliotek po stronie klienta rozszerza również wzorzec MVC, wprowadzając pomocniki widoku (stają się widokami) i szablony (stają się widokami).


Nowe funkcje JavaScript:

Będzie to skuteczne tylko, jeśli korzystasz z Node.js, ale mimo to jest nieocenione. Ta rozmowa Brendana Eicha na NodeConf przynosi nowe, fajne funkcje. Proponowana składnia funkcji, a zwłaszcza biblioteka js Task.js.

Prawdopodobnie to rozwiąże większość problemów z zagnieżdżaniem funkcji i przyniesie nieco lepszą wydajność z powodu braku narzutu funkcji.

Nie jestem pewien, czy V8 obsługuje to natywnie, ostatnio sprawdziłem, czy potrzebujesz włączyć flagi, ale działa to w porcie Node.js, który używa SpiderMonkey .

Dodatkowe zasoby:

Daniel
źródło
2
Niezły opis. Osobiście nie mam pożytku z MV? biblioteki. Mamy wszystko, czego potrzebujemy, aby uporządkować nasz kod dla większych, bardziej złożonych aplikacji. Wszyscy oni zbytnio przypominają mi Javę i C #, które próbują rzucić swoje różne bzdury na to, co faktycznie dzieje się w komunikacji serwer-klient. Mamy DOM. Mamy delegację wydarzeń. Mamy OOP. Potrafię powiązać własne zdarzenia ze zmianami danych tyvm.
Erik Reppen
2
„Zamiast ogromnego bałaganu wywołanego piekłem zwrotnym zachowaj jedną funkcję dla jednego zadania i wykonaj to zadanie dobrze”. - Poezja.
CuriousWebDeveloper
1
JavaScript jest w bardzo mrocznej epoce na początku i w połowie 2000 roku, kiedy niewielu rozumie, jak pisać przy użyciu dużych aplikacji. Jak mówi @ErikReppen, jeśli znajdziesz aplikację JS wyglądającą jak aplikacja Java lub C #, robisz to źle.
backpackcoder
3

Dodając do odpowiedzi Danielsa:

Obserwowalne wartości / komponenty

Pomysł ten został zapożyczony z frameworka MVVM Knockout.JS ( ko.observable ), z myślą, że wartości i obiekty mogą być obserwowalnymi podmiotami, a po zmianie jednej wartości lub obiektu automatycznie zaktualizuje wszystkich obserwatorów. Zasadniczo jest to wzorzec obserwatora zaimplementowany w Javascript, a zamiast tego jak większość implementowanych ram / pubów jest zaimplementowanych, „klucz” jest sam podmiotem zamiast dowolnego obiektu.

Wykorzystanie jest następujące:

// the subjects
// plain old javascript object with observable values
var shipComponent = {
    velocity : observable(0)
};

// the observer, a player user interface
// implemented with revealing module pattern
var playerUi = (function(ship) {

  var module = {
    setVelocity: function (x) { 
      // ... sets the velocity on the player user interface
    },

    // only called once
    init: function() {

      // subscribe to changes on the velocity value
      // using the module's function as callback
      module.velocity.onChange(playerUi.setVelocity);
    }
  };

  return module;
})(shipComponent).init();

// the player ui will change when the velocity value is changed
shipComponent.velocity.set(10);

Chodzi o to, że obserwatorzy zwykle wiedzą, gdzie jest temat i jak go subskrybować. Zaletą tego zamiast pub / sub jest zauważalna, jeśli trzeba dużo zmienić kod, ponieważ łatwiej jest usunąć tematy z etapu refaktoryzacji. Mam na myśli to, ponieważ po usunięciu tematu każdy, kto był na nim zależny, zawiedzie. Jeśli kod szybko zawiedzie, wiesz, gdzie usunąć pozostałe odwołania. Jest to w przeciwieństwie do całkowicie oddzielonego podmiotu (takiego jak klucz ciągowy we wzorze pub / sub) i ma większą szansę na pozostanie w kodzie, szczególnie jeśli użyto kluczy dynamicznych, a programista konserwacji nie został o tym poinformowany (martwy) kod w programowaniu konserwacji jest denerwującym problemem).

W programowaniu gier, zmniejsza potrzebę Ye Olde pętli zmiana wzorca i więcej do wiadomości i walczył / biernej programowania idiom, bo jak tylko coś się zmieniło przedmiotem będzie automatycznie aktualizować wszystkie obserwatorów na zmiany, bez konieczności oczekiwania na pętli aktualizacji wykonać. Istnieją zastosowania pętli aktualizacji (dla rzeczy, które muszą być zsynchronizowane z upływem czasu gry), ale czasami po prostu nie chcesz zaśmiecać, gdy same komponenty mogą się automatycznie aktualizować z tym wzorcem.

Rzeczywista implementacja obserwowalnej funkcji jest w rzeczywistości zaskakująco łatwa do napisania i zrozumienia (szczególnie jeśli wiesz, jak obsługiwać tablice w javascript i wzorcu obserwatora ):

var observable = function(v) {
    var val = v, subscribers = [];

    // the observable object,
    // as revealing module
    var output = {

        // subscribes to event
        onChange : function(func) {
            // idiomatic JS to add object to the
            // subscribers array
            subscribers.push(func);

            return output: // enables chaining
        },

        // the method that changes the observable object
        // and emits the event
        set : function(v) {
            var i;
            val = v;
            for (i = 0, i < subscribers.length; i++) {
                // this is hardly fault tolerant but as long
                // as subscribers are functions it'll work
                subscribers[i](v);
            }

            return output;
        }

    };

    return output;
};

Zrobiłem implementację obserwowalnym obiektu w JsFiddle która trwa na ten temat z zachowaniem elementów i jest w stanie usunąć abonentów. Zapraszam do eksperymentowania z JsFiddle.

Łup
źródło