chevron-left chevron-right

[JS] Prawda czy fałsz? Sztuczki z operatorami bitowymi i logicznymi w języku JavaScript

Tworząc aplikacje internetowe, czasami pojawia się potrzeba dokonania mikro optymalizacji w szybkości działania funkcji zaimplementowanych w aplikacjach. Dzięki nim, można osiągnąć lepszą wydajność aplikacji, a także większą stabilność działania. Jedną z możliwości jakie daje nam język JavaScript (i nie tylko) jest stosowanie operatorów bitowych i operatorów logicznych, które będą sprawdzały wartości zmiennych, odpowiednio je przekształcały do pożądanej przez nas formy czy też zastępowały pętle.

Zaokrąglanie liczb za pomocą operatora NOT

W przypadku, gdy istnieje potrzeba zaokrąglania liczby w dół web developerzy stosują Math.floor(x), podczas gdy istnieją co najmniej 3 szybsze sposoby, aby osiągnąć ten sam efekt:

  • y = ~~x;,
  • y = x | 0;,
  • y = x >> 0;.

Wszystkie te 3 sposoby, są szybszymi zamiennikami popularnej metody do zaokrąglania w dół.

Sprawdzanie czy liczba jest parzysta?

Tworząc różnego listingi lub budując tabelę z dynamicznie generowaną treścią (dodawanie/odejmowanie wierszy) bardzo często stosowane są klasy definiujące czy dany wiersz jest parzysty czy nieparzysty. W takich sytuacjach często są używane sprawdzenia czy kolejna liczba jest modułem z 2 - y = x % 2; i tutaj też jest zamiennik, który jest szybszy: y = (x & 1);. Zapis ten zwraca true, jeśli wartość jest nieparzysta.

Zwracanie wartości typu boolowskiego/logicznego

Czasami web developerzy operują na zmiennych true/false i to jest w porządku. Lecz ze względu na to, że istnieją różne postaci typu fałszywego, takie jak:

  • false,
  • NaN,
  • undefined,
  • null,
  • '' (pusty string),
  • 0.

To nie mogą być one używane jako wartości właściwosci obiektów, które w założeniach przyjmują tylko wartości true lub false. W takich sytuacjach można wykorzystać zapis: y = !!x;. Gdyby x przyjął wartość undefined, to ten zapis zwróci wartość false, a w przypadku gdy wartość zmiennej będzie określona lub nie będzie typu fałszywego to będzie zwracała true, np. gdy zastosujemy taki zapis if (!!$('body').length) ....

Działania warunkowe - if

Działania warunkowe w językach programowania są widokiem tak powszechnym, że z reguły nie sprawiają żadnych problemów z ich odczytaniem. Czasami można natknąć się na tajemnicze zapisy, które zachowują się jak warunki, ale na takie nie wyglądają. Są to skrócone zapisy warunku if lub inaczej wywołania warunkowe.

1
2
3
if (true) {
  wykonaj();
}

Standardowy warunku if można zastąpić zapisem: true && wykonaj();. Jest to zapis krótszy i może być wykorzystywany, gdy zależy będzie Ci zależało na maksymalnym zmniejszeniu wagi pliku.

Inne przykłady wykorzystania wywołań warunkowych są następujące:

1
2
3
if (!true) {
  wykonaj();
}

Powyższy zapis można zastąpić następującym kodem: true || wykonaj();. Czyli, jeśli wartość nie jest prawdziwa/nie istnieje to wykonaj funkcję.

Do tej pory wykonywaliśmy działania warunkowe, które wywołują się tylko w jednym przypadku. Lecz można też utworzyć skrótową wersję warunku if else za pomocą zapisu: x && wykonaj() || nieWykonuj();. Ten sam warunek można zapisać jeszcze inaczej: x ? wykonaj() : nieWykonuj();

Jednym z przykładów z życia wziętych może być następujący kod:

1
2
3
4
5
6
7
8
var element = $('.element');
$('.element').hasClass('focus') && (function () {
  element.sibling().removeClass('active');
  $('#activity').text('Element ma klasę .focus');
  setTimeout(function () {
    wykonajAnimacje(element);
  }, 1000);
});

Jeśli element ma klasę focus, to wykonaj zawartość anonimowej funkcji. Jest to proste i efektywne rozwiązanie.

Funkcje z parametrami o wartościach domyślnych

W języku JavaScript w definicji funkcji nie możemy definiować wartości domyślnych parametrów, tak jak to ma miejsce chociażby w języku PHP. Na szczęście, istnieje inne wyjście - przypisania warunkowe.

