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 foo obiektem. 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ć.
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 = {
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.
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();