Ta strona używa ciasteczek (cookies), dzięki którym nasz serwis może działać lepiej. Dowiedz się więcej OK, rozumiem
WebHelp.pl Warsztat Artykuły Funkcje w JavaScript - cz. 2

Warsztat / Artykuły i tutoriale

Funkcje w JavaScript - cz. 2

Rafał Kukawski 25 listopada 2016 komentarze ()

Tagi:JavaScript

W tym artykule kontynuowany będzie temat funkcji, ze szczególnym naciskiem na operator new i model obiektowy JavaScriptu. Przebrnięcie przez ten etap pozwoli zająć się nowościami w tym obszarze wprowadzonymi do szóstej edycji języka.

W artykułach celowo skupiam się na 5 generacji języka, ponieważ minie jeszcze trochę czasu zanim w przeglądarkowym świecie będziemy swobodnie korzystać z nowszych generacji bez używania Babel, Traceur i im podobnych narzędzi.

Operator new

Pewnie nieraz spotkałeś się ze skryptami, w których używano instrukcji typu

Kod: Zaznacz cały
var arr = new Array();
var now = new Date();
var newYear2017 = new Date('2017-01-01 00:00:00');

W każdej z nich powtarza się słowo new, które jest JavaScriptowym operatorem. Operator ten używany jest z funkcjami. Tak, Array, Date są funkcjami, które będziemy nazywać konstruktorami. Zwykle konstruktory rozpoznamy po nazwie zaczynającej się od dużej litery, choć jest to tylko przyjęta konwencja nazewnictwa a nie ograniczenie narzucone przez język.

JavaScript domyślnie oferuje 15 konstruktorów:

  • Array
  • Boolean
  • Date
  • Error
    • EvalError
    • RangeError
    • ReferenceError
    • SyntaxError
    • TypeError
    • URIError

  • Function
  • Number
  • Object
  • RegExp
  • String
W szóstej generacji języka zestaw konstruktorów zwiększył się do 33 pozycji

Zestaw ten jest rozszerzony przez inne specyfikacje, np. w przeglądarkach znajdziemy konstruktory zdefiniowane w Document Object Model, XMLHttpRequest, itd.

Wynikiem działania operatora new jest obiekt posiadający pewne cechy. W przypadku Array dostaniemy tablicę z jej charakterystycznymi metodami, w przypadku Date – obiekt pozwalający na operacje datą i czasem.

Kod: Zaznacz cały
console.log(
    arr.push(2), // 1
    newYear2017.getFullYear() // 2017
);

Nie wszystkie funkcje są konstruktorami. Większość wbudowanych funkcji oferowanych przez język JavaScript nie działa z operatorem new.

Kod: Zaznacz cały
new parseInt("1234567890", 10); // TypeError: parseInt is not a constructor

Cechy obiektów

Każdy z tworzonych obiektów posiada charakterystyczny dla siebie zestaw metod i cech. JavaScript realizuje to poprzez prototypy. Jeśli chcemy sprawdzić jakie cechy będzie posiadał obiekt danego konstruktora, wykonujemy

Kod: Zaznacz cały
console.log(
    Object.prototype,
    Array.prototype,
    String.prototype
);

Na poniższym obrazku widać prototyp dla obiektów String dostępny w Firefoksie, zawierający mniej i bardziej przydatne funkcje operujące na tekstach.

String.prototype w Mozilla Firefox

Rozszerzanie prototypów

Jeśli brakuje nam implementacji jakiejś metody, możemy ją dodać do prototypu.

Kod: Zaznacz cały
String.prototype.ucfirst = function () {
    return this.charAt(0).toUpperCase() + this.substr(1);
};

Względnie lepszym sposobem będzie użycie kodu

Kod: Zaznacz cały
Object.defineProperty(String.prototype, 'ucfirst', {
    value: function () {
        return this.charAt(0).toUpperCase() + this.substr(1);
    },
    writeable: true,
    configurable: true
});

Różnicą pomiędzy obydwoma sposobami jest deskryptor, który nowa własność ucfirst otrzyma. Przykład używający Object.defineProperty ustawia deskryptor na taki sam jaki posiadają wszystkie metody natywnych prototypów. W pierwszym przypadku deskryptor otrzyma enumerable: true, co będzie skutkować tym, że iterując po takim obiekcie pętlą for..in, nowa własność będzie jednym z elementów iteracji.

Kod: Zaznacz cały
for (var prop in "lorem ipsum") {
    console.log(prop); // zobaczymy w konsoli nazwę "ucfirst"
}

Własne konstruktory

Możemy też tworzyć własne konstruktory. Wystarczy… zdefiniować nową funkcję.

Kod: Zaznacz cały
function Rectangle (width, height) {
    this.width = width;
    this.height = height;
}