1
2
3
4
function (active) {
  var isActive = active || false;
  ...
}

Wyżej wymieniona funkcja przyjmuje jeden parametr: active, jeśli jej wartość jest zdefiniowana, to zmienna isActive przyjmie wartość true, a jeśli nie jest zdefiniowana to zmienna przyjmie wartość domyślną (zdefiniowaną przez programistę).

Podsumowanie

Wyżej wymienione przykłady to tylko część możliwości jakie daje nam operowanie na wartościach zmiennych za pomocą operatorów bitowych i logicznych. W części przypadków znacząco to przyspiesza działanie aplikacji, a w części przypadków pozwala na redukcję wagi pliku skryptu.
Takie rozwiązania są bardzo użyteczne, ale bardzo często jest to osiągane zmniejszeniem czytelności kodu.
W większości przypadków, czytelność kodu w wersji rozwojowej jest więcej warta niż mikro optymalizacje. Dzięki czytelności kodu możemy zaoszczędzić czas poświęcony na rozwiązywanie problemów z kodem lub poświęcony na wprowadzanie nowych funkcjonalności. Warto jednak mieć na uwadze sposoby na optymalizację kodu wymienione wyżej, ponieważ mogą się przydać w krytycznych sytuacjach.

  • Comandeer

    „To nie mogą być one używane jako wartosci właściwosci obiektów, które w założeniach przyjmują tylko wartości true lub false.”
    raczej nie zgodziłbym się z tym stwierdzeniem. chyba, że myślimy o czymś innym, mówiąc „właściwości obiektów”
    poza tym jak dla mnie zapis if(!!$(‚body’).length) wydaje mi się lekką przesadą – silnik sobie to sam przekonwertuje (a na logikę: jeśli taka konwersja i tak zachodzi, to chyba nie zyskamy tutaj zbyt wiele). a minifikator i tak to sprowadzi do $(„body”).length&&akcja() 😉
    „Lecz można też utworzyć skrótową wersję pętli if else za pomocą zapisu”
    można, ale… https://github.com/Comandeer/gui/blob/master/src/js/keyboard.js#L272 musiałem być narąbany kofeiną, bo następnego dnia, gdy na to spojrzałem, nie miałem zielonego pojęcia co to robi. i do dziś nie mam. takie optymalizacje to raczej lepiej zostawić dla uglify.js

  • Tamten zapis z body nie jest może zbyt najszczęśliwszym przykładem, ale oddaje sens tego co chciałem przekazać.
    W drugim przypadku, należy pamiętać o tym, aby nie przedobrzyć. Tak jak napisałem w podsumowaniu, czytelność kodu jest ważniejsza w fazie developerskiej od mikrooptymalizacji. Dlatego trzeba mikrooptymalizacje stosować z umiarem.

  • Moze sie silnie myle, jednak mam bardzo mocne wrazenie, ze nie istnieje cos takiego jak ‚petla if else’. Jest instrukcja warunkowa.

    Petla nie powinna przypadkiem pozwalac wielokrotnie wykonywac blok kodu?

  • Dzięki za zwrócenie uwagi na tą nieścisłość. Oczywiście chodzi o zapisy warunkowe a nie o pętle.

  • maciejsmolinski

    Wszystko ok z tymi zaokrągleniami dopóki nie zaokrąglisz Infinity, e.g.

    Math.floor(Infinity) === Infinity // => true

    (~~Infinity) === Infinity // => false
    (Infinity | 0) === Infinity // => false
    (Infinity >> 0) === Infinity // => false

    W tym przypadku bitwise operators do zaokrągleń również zawiodą:

    2147483648..toString(2) // => „10000000000000000000000000000000”

    Math.floor(2147483648) === 2147483648 // => true

    (~~2147483648) === 2147483648 // => false
    (2147483648 | 0) === 2147483648 // => false
    (2147483648 >> 0) === 2147483648 // => false

    Warto mieć to na uwadze 😉 Moim zdaniem to mikrooptymalizacje wiodące jedynie do kodu, który jest mniej czytelny i trudniejszy w utrzymaniu. Bardzo ciekawe wykorzystanie bitwise operators to np. variables swap (http://michalbe.blogspot.co.uk/2013/03/javascript-less-known-parts-bitwise.html), ale ponownie komplikuje to niepotrzebnie kod. Dopóki nie zależy nam na świetnym performance (np. obsługa grafiki 3d), to zwykłe Math.floor doskonale spełni swoją rolę.

    Pozdrawiam,
    Maćko