Repozytorium Web Developera

JavaScript - obiekty

W JS wszystko zachowuje się jak obiekt, oprócz null i undefined.

Literał liczbowy nie jest obiektem, należy go najpierw zewaluować, np. (2).toString();.

Poniżej ES5 niektóre właściwości obiektu muszą być pisane jako string, jeśli nie to parser zwraca SyntaxError, np. { 'delete' : 'text' }.

Programowanie obiektowe - definicje

Enkapsulacja (hermetyczność)

Niektóre elementy danej klasy powinny być zabezpieczone przez bezpośrednie wywołanie z innych obszarów programu, aby zachować kontrolę modyfikacji danych. Ta zasada głównie tyczy się pól (zmiennych). Przykładem obrazującym tę zasadę może być klasa Biblioteka, której książki powinny być dostępne jedynie poprzez specjalne metody (symulujące osobę obsługującą czytelnika), a nie bezpośrednio (jak gdyby czytelnik mógł wejść do biblioteki, wziąć samemu książkę i wyjść).

Dziedziczenie

Definicje pewnych bytów mogą być uszczegóławiane poprzez zdefiniowanie innych bytów dziedziczących z tych bardziej ogólnych. Dzięki temu, klasy podrzędne nie muszą na nowo definiować pewnych metod, a jedynie mogą korzystać z tych, które zostały wprowadzone dla klasy nadrzędnej.

Polimorfizm

Obiekty mogą być przechowywane jako instancje tej samej, ogólnej klasy, ale jednocześnie być instancją różnych, szczegółowych klas, dzięki czemu diagramy stanów i klas zyskują nowe właściwości bądź zachowują się charakterystycznie dla klasy podrzędnej.

Abstrakcyjność

Właściwość, dzięki której byty w programie mogą się komunikować i działać bez wiedzy o szczegółach działania (implementacji) innych bytów. W językach programowania jest to zasada realizowana głównie przez klasy i metody abstrakcyjne, a także interfejsy.

Szybki przykład obiektowości JS


        // wersja 1 - obiekt Car
        myCar.make = 'Ford';
        myCar.model = 'Mustang';
        myCar.year = '1969';

        // wersja 2 - obiekt Car
        myNewCar = {
            make: 'Honda',
            model: 'Civic',
            year: 1999
        };

        // wersja 3 - konstruktor car
        function car(make, model, year) {
            this.make = make;
            this.model = model;
            this.year = year;
        }
        myNextCar = new car('Volvo', 'S70', 1998);

        // metody obiektu
        function displayCar() {
          var result = 'A Beautiful ' + this.year + ' ' + this.make + ' ' + this.model;
          pretty_print(result);
        }

        function car(make, model, year) {
          this.make = make;
          this.model = model;
          this.year = year;
          this.displayCar = displayCar;
        }

        myNextCar = new car('Volvo', 'S70', 1998);
        myNextCar.speed = 240;

        // prototypy - tworzenie hierarchii dziedziczenia
        function car(make, model, year) {
          this.make = make; this.model = model;
          this.year = year || new Date().getFullYear();
        }

        function truck(make, model, capacity, year) {
          this.base = car;
          this.base(make, model, year);
          this.capacity = capacity;
        }

        truck.prototype = new car;
        

Własne obiekty

Przykład JSON

Przykład literału obiektowego.


		{
			"firstName" : "Robert",
			"lastName" : "Kowalski"
		}
		

Przykład obiektu

Poniższy przykład prezentuje obiekt myObj z metodą myFunc().


		var myObj = {
		    myString: "Hello world!",
		    myFunc: function(){
		        return this.myString;
		    }
		};

		myObj.myFunc(); // = "Hello world!"
		

Alternatywnie:


        var myObj = new Object();

        myObj.myString = "Hello world!";
        myObj.myFunc = function(){
                return this.myString;
            };

        myObj.myFunc(); // = "Hello world!"
        

Przykład modułu

Poniżej znajduje się wzorzec modułu w JavaScript, z którego najczęściej korzystam.


        var myApp = (function(space) {

            var newPrivate = 2;

            space.init = function () {

                $(document.ready() {
                    // ..
                });

            };

            return space;

        }(myApp || {}));
        // ..
        myApp.init();
        

Do space.init warto dodać dwa kontenery na metody i eventy. Poniższy kod pochodzi z developer.mozilla.org.


        // Utwórz pojemnik MyApp.commonMethod na typowe metody i właściwości
        commonMethod = {
          regExForName: "",
          // zdefiniuj wyrażenie regularne do walidacji nazwiska
          regExForPhone: "",
          // zdefiniuj wyrażenie regularne do walidacji numeru telefonu
          validateName: function(name){
            // Zrób coś z nazwiskiem. Możesz użyć zmiennej regExForName
            // użycie "this.regExForName"
          },

          validatePhoneNo: function(phoneNo){
            // zrób coś z numerem telefonu
          }
        }

        // Obiekt razem z deklaracją metod
        event = {
            addListener: function(el, type, fn) {
            // jakiś kod
            },
           removeListener: function(el, type, fn) {
            // jakiś kod
           },
           getEvent: function(e) {
           // jakiś kod
           }

           // Można dodać kolejne metody i właściwości
        }

        // Składnia do użycia metody AddListener:
        MyApp.event.addListener("yourel", "type", callback);
        

Przykład konstruktora

Obiekty tworzy się również poprzez wywołanie konstruktorów za pomocą new. Tak na prawdę każde wywołanie funkcji, które jest poprzedone słowem kluczowym new, zachowuje się jak konstruktor.

Wewnątrz konstruktora - wywoływanej fukcji - wartość this wskazuje na nowo utworzony obiekt Object. Prototyp prototype nowo utworzonego obiektu będzie wskazywał na prototyp prototype obiektu funkcji, która została wywołana jako konstruktor.


		var MyConstructor = function(){
		    this.myNumber = 5;
		}
		myNewObj = new MyConstructor(); // = {myNumber: 5}
		myNewObj.myNumber; // = 5
		

W odróżnieniu od fabryki:

  • nie ma tworzonego obiektu bezpośrednio w funkcji
  • właściwości i metody są skojarzone bezpośrednio z obiektem this
  • nie ma wywołania return

Fabryki

Często zaleca się nie korzystać z operatora new, ponieważ zapominanie o jego stosowaniu może prowadzić do błędów.

W celu stworzenia nowego obiektu, powinno się używać fabryki i konstruować nowy obiekt wewnątrz tej fabryki.

Poniższa fabryka Foo() zwraca nowu obiekt obj.


		function Foo() {
		    var obj = {};
		    obj.value = 'blub';

		    var private = 2;
		    obj.someMethod = function(value) {
		        this.value = value;
		    }

		    obj.getPrivate = function() {
		        return private;
		    }
		    return obj;
		}
		

Metoda ta posiada jednak pewne wady.

  1. Zużywa więcej pamięci, ponieważ tworzony obiekt nie współdzieli metod poprzez prototyp.
  2. Aby móc dziedziczyć fabryka musi skopiować wszystkie metody z dziedziczonego obiektu lub przypisać ten obiekt, z którego się dziedziczy, jako prototyp do nowo utworzonego obiektu.
  3. Porzucenie łańcucha prototypów tylko ze względu na opuszczone słowo kluczowe new jest sprzeczne z duchem języka.

Przykład przypisywania prototypu

JavaScript nie posiada klasycznego modelu dziedziczenia. Zamiast tego dziedziczenie jest realizowane poprzez prototypy. W JavaScripcie, jeśli metoda lub pole/właściwość nie może zostać znalezione w aktualnie przetwarzanym obiekcie, to następuje przejście do prototypu i szukanie tych wartości tam. Podczas dostępu do właściwości obiektu JavaScript przejdzie w górę łańcucha prototypów, dopóki nie znajdzie właściwości bez nazwy. Nie ma tutaj żadnego kopiowania, obiekt przechowuje referencję do prototypu, tak więc zmiana prototypu powoduje zmianę również w podstawowym obiekcie.

Prototyp za pomocą __proto__

Jest to metoda nieustandaryzowana.


		var myObj = {
		    myString: "Hello world!"
		};
		var myPrototype = {
		    meaningOfLife: 42,
		    myFunc: function(){
		        return this.myString.toLowerCase()
		    }
		};

		myObj.__proto__ = myPrototype;
		myObj.meaningOfLife; // = 42

		// This works for functions, too.
		myObj.myFunc(); // = "hello world!"
		

[ES5] Prototyp za pomocą Object.create()

Metoda Object.create() jest dostępna od ES5.


		var myObj = {
		    myString: "Hello world!"
		};
		var myPrototype = {
		    meaningOfLife: 42,
		    myFunc: function(){
		        return this.myString.toLowerCase()
		    }
		};

		var myObj = Object.create(myPrototype);
		myObj.meaningOfLife; // = 42

		// This works for functions, too.
		myObj.myFunc(); // = "hello world!"
		

Istnieje jednak polyfill dla tej metody.


		if (Object.create === undefined){ // don't overwrite it if it exists
		    Object.create = function(proto){
		        // make a temporary constructor with the right prototype
		        var Constructor = function(){};
		        Constructor.prototype = proto;
		        // then use it to create a new, appropriately-prototyped object
		        return new Constructor();
		    }
		}
		

Prototyp za pomocą konstruktora

Metoda ta działa powszechnie. Konstruktory mają pole/właściwość nazywaną prototype, nie jest to jednak prototyp konstruktora, a prototyp obiektu utworzonego dzięki danemu konstruktorowi.


		MyConstructor.prototype = {
		    myNumber: 5,
		    getMyNumber: function(){
		        return this.myNumber;
		    }
		};
		var myNewObj2 = new MyConstructor();
		myNewObj2.getMyNumber(); // = 5
		myNewObj2.myNumber = 6
		myNewObj2.getMyNumber(); // = 6
		

Rozszerzanie prototypu

