Prototyp funkcji

Funkcje w JavaScript tak naprawdę są obiektami, a obiekty te mają swój specjalny podtyp Function. Gdy więc tworzymy funkcję, to tak naprawdę tworzymy obiekt, do którego możemy zaglądnąć. Jest tam jedna bardzo interesująca rzecz.

Obiekt funkcyjny

Na początek stworzę prostą funkcję:

function person() {
  console.log('John rambo');
}

console.dir(person);
1
2
3
4
5

W ogóle na razie nie interesuje nas, co robi ta funkcja, bardziej interesuje nas jaki obiekt ta funkcja wytworzyła. Wykorzystując console.dir mogę wypisać nazwę funkcji do konsoli i w ten sposób podejrzę obiekt, jaki tworzy. Również mógłbym przypisać po prostu funkcje do zmiennej i w ten sposób przechwycić ten obiekt:

arguments: null
caller: null
length: 0
name: "person"
prototype: {constructor: ƒ}
__proto__: ƒ ()
1
2
3
4
5
6

Okazuje się, że obiekt, który został stworzony przez funkcję, posiada wiele różnych właściwość związanych z funkcją, ale posiada także właściwość prototype, którą do tej pory mogliśmy zauważyć głównie tylko w obiektach wbudowanych jak Object.prototype, Array.prototype i tak dalej. Nigdy jednak, obiekty, które tworzyliśmy nie miały swojej właściwości prototype.

Nie mylmy jednak tej właściwości z właściwością [[Prototype]], którą zapisuje się z dwoma nawiasami kwadratowymi i do której jest dostęp przez akcesor __proto__. Prototyp, który posłużył do stworzenia tego obiektu jest tutaj dostępny właśnie przez akcesor __proto__ i pochodzi on z Function.prototype.

Wróćmy jednak do tej zwykłej właściwości prototype. Okazuje się, że tylko obiekty podtypu Function mają właściwość prototype, która jak wiemy, może być dziedziczona przez kolejne obiekty. Czyli ten obiekt może nam posłużyć do budowania innych obiektów.

Tak jak inne prototypy jak Object.prototype, Array.prototype i tak dalej, tak nasz prototyp person.prototype może również posłużyć do stworzenia innych obiektów. W ten sposób stworzyliśmy bazowy prototyp dla naszych własnych obiektów.

Wywołanie konstruktora funkcji

Wiemy już, że obiekty funkcyjne mają unikatową cechę w postaci właściwości prototype, możemy to zatem wykorzystać i tworzyć własne obiekty na bazie tego prototypu:

function Animal(name) {
  this.name = name;
  this.voice = function() {
    console.log('hrum hrum');
  };
};

const animalObj = new Animal('Cat');
console.log(animalObj);
1
2
3
4
5
6
7
8
9

Stworzyłem nową funkcję o nazwie Animal. Zauważcie, że tworzę w tej funkcji właściwości przez odwołanie się do this.

Tym razem również zmienna zaczyna się wielką literą. Jest to konwekcja, która powstała w środowisku JavaScript, aby funkcje lub wyrażenia funkcyjne, które będą tworzyć obiekty zapisywać wielką literą.

W JavaScript, obiekty możemy wywołać jako funkcje lub też możemy wywołać je z konstruktorem, czyli słówkiem new. Zawsze, gdy używamy new, wywołujemy konstruktor obiektu. Każdy obiekt wbudowany w JavaScript ma swój konstruktor jak new String(), new Array() czy nawet new Object().

Gdy wywołamy new na obiekcie typu Function czyli naszym Animal, powstanie normalny obiekt, jaki znamy do tej pory, ale prototypem dla tego właśnie obiektu będzie Animal.prototype. Czyli nie jak do tej pory Object.prototype , Array.prototype czy Function.prototype tylko nasz stworzony prototyp obiektu.

Animal {name: "Cat", voice: ƒ}
name: "Cat"
voice: ƒ ()
__proto__:
	constructor: ƒ Animal(name)
	__proto__: Object
1
2
3
4
5
6

Po wywołaniu funkcji z konstruktorem to, co było w Animal.prototype stało się wewnętrzną właściwością [[Prototype]] stworzonego właśnie obiektu. Stworzony animalObject ma akcesor __proto__ prowadzący teraz do Animal.prototype. Oczywiście na końcu tego łańcuchu prototypów jest Object.prototype, ale wcześniej jest nasz ustalony Animal.prototype.

Obiekt, który został zwrócony przez new Animal() to zwykły obiekt JavaScript, taki sam jak tworzyliśmy do tej pory literalnie za pomocą nawiasów klamrowych, jednak jego podstawowym prototypem jest Animal.prototype, a nie Object.prototype. Jest to nowy typ Animal, który sami stworzyliśmy.

Co nam daje taka funkcjonalność? Przede wszystkim mając zdefiniowaną funkcję i wywołując ją z konstruktorem za pomocą słówka new możemy tworzyć kolejne obiekty w bardzo prosty sposób:

const animalObj2 = new Animal('Dog');
const animalObj3 = new Animal('Mouse');
1
2

Obiekty te mają te same pola i te same metody. Otrzymujemy więc namiastkę klas, które znane są z języków obiektowych jak Java, C++ czy C#. Dodatkowo obiekty te bazują na naszym stworzonym prototypie Animal.prototype:

Animal.prototype.run = function() {
  console.log('running');
};

animalObj.run();
1
2
3
4
5

Możemy więc swobodnie modyfikować ten prototyp, dodawać nowe właściwości. Zmiany te będą dotyczyć tylko naszego stworzonego obiektu.