Każda tworzona przez nas funkcja może służyć za konstruktor. Domyślnie prototyp takiego konstruktora jest pusty.

Powyższy kod definiuje konstruktora obiektów Rectangle, które posiadają 2 cechy specyficzne dla każdej nowej instancji – szerokość i wysokość. Ale prostokątom, niezależnie od wymiarów, można zdefiniować wspólne cechy, np. metodę area, która zwróci pole dowolnego prostokąta. I takie rzeczy należy wrzucać do prototypu.

Kod: Zaznacz cały
Rectangle.prototype.area = function () {
    return this.width * this.height;
};

Teraz możemy wywołać

Kod: Zaznacz cały
var rect = new Rectangle(200, 100);
var square = new Rectangle(200, 200);

console.log(
    rect.area(), // 20000
    square.area() // 40000
);

Jak działa operator new?

Patrząc na listingi kodu w poprzednim rozdziale, mogą pojawić się pytania „jakim cudem wynikiem jest obiekt, skoro konstruktor nic nie zwraca?” oraz „na co wskazuje this wewnątrz konstruktora”. Odpowiedź tkwi w detalach działania operatora new.

Wywołując

Kod: Zaznacz cały
var rect = new Rectangle(20, 10);

Operator new tworzy nowy obiekt, który w swoim wnętrzu ma zaszyte powiązanie z prototypem Rectangle.prototype. Gdyby wykonać to ręcznie użylibyśmy

Kod: Zaznacz cały
var obj = Object.create(Rectangle.prototype);

Następnie operator wywołuje wskazanego konstruktora. Wywołanie to jest o tyle szczególne, że jako this będzie ustawiony przed momentem utworzony obiekt.

Kod: Zaznacz cały
var result = Rectangle.apply(obj, arguments);

Wewnątrz konstruktora mamy okazję ustawić stan utworzonego obiektu tak, żeby odpowiadał logice programu. W naszym przypadku obj otrzyma cechy width i height z wartościami 20 i 10.

Na koniec operator new zwraca utworzony przez siebie obiekt.

Kod: Zaznacz cały
return obj;

I to by było na tyle, gdyby nie możliwy alternatywny, choć rzadzko używany, scenariusz. Operator new w pewnych warunkach może zwrócić inny obiekt niż ten utworzony przez siebie. Jeśli konstruktor zwróci wartość obiektową, to operator new zwróci właśnie tę wartość zamiast utworzony przez siebie obiekt, który po prostu przepadnie. W naszym przykładzie konstruktor Rectangle nie zwraca nic, tj. niejawnie zwraca undefined, zatem podążamy główną ścieżką opisanego tu algorytmu.

Dlatego ostatni fragment kodu „emulującego” działanie operatora new trzeba rozszerzyć do postaci:

Kod: Zaznacz cały
return typeof result === "object" && result !== null ? result : obj;

Ta ścieżka będzie bardzo rzadko wykorzystywana, ale warto o niej wiedzieć.

Jak sprawdzić, czy obiekt jest danego typu?

W wielu przypadkach pierwszą myślą byłoby użycie operatora typeof. Niestety, operator ten jest mocno ograniczony, o czym mogliśmy się już przekonać w tekście Typy danych, wartości i konwersja typów w JavaScript

Kod: Zaznacz cały
console.log(
    typeof [], // "object"
    typeof new Rectangle(20, 10), // "object"
    typeof null, // "object"
    typeof NaN // "number"
);

Do naszych celów bardziej przydatny będzie operator instanceof.

Kod: Zaznacz cały
console.log(
    [] instanceof Array, // true
    new Date() instanceof Date, // true
    RegExp('.') instanceof Date, // false
    new Rectangle(20,10) instanceof Rectangle // true
);

Dziedziczenie

Eksperymentalnie sprawdźmy sobie

Kod: Zaznacz cały
console.log(
    [] instanceof Object, // true
    new Date() instanceof Object, // true
    new Rectangle(20, 10) instanceof Object // true
);

Wychodzi na to, że zarówno tablice, obiekty dat jak i własny Rectangle są też typem Object. Z czego to wynika? Wynika to z dziedziczenia, które jest realizowane przez łańcuch prototypów.

Prostokąt w naszym przykładzie został utworzony na bazie prototypu Rectangle.prototype. Dlatego operator instanceof zwróci true dla

Kod: Zaznacz cały
console.log(
    new Rectangle(20, 10) instanceof Rectangle
);

Ale sam prototyp Rectangle.prototype też jest obiektem, który powstał na bazie jakiegoś prototypu – w tym wypadku na bazie Object.prototype.

Kod: Zaznacz cały
console.log(
    Object.getPrototypeOf(Rectangle.prototype) === Object.prototype // true
);

Object.prototype jest końcowym elementem łańcucha, nie ma już swojego bazowego prototypu.

