Funkcje, klasy i obiekty

Klasyczny Javascript nie jest językiem w pełni obiektowym, choć zmienne a nawet stałe (napisy i liczby) i funkcje zachowują się jak obiekty. Jednak nie można w prosty sposób tworzyć obiektów pochodnych (dziedziczenie) ani definiować klas obiektów. Wykorzystujemy do tego funkcje, które mogą udawać klasy (obiekt wzorcowy) lub obiekty. Możemy utworzyć egzemplarz takiej klasy (funkcji wzorcowej), który jest obiektem. Wykorzystywany jest do tego tak zwany prototyp.

Klasa w JavaScript wygląda mniej więcej tak:

var NazwaKlasy = (function () {
 function NazwaKlasy() {
 }
 NazwaKlasy.prototype.jakasWlasnosc = function () {
 };
 return NazwaKlasy;
})();

Sprawdźmy:

> var nazwaKlasy = (function () {
   function Konstuktor() { }
   return Konstuktor;
 })();
> var obiekt= new nazwaKlasy();
> typeof(nazwaKlasy);
'function'
> typeof(obiekt);
'object'

Polecenie: new Klasa() tworzy nowy obiekt danej klasy.

Przykład:

> var Zwierze = (function () {
 function Zwierze() {
 }
 Zwierze.prototype.rodzaj = function () {
 return 'zwierze';
 };
 return Zwierze;
 })();
> var obiekt = new Zwierze();
> console.log(obiekt.rodzaj());
zwierze

Na szczęście takie klasy są tworzone głównie przez twórców bibliotek. W codziennym programowaniu częściej mamy do czynienia z konkretnymi obiektami, których użycie jest łatwe.

Słowo this

Słowo kluczowe this jest używane do wskazania na obiekt właściciela funkcji w której to słowo stosujemy. W języku JavaScript działa ono inaczej, niż mogą tego oczekiwać osoby znające inne – obiektowe języki programowania. Wynika to z faktu, że w Javascript to funkcje „udają” obiekty.

Zdefiniujmy klasę z własnością "komunikat" i metodę o nazwie "metoda":

var NazwaKlasy = (function () {

  function NazwaKlasy(komunikat) {
    this.komunikat = komunikat;
  }

  NazwaKlasy.prototype.metoda = function () {
    return "Moja własność: " + this.komunikat;
  };

 return NazwaKlasy;

})();


var instancja = new NazwaKlasy("witaj");

console.log(instancja.komunikat);
console.log(instancja.metoda());
instancja.komunikat='zmiana';
console.log(instancja.metoda());

Na wynik otrzymamy:

witaj
Moja własność: witaj
Moja własność: zmiana

Wszystko w miarę jasne. Co jednak się stanie, gdy definiowane metody będą bardziej złożone?

var NazwaKlasy = (function () {

  function NazwaKlasy(komunikat) {

    this.komunikat = komunikat;

  }

  NazwaKlasy.prototype.metoda1 = function () {
    return "Moja własność: " + this.komunikat;
  };


  NazwaKlasy.prototype.metoda2 = function () {
    function wyswietl() {
      return "Moja własność: " + this.komunikat;
    };
    return wyswietl();
  };

  return NazwaKlasy;

})();


let instancja = new NazwaKlasy("witaj");..

console.log(instancja.metoda1());
console.log(instancja.metoda2());

Na wynik dostaniemy:

Moja własność: witaj
Moja własność: undefined

Dlaczego?

Bo słowo this odnosi się do "właściciela" - a dla funkcji "wyswietl" właścicielem jest "metoda2"!

Aby uniknąć takich problemów wprowadza się dodatkową zmienną (własność) zwyczajowo nazywaną "self":

var NazwaKlasy = (function () {

  function NazwaKlasy(komunikat) {

    this.komunikat = komunikat;

  }

  NazwaKlasy.prototype.metoda1 = function () {
    return "Moja własność: " + this.komunikat;
  };


  NazwaKlasy.prototype.metoda2 = function () {
    var self = this;

    function wyswietl() {
      return "Moja własność: " + self.komunikat;
    };
    return wyswietl();
  };

  return NazwaKlasy;

})();


let instancja = new NazwaKlasy("witaj");..

console.log(instancja.metoda1());
console.log(instancja.metoda2());

Teraz otrzymujemy wynik zgodny z oczekiwaniem.