Ma to duża przewagę nad zwykłymi obiektami, które tworzyliśmy do tej pory literalnie i dziedziczyły one prototyp po Objec.prototype. Trudno jest tworzyć seryjnie obiekty o tych samych właściwościach w sposób literalny. Dodatkowo obiekty takie mają wtedy ogólny prototyp Object.prototype, którego lepiej nie modyfikować.

Ta właściwość funkcji była bardzo mocno wykorzystywana, zanim pojawiły się klasy w ESCMAScript 6. Dzisiaj w nowoczesnym kodzie raczej nie spotkacie się z tworzeniem obiektów w taki sposób. Obecnie wykorzystuje się klasy, które są trochę * syntactic sugar* dla tworzenia obiektów przy pomocy konstruktora funkcji. O klasach będzie zupełnie nowy dział.

Jednak to, co teraz widzimy to podstawy do zrozumienia klas i programowania obiektowego w JavaScript, które opiera się na prototypach i dziedziczeniu prototypowym.

Słówko new jest bardzo ważne

Gdy tworzymy funkcję, z której będziemy potem tworzyć obiekty za pomocą wywołania konstruktora, ważne jest, aby użyć słówko new:

function Car(name) {
  this.name = name;
}

const car1 = Car('Audi');
console.log(car1); // undefined
console.log(window.name); // Audi

const car2 = new Car('Opel');
console.log(car2); // {name: "Opel"}
1
2
3
4
5
6
7
8
9
10

Mamy funkcję, która tworzy wewnętrzną właściwość przez this.name. Pierwszy przypadek wywołuje funkcję bez new. Funkcja ta zwraca undefined. Jeżeli nie użyliśmy return to funkcja zawsze zwraca sama z siebie undefined . Takie wywołanie nie tworzy nam też obiektu.

Nie jesteśmy także w strict mode dlatego w tym przypadku, this w funkcji wskazuje na obiekt window, z tego powodu nasze pole model znalazło się w obiekcie window. Gdybyśmy użyli trybu ścisłego w ogóle nie moglibyśmy wywołać funkcji bez słówka new. W trybie ścisłym this w funkcji wskazuje na undefined. I tak zapisana funkcja nie może zostać wywołana bez new.

Dlatego tak napisane funkcje powinniśmy wywoływać tylko z konstruktorem, czyli za pomocą new. Wtedy otrzymujemy nowy obiekt, a this jest odwołaniem do tego obiektu. Na szczęście używając trybu ścisłego, otrzymamy stosowne błędy informujące, że nie możemy wywoływać takich funkcji bez wywołania konstruktora.

Sprawdzanie typu przez instanceof

Użyliśmy do tej pory funkcji, potem wywoływaliśmy konstruktor tej funkcji i powstawał obiekt. Można się pogubić, co jest czym, dlatego warto sprawdzić sobie typy, które powstają w czasie tworzenia takich konstrukcji:

function Book() {
};

console.log(Book instanceof Function); // true
console.log(Book instanceof Object); // true
console.log(Book instanceof Book); // false
1
2
3
4
5
6

Mamy funkcję Book, którą za chwilę będziemy wywoływać z konstruktorem. Za pomocą operatora instanceof mogę sprawdzić, do jakich typów należy konkretny obiekt. W tym przypadku porównując do typu Function otrzymujemy true ponieważ Function.prototype to pierwszy prototyp tej funkcji. Również otrzymujemy true gdy sprawdzamy, czy funkcja jest instancją Object. Każdy obiekt pochodzi z Object.prototype. Natomiast sama funkcja Book nie jest w żadne sposób związana z typem Book.

Gdy teraz wywołamy funkcję z konstruktorem, czyli de facto stworzymy nowy obiekt to wyniki mamy trochę inne:

const bookObject = new Book();

console.log(bookObject instanceof Function); // false
console.log(bookObject instanceof Book); // true
console.log(bookObject instanceof Object); // true
1
2
3
4
5

Stworzony obiekt przy pomocy konstruktora funkcji nie jest już typem Function. Tak stworzony obiekt ma swój prototyp i jest teraz typu Book oraz jest typu Object jak każdy obiekt w JavaScript.

To nam pokazuje, że przy pomocy konstruktora funkcji jesteśmy w stanie tworzyć własne typy w JavaScript, to samo będziemy robili za pomocą klas.

Czasami też możecie się spotkać w Internecie ze sprawdzaniem typu za pomocą właściwości constructor:

console.log(bookObject.constructor === Book); // true
console.log(bookObject.constructor === Object); // false
1
2

Każdy prototyp ma właściwość constructor i zostaje ona odziedziczona przez każdy obiekt. Właściwość ta dokładnie wskazuje, z jakiej funkcji wywołanej przez konstruktor powstał obiekt, w tym wypadku wskazuje dokładnie na Book. Gdy porównamy wartość konstruktora do Object otrzymujemy false, ponieważ konstruktor wskazuje tylko na jeden konkretny obiekt, który posłużył do konstrukcji.

Używanie konstruktora jednak może być czasami mylne, ponieważ może zostać nadpisany nawet przez programistę. Są to jednak już techniki, którymi nie musimy sobie zaprzątać głowy. Jeżeli chcemy sprawdzić dokładny typ obiektu, używajmy operatora instanceof.

Co warto zapamiętać

  • funkcje w JavaScript to obiekty
  • obiekty stworzone przez funkcje jako jedyne mają dodatkową właściwość prototype
  • wykorzystując konstruktor funkcji, możemy tworzyć obiekty, które mają nasz własny prototyp jak Book.prototype , Car.prototype
  • tylko wywołanie funkcji z konstruktorem, czyli z użyciem new stworzy obiekt z naszym własnym prototypem
  • do sprawdzania, z jakich typów został stworzony obiekt najlepiej użyć instanceof