Kod: Zaznacz cały
console.log(
    Object.getPrototypeOf(Object.prototype) // null
);

A co wynika z dziedziczenia? Obiekty potomne mają oprócz cech zdefiniowanych we własnych prototypach również cechy (metody) typów nadrzędnych. Dla przykładu obiekty Rectangle automatycznie zyskują metodę toString zdefiniowaną w prototypie Object.prototype.

Kod: Zaznacz cały
var rect = new Rectangle(20, 10);
console.log(rect.toString()); // "[object Rectangle]"

Jak dziedziczyć z własnych typów?

Załóżmy, że chcemy zdefiniować sobie typ Square, który będzie dziedziczył z Rectangle. Najpierw musimy utworzyć konstruktora.

Kod: Zaznacz cały
function Square (size) {}

a następnie zdefiniować prototyp, który został zbudowany na bazie prototypu Rectangle.

Kod: Zaznacz cały
Square.prototype = Object.create(Rectangle.prototype);

Ale to nie wszystko, ponieważ

Kod: Zaznacz cały
var square = new Square(10);
console.log(square.area()); // NaN
console.log(
    square.width, // undefined
    square.height // undefined
);

Okazuje się, że szerokość i wysokość są niezdefiniowane. Musimy jeszcze w którymś momencie wywołać konstruktor Rectangle gdy tworzymy nową instancję Square.

Kod: Zaznacz cały
function Square (size) {
    Rectangle.call(this, size, size);
}

Dla testu sprawdzimy, czy wszystko działa

Kod: Zaznacz cały
var square = new Square(10);
console.log(
    square.area(), // 100
    square instanceof Square, // true
    square instanceof Rectangle, // true
    square instanceof Object // true
);

Wywoływanie konstruktora bez new

Skoro konstruktory są zwykłymi funkcjami, to możemy je wywołać bez towarzystwa new. Co się stanie? Otóż w przypadku natywnych konstruktorów zachowanie jest ściśle zdefiniowane w specyfikacji języka.

Przykładowo wywołanie Array() będzie równoważne z wywołaniem new Array(). Wywołując Date() otrzymamy tekstową reprezentację daty. Funkcje takie jak Boolean, Number, String, Object będą spełniały rolę konwertera typów.

Kod: Zaznacz cały
var str = "3.14";
var num = Number(str);
var num2 = new Number(str);
var date = new Date();
var date2 = Date();

console.log(
    typeof str, // "string"
    typeof num, // "number"
    typeof num2, // "object"
    typeof num2.valueOf(), // "number"
    typeof date, // "object"
    typeof date2 // "string"
);

W przypadku własnych konstruktorów, krótko ujmując sprawę, wykonana zostanie tylko logika zawarta konstruktorze, bez całej otoczki w postaci wywołania new.

Konsekwencją jest, że jeśli konstruktor modyfikował obiekt przekazany przez new (a konstruktor Rectangle przypisuje własności width i height), to teraz zostanie zmodyfikowane to, co będzie pod this wywołania. A co będzie pod this? Z tekstu JavaScript: this wynika, że jeśli funkcja pracowała w trybie ścisłym, to thisem będzie wartość undefined, czyli otrzymamy meldunek o błędzie. Jeśli konstruktor nie pracował w trybie ścisłym, pod this trafi obiekt globalny, co jest raczej niepożądanym zachowaniem. Na szczęście konstruktory zwykle nie zwracają nic, więc zamiast obiektu, który gwarantował operator new otrzymamy undefined, więc mamy szansę, że błąd jeszcze w miarę szybko wychwycimy.

Czy można jakoś temu zapobiec?

Jeśli chcemy zapobiec niekontrolowanemu wyciekowi informacji do obiektu globalnego, to wystarczy użyć trybu ścisłego.

Kod: Zaznacz cały
function Rectangle (width, height) {
    "use strict";

    this.width = width;
    this.height = height;
};

var rect = Rectangle(20, 10); // TypeError: this is undefined

A jeśli chcemy zezwolić na korzystanie z konstruktora bez new, można pokusić się o sprawdzenie, czy obiekt zdefiniowany w this ma określony prototyp.

Kod: Zaznacz cały
function Rectangle (width, height) {
    "use strict";

    if (this instanceof Rectangle === false) {
        return new Rectangle(width, height);
    }
    
    this.width = width;
    this.height = height;
};

Masz pytania lub wątpliwości? Odwiedź nasze forum dyskusyjne.

Rafał Kukawski

Programista, webmaster. Szczególnie upodobał sobie JavaScript i technologie klienckie, choć strona serwera i bazy danych nie stanowią tajemnicy. Tworzy też aplikacje na urządzenia mobilne. kukawski.pl.


Komentarze


HTML CSS JavaScript PHP bazy danych MySQL Flash grafika framework hosting domeny pozycjonowanie wordpress Facebook