Na czym polega chaining w JavaScript?
Implementacja kalkulatora oraz własnej biblioteki DOM, które działałyby na zasadzie chainingu,
czyli uruchamianiu metod po metodzie na danym obiekcie, po kropce, podobnie jak w jQuery.
W przypadku kalkulatora mogłoby to wyglądać np. tak:
czysty tekst
1.
calculator.add(2).multiply(3).subtract(3).getResult(); // 3
Działania wykonywane byłyby od lewej, nie zwracalibyśmy również uwagi na kolejność (a więc mnożenie
byłoby równe dodawaniu). Zatem najpierw dodajemy do zera dwójkę, potem mnożymy to przez trzy, a na
końcu odejmujemy trójkę. Wynikiem jest… trzy.
W wypadku biblioteczki DOMowej, przykładowe jej użycie mogłoby wyglądać następująco:
czysty tekst
1.
$("div > p").css("margin-left", "20px").click(function() {
2.
console.log("klik!");
3.
});
Skupmy się jednak na kalkulatorze.
Przed przystąpieniem do implementacji zazwyczaj pojawia się wiele wątpliwości. Po pierwsze należy
zauważyć, analizując nasz przykład, że najpierw uruchamia się funkcja
add
, potem
multiply
,
następnie
subtract
, a na końcu
getResult
. Wszystko dzieje się po kropce.
Warto więc przypomnieć sobie, co oznacza konstrukcja
foo.bar()
, czyli dwie części kodu oddzielone
kropką. Otóż używając takiego kawałka kodu spodziewamy się, że bar jest funkcją (w końcu mamy do
czynienia z nawiasami, więc zapewne chcemy uruchomić jakąś funkcję), a fooobiektem. Otóż tylko wtedy
taka konstrukcja ma sens – po prawej stronie kropki powinna być funkcja, a po lewej obiekt zawierający
taką funkcję. Kod mógłby wyglądać więc tak:
czysty tekst
1.
var foo = { bar: function() { return "test"; } };
2.
foo.bar(); // test
W naszym z kolei przypadku mamy kilka uruchomień funkcji po kropce. Zgadujemy więc, że to co będzie
z lewej musi zwracać jakiś obiekt, zawierający metodę z prawej. A więc po
odpaleniu
calculator.add(2)
spodziewamy się, że
calculator
będzie obiektem zawierającym
metodę
add
. Następnie po uruchomieniu
calculator.add(2).multiply(3)
chcemy, by to, co
zwróci wywołanie
calculator.add(2)
, zawierało metodę
multiply
, ponieważ JS napotkał kropkę,
co znaczy, że z lewej ma znaleźć się obiekt, a z prawej funkcja bądź inna własność z tego obiektu. I tak
dalej. A więc każde wywołanie funkcji kończące się kropką ma zwrócić jakiś obiekt, by kod mógł
zadziałać.
Rozpocznijmy implementację:
czysty tekst
1.
var calculator = {};
Tworzymy obiekt calculator, od którego wszystko się zacznie. Taki obiekt ma metodę add, w końcu nasz
przykład wygląda tak:
czysty tekst
1.
calculator.add(2).multiply(3).subtract(3).getResult(); // 3
Piszemy!
czysty tekst
1.
var calculator = {
2.
add: function(n) {
3.
}
4.
};
Nic prostszego, prawda?
Używamy tego tak:
czysty tekst
1.
calculator.add(2);
Ale my chcemy mieć więcej możliwości, chcemy teraz pomnożyć wszystko przez trzy:
czysty tekst
1.
calculator.add(2).multiply(3)
Wykorzystując obecny, skromny kod, dostajemy błąd. Nic dziwnego – to, co zwraca metoda
add
, nie
zawiera funkcji
multiply
. Naprawmy nasz błąd:
czysty tekst
1.
var calculator = {
2.
add: function(n) {
3.
return {
4.
multiply: function(n2) {
5.
}
6.
};
7.
}
8.
};
Wszystko działa, tylko co, gdyby po add nie było multiply, a na przykład odejmowanie lub kolejne
dodawanie? Pisalibyśmy takie podobiekty w nieskończoność. Zdefiniujmy więc najpierw proste API, które
będzie implementował kalkulator.
czysty tekst
1.
var calculator = {
2.
add: function() {},
3.
subtract: function() {},
4.
multiply: function() {},
5.
divide: function() {}
6.
};
Wszystko fajnie. Brakuje tylko miejsca na przechowywanie wyniku:
czysty tekst
1.
var calculator = {
2.
add: function() {},
3.
subtract: function() {},
4.
multiply: function() {},
5.
divide: function() {},
6.
result: 0
7.
};
Aktualny rezultat naszych operacji składowany jest w
calculator.result
. Początkowa jego wartość
to 0. Uzupełnijmy teraz metody:
czysty tekst
1.
var calculator = {
2.
add: function(n) {
3.
if (typeof n === "number") {
4.
calculator.result += n;
5.
}
6.
},
7.
subtract: function(n) {
8.
if (typeof n === "number") {
9.
calculator.result -= n;
10.
}
11.
},
12.
multiply: function(n) {
13.
if (typeof n === "number") {
14.
calculator.result *= n;
15.
}
16.
},
17.
divide: function(n) {
18.
if (typeof n === "number" && n !== 0) {
19.
calculator.result /= n;
20.
}
21.
},
22.
getResult: function() { return calculator.result; },
23.
result: 0
24.
};
W każdej z nich sprawdzamy, czy podana liczba jest rzeczywiście typy liczbowego (
typeof number
).
Dodatkowo w dzieleniu
divide
nie chcemy dzielić przez zero.
Wszystko pięknie, tylko nadal nie działa nam zapis metod po kropce. Jak wiemy, aby uruchomić metodę
poprzedzoną kropką, musi być ona składnikiem obiektu z lewej strony. A więc każda z naszych funkcji
musi zwracać obiekt z tymi czteroma metodami.
czysty tekst
1.
var calculator = {
2.
add: function(n) {
3.
if (typeof n === "number") {
4.
calculator.result += n;
5.
}
6.
return calculator;
7.
},
8.
subtract: function(n) {
9.
if (typeof n === "number") {
10.
calculator.result -= n;
11.
}
12.
return calculator;
13.
},
14.
multiply: function(n) {
15.
if (typeof n === "number") {
16.
calculator.result *= n;
17.
}
18.
return calculator;
19.
},
20.
divide: function(n) {
21.
if (typeof n === "number" && n !== 0) {
22.
calculator.result /= n;
23.
}
24.
return calculator;
25.
},
26.
getResult: function() { return calculator.result; },
27.
result: 0
28.
};
Jak łatwo zauważyć, każda z nich zwraca obiekt, do którego należy – a więc możemy być pewni, że będzie
zawierał on wszystkie z interesujących nas funkcji, w końcu zwracamy cały kalkulator.
Pozostaje jednak jedna drażniąca kwestia. Otóż możemy łatwo ingerować w wynik – na przykład pisząc
tak:
czysty tekst
1.
calculator.add(2).add(3);
2.
calculator.result = 10;
3.
calculator.getResult(); // 10
Byłoby bardzo miło ukryć dostęp do zmiennej przechowującej tymczasowy wynik. Możemy to zrobić
korzystając z closures i wywołania funkcji anonimowej.
czysty tekst
1.
var calculator = (function() {
2.
var result = 0;
3.
var calculator = {
4.
add: function(n) {
5.
if (typeof n === "number") {
6.
result += n;
7.
}
8.
return calculator;
9.
},
10.
subtract: function(n) {
11.
if (typeof n === "number") {
12.
result -= n;
13.
}
14.
return calculator;
15.
},
16.
multiply: function(n) {
17.
if (typeof n === "number") {
18.
result *= n;
19.
}
20.
return calculator;
21.
},
22.
divide: function(n) {
23.
if (typeof n === "number" && n !== 0) {
24.
result /= n;
25.
}
26.
return calculator;
27.
},
28.
getResult: function() { return result; }
29.
};
30.
31.
return calculator;
32.
})();
Z obiektu
calculator
wydzieliliśmy
result
do oddzielnej zmiennej o tej samej nazwie. Jest ona
dostępna w scope anonimowej funkcji, a więc tylko wewnątrz:
czysty tekst
1.
var calculator = (function() {
2.
// o, tutaj
3.
})();
Wszystkie inne funkcje zdefiniowane wewnątrz niej będą miały do niej dostęp – tak działa mechanizm
closures.
Voila! Podany przykład można by rozszerzyć o
.prototype
i tak dalej, co mogłoby wspierać
dopisywanie pluginów. Można też dodać obsługę błędów. To jednak temat na następny artykuł.
Równie dobrze można skorzystać z this:
czysty tekst
1.
var calculator = (function() {
2.
var result = 0;
3.
var calculator = {
4.
add: function(n) {
5.
if (typeof n === "number") {
6.
result += n;
7.
}
8.
return this;
9.
},
10.
subtract: function(n) {
11.
if (typeof n === "number") {
12.
result -= n;
13.
}
14.
return this;
15.
},
16.
multiply: function(n) {
17.
if (typeof n === "number") {
18.
result *= n;
19.
}
20.
return this;
21.
},
22.
divide: function(n) {
23.
if (typeof n === "number" && n !== 0) {
24.
result /= n;
25.
}
26.
return this;
27.
},
28.
getResult: function() { return result; }
29.
};
30.
31.
return calculator;
32.
})();
Ryzykujemy jednak w takim przypadku:
czysty tekst
1.
var dodaj = calculator.add;
2.
dodaj(2).add(2).add(2).getResult();