Bind

Jedną z konsekwencji używania w funkcjach this jest to, że metody obiektu nie można bezproblemowo oderwać od obiektu.Dotyczy to na przykład przykład obiektu console. Poniższy kod jest błędny!!!:

  var log = console.log;
  log('ABC');

Funkcja log zakłada, żethis odnosi się do console, ale to jest zachowane wyłącznie wtedy, gdy stosujemy notację kropkową - sięgając wprost do wnętrza obiektu (console.log). Można ten problem rozwiązać, stosując metodę bind - wiążącą funkcję z właściwym obiektem (funkcja this będzie wskazywać poprawny obiekt):

  var log = console.log.bind(console);
  log('ABC');

Struktura a funkcja

Określenie "obiekt" używane jest w odniesieniu do struktury ( {} ) jak i pamiętanej w zmiennej funkcji. Czy to nie prowadzi do pomyłek? Nie - ponieważ na najbardziej podstawowym poziomie obiekt w języku JavaScript ma zawsze postać "tablicy asocjacyjnej" złożonej z kluczy i wartości. Możesz nawet sam się o tym przekonać, uzyskując dostęp do własności obiektu za pomocą składni tablicy. Oto przykład:

let obiekt1 = { nazwa : 'Obiekt 1'};
let obiekt2 =  function () { }
obiekt2.nazwa ='Obiekt 2';
console.log(obiekt1["nazwa"]);
console.log(obiekt2["nazwa"]);

W obu przypadkach dostaniemy na wynik właściwą nazwę obiektu. Z uwagi na prostszą składnię wygodniej jest oczywiście używać struktur.

Nowy standard

Jak widać użycie obiektów jest proste. Jednak stosowanie klas już jest mocno pogmatwane….

Nic więc dziwnego, że nowsze dialekty Javascript wprowadzają zmiany, dzięki którym klasy i obiekty przypominają to, co znamy z innych języków programowania.

Standard ECMAScript 2015 (ES2015) oferuje tak zwany „lukier składniowy”, czyli proste zapisy kryjące nieco bardziej złożone mechanizmy. Prosty przykład obiektów w ES2105:

class Empty {
}
class HelloWorld extends Empty {
  render() {
  return"Hello World";
}
}
var instance1 = new HelloWorld();
console.log(instance1.render());

Po przetworzeniu translatorem Babel uzyskujemy czysty Javascript.

Fragmenty wyniku tej translacji widzimy poniżej:

"use strict";


var _createClass = function() {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) { var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true; if ("value"in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
returnfunction(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();


function _possibleConstructorReturn(self, call) {
if (!self) {
thrownew ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}


function _inherits(subClass, superClass) {
if (typeof superClass !== "function"&& superClass !== null) {
thrownew TypeError("Super expression must either be null or a function, not " + 
          typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ?
Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}


function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) { thrownew TypeError(
   "Cannot call a class as a function"); }
}


var Empty = function Empty() {
_classCallCheck(this, Empty);
};


var HelloWorld = function(_Empty) {
_inherits(HelloWorld, _Empty);


function HelloWorld() {
_classCallCheck(this, HelloWorld);


return _possibleConstructorReturn(this,
(HelloWorld.__proto__ || Object.getPrototypeOf(HelloWorld)).apply(this, arguments));
}


_createClass(HelloWorld, [{
key: "render",
value: function render() {
return"Hello World";
}
}]);


return HelloWorld;
}(Empty);


var instance1 = new HelloWorld();


console.log(instance1.render());

Uff - łatwo nie jest ;-). Na szczęście nie musimy wnikać w te szczegóły implementacyjne.

Nieco prościej robi to TypeScript (https://www.typescriptlang.org/play/index.html):

var __extends = (this&&this.__extends) || (function () {
// przepisanie własności przodka
returnfunction (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();


var Empty = /**@class*/ (function () {
function Empty() {
}
return Empty;
}());
var HelloWorld = /**@class*/ (function (_super) {
__extends(HelloWorld, _super);
function HelloWorld() {
return _super !== null&& _super.apply(this, arguments) || this;
}
HelloWorld.prototype.render = function () {
return"Hello World";
};
return HelloWorld;
}(Empty));

__extends Odpowiada ona za kopiowanie funkcji z prototypu podstawowego do klasy potomnej