Do prototypu można dodawać własne metody oraz pola i właściwości. Przykładowo możemy rozszerzyć prototyp wbudowanego obiektu String() o funkcję zwracającą pierwszy znak ciągu. Nie należy jednak z tej opcji korzystać, chyba że chcemy stworzyć polyfill dla starszych wersji JS.


		String.prototype.firstCharacter = function(){
		    return this.charAt(0);
		}
		"abc".firstCharacter(); // = "a"
		

Pozostałe metody tworzenia konstruktorów i prototypów

  • Combination Constructor/Prototype Pattern
  • Dynamic Prototype Pattern
  • Parasitic Constructor Pattern
  • Durable Constructor Pattern

Metody dziedziczenia

  • Combination Inheritance
  • Prototypal Inheritance
  • Parasitic Inheritance
  • Parasitic Combination Inheritance

Właściwości obiektu

Wyróżniamy dwa typy właściwości obiektów:

  • data properties - z atrybutami configurable, enumerable, writable, value
  • accessor properties - z atrybutami configurable, enumerable, get, set

Metoda Object.defineProperties()

Metoda ta służy do deklarowania właściwości, w szczególności tych, które mają mieć niestandardowe ustawienia atrybutów.

Poniżej przykładowy kod, w którym ustalamy własne metody set i get. Notacja zapisu dla _year oznacza, że nie chcemy, aby zmienna była dostępna poza obiektem (ma być hermetyczna).

Kiedyś do ustalania getterów i setterów były wykorzystywane właściwości __defineGetter__() oraz __defineSetter().


		var book = {};

		Object.defineProperties(book, {
			_year: {
				value: 2004
			},

			edition: {
				value: 1
			},

			year: {
				get: function() {
					return this._year;
				},

				set: function(newValue) {
					if(newValue > 2004) {
						this._year = newValue;
						this.edition += newValue - 2004;
					}
				}
			}
		});
		

Metoda Object.getOwnPropertyDescriptor()

Metoda ta służy do uzyskiwania deskryptora właściwości. Działa tylko na instancji, więc jeśli chcemy mieć dostęp do właściwości prototypu musimy ją wywołać bezpośrednio na nim.


        var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
        alert(descriptor.value);         //2004
        alert(descriptor.configurable);  //false
        alert(type of descriptor.get);   //undefined
        

[ES5] Metoda Object.keys()

Zwraca wszystkie klucze właściwości obiektu. Jest to ulepszona wersja metody getOwnPropertyNames(), która to wypisywała również właściwość constructor.

[ES5] Definiowanie konstruktora obiektu

Poniższy kod zadziała tylko w ES5.


        Object.defineProperty(Person.prototype, "constructor", {
            enumerable: false,
            value: Person
        });
        

Funkcje obiektów

Metoda hasOwnProperty()

Jedyną metodą służącą do sprawdzenia istnienia jakiejś właściwości w konkretnym obiekcie jest metoda hasOwnProperty. Zaleca się korzystać z hasOwnProperty w każdej pętli for in. Pozwoli to uniknąć błędów pochodzących z rozszerzonych natywnych prototypów.

[ES5] Metoda Object.getPrototypeOf()

Jest alternatywą dla metody isPrototypeOf().

Metody call() i apply()

Tymczasowe przesyłanie obiektów do funkcji.


		// We can also specify a context for a function to execute in when we invoke it
		// using `call` or `apply`.
		var anotherFunc = function(s){
		    return this.myString + s;
		}
		anotherFunc.call(myObj, " And Hello Moon!"); // = "Hello World! And Hello Moon!"

		// The `apply` function is nearly identical, but takes an array for an argument
		// list.

		anotherFunc.apply(myObj, [" And Hello Sun!"]); // = "Hello World! And Hello Sun!"
		

Jest to przydatne w przypadku metody hasOwnProperty(), która sprawdza, czy właściwość istnieje w danym obiekcie (łącznie z prototypami), a która może zostać nadpisana w obiekcie, dlatego konieczne jest wywołanie jej przez metodę call().


		var foo = {
		    hasOwnProperty: function() {
		        return false;
		    },
		    bar: 'Here be dragons'
		};

		foo.hasOwnProperty('bar'); // zawsze zwraca false

		// Została użyta metoda innego obiektu i wywołana z kontekstem
		// `this` ustawionym na foo
		({}).hasOwnProperty.call(foo, 'bar'); // true
		

Metoda bind()

Stały odpowiednik call() i apply().


		// But, `call` and `apply` are only temporary. When we want it to stick, we can
		// use `bind`.

		var boundFunc = anotherFunc.bind(myObj);
		boundFunc(" And Hello Saturn!"); // = "Hello World! And Hello Saturn!"
		

Wbudowane obiekty

Obiekt Image()

Przed zastąpieniem rysunku widocznego na stronie należy skorzystać z metody wstępnego pobierania grafiki.


		var preloadImage =
			['images/roll.png',
			'images/flower.png',
			'images/cat.jpg'];

		var imgs = [];

		for(var i = 0; i < preloadImages.length; i++) {
			imgs[i] = new Image();
			imgs[i].src = preloadImages[i];
		}