piątek, 22 lutego 2019

świadome operacje arytmetyczne na zmiennych w JS

NaN

Każdy szanujący się programista JS wie, że jeśli chce wykonać operację arytmetyczną na dwóch zmiennych i co najmniej jedna z nich jest undefined lub obiektem, to wynikiem takiego działania będzie NaN.

1 + undefined // NaN
undefined / 2 // NaN
3 * {} // NaN

Tak samo będzie, jeśli operacja nie jest dodawaniem i żadnej z liczb nie da się sparsować do liczby (parseInt lub parseFloat).

1 - 'jeden' // NaN
'dwa' / 1 // NaN

Infinity

dzielenie przez 0 lub przez null daje już inny wynik - Infinity.

3 / null // Infinity
4 / 0 // Infinity

Wartości logiczne

Wartości logiczne (true i false) są traktowane w operacjach arytmetycznych jak... 1 i 0...

3 / false // Infinity
3 / true // 3
8 * false // 0
8 * true // 8

Niebezpieczny string

Dodawanie, w którym jedną z wartości jest string jest zamieniane na łączenie stringów. Druga wartość, nieważne, jaką ma oryginalną wartość, też jest transformowana do stringa

1 + '1' // "11"
1 + 'dwa' // "1dwa"
null + 'undefined' // "nullundefined"

ale, każda inna operacja arytmetyczna już działa inaczej

3 * '4' // 12

Tablice

Jeśli operacja arytmetyczna jest dodawaniem, to wszystko co między nawiasami kwadratowymi jest traktowane, jak string. Literalnie wszystko.

2 + [3] // "23"
2 + [3, 4] // "23,4"

Zagnieżdżanie struktur nic nie daje:

2 + [3, [4, 'aaa', true, null, { a: 1}]] // "23,4,aaa,true,,[object Object]"

Jeśli chcemy wykonać operację arytmetyczną inną niż dodawanie i co najmniej jedna ze zmniennych będzie tablicą, to sprawa się komplikuje. Jeśli tablica ma tylko jeden element i jego wartość jest typem prostym, to ta wartość jest brana pod uwagę w operacji arytmetycznej.

2 * [3] // 6
2 * ['4'] // 8
3 / [0] // Infinity
['cztery'] * 2 // NaN

Przy operacji arytmetycznej innej niż dodawanie, jeśli tablica ma więcej elementów, to wynik jest zawsze NaN.

wyjątki:

[true] - 1 zwraca NaN chociaż true - 1 daje 0.
3 / [] zwraca Infinity choć [][0] daje undefined

Jak się chronić przed błędami?

Jeśli operacje arytmetyczne mają dużą wagę i muszą być wykonywane w JS jedynym działającym sposobem jest sprawdzanie typu zmiennych na których dokonujemy operacji.

Funkcje typu isFinite lub isNaN są niestety ułomne.

isFinite(true) // true
isNaN('') // false

Jeśli czekamy na liczby, które nie są wynikiem innej operacji arytmetycznej operator typeof załatwia sprawę

typeof true === 'number' // false
typeof undefined === 'number' // false
typeof null === 'number' // false
typeof '1' === 'number' // false

Jeśli natomiast wchodzące dane są wynikiem innych operacji arytmetycznych (czyli ich wartość moze być NaN), to trzeba pamiętać, że:

typeof NaN === 'number' // true

więc należy sprawdzić, czy to co wchodzi nie jest NaN

isNaN(NaN) // true

poniedziałek, 13 listopada 2017

Domknięcie (closure), co to jest i z czym to się je?

Na stronie MDN jest taka definicja A closure is the combination of a function and the lexical environment within which that function was declared. Pamiętam, jak przeczytałam ją pierwszy raz. Jak to mawiała moja mama "mózg mi stanął w poprzek". Każde słowo z osobna wydawało mi się co najmniej znajome, nawet jeśli nie w 100% zrozumiałe. Ale wszystkie do kupy powodowały, że mi się odechciewało.

To było wiele lat temu. Wiele lat, w trakcie których wciąż od nowa odkrywałam definicję domknięcia. Wczoraj robiłam proste zadanko, podpięcie pod kolumnę z danymi numerycznymi sortowania po liczbach a nie alfabetycznie (żeby 70 było mniejsze niż 320). Metodę napisałam dość banalną:

function sort(a, b, order) {
  return order === 'desc' ?
    parseFloat(a.balance) - parseFloat(b.balance) :
    parseFloat(b.balance) - parseFloat(a.balance);
};

Zadowolona z wyniku wzięłam się za testowanie. Już po chwili odkryłam, że poza kolumną opisaną w zadaniu, w tabeli jest więcej sortowalnych kolumn z danymi numerycznymi. Moja metoda, która świetnie spełniała swoje zadanie, dla jednej konkretnej kolumny, nie będzie działać, dla dowolnej innej kolumny, ponieważ sortuję wartości ukryte we właściwości balance (a.balance i b.balance). Dla innych kolumn potrzebowałam bardziej elastycznego rozwiązania.

Najłatwiej byłoby dodać nazwę kolumny, do argumentów funkcji. Ale interface funkcji przewidywał 3 konkretne argumenty: pierwszą wartość do sortowania, drugą wartość do sortowania oraz kierunek sortowania. I tu właśnie na pomoc przyszło mi domknięcie.

Ponieważ nie mogę przekazać do mojej funkcji nazwy właściwości, która przechowuje wartość do sortowania, muszę sprawić, żeby moja funkcja została stworzona w środowisku, które już zna tę nazwę. Ten przykład jasno pokazuje o co mi chodzi:

var name = 'Ania';

function greeting() {
  console.log('hello ' + name);
}

Wewnątrz funkcji greeting użyta jest zmienna name utworzona na zewnątrz funkcji. Tego typu działanie nazywa się scope, wszystko to co jest stworzone na zewnątrz funkcji jest widoczne wewnątrz, ale jeśli coś zostało stworzone wewnątrz (z użyciem któregoś ze słów kluczowych var, let lub const) nie jest widoczne na zewnątrz.

Czyli potrzebuję stworzyć środowisko, w którym znana będzie nazwa właściwości która przechowuje wartość do sortowania. Do tego środowiska będę przekazywać tę nazwę (w każdej kolumnie będę korzystać z innej właściwości):

function sortByPropName(propName) {}

Środowisko już jest, wnętrze funkcji będzie znało wartość przekazanego tam argumentu propName, jednak ja do tabeli mam przekazać funkcję, która bedzie przyjmować 3 konkretne argumenty.

function sortByPropName(propName) {
  return function(a, b, order) {}
}

Coś zaczyna z tego wychodzić. Mam już funkcję z potrzebnymi danymi, która zwraca inną funkcję, która oczekuje argumenty wyspecyfikowane w interfejsie. Teraz wystarczy dodać samą logikę, która uwzględnia przekazaną nazwę właściwości przechowujacej wartość do sortowania:

function sortByPropName(propName) {
  return function(a, b, order) {
    return order === 'desc' ?
      parseFloat(a[propName]) - parseFloat(b[propName]) :
      parseFloat(b[propName]) - parseFloat(a[propName]);
  }
}

Wygląda nieźle, prawda? To teraz wystarczy przekazać wynik sortByPropName jako funkcję sortujacą dla kolumny (kod jsx):

<TableHeaderColumn dataField="balance" dataFormat={signedNumber} dataSort sortFunc={sortByPropName('balance')}>Balance</TableHeaderColumn>
<TableHeaderColumn dataField="total_amount" dataFormat={signedNumber} dataSort sortFunc={sortByNumericValue('total_amount')}>Total amount</TableHeaderColumn>

Powyższy kod używa HTML wewnątrz JS, ale nie jak ciągu tekstowego, a bardziej jak obiektów XML. To co jest najważniejsze, to sortFunc, do której przekazuję WYNIK działania sortByPropName. sortFunc oczekuje na funkcję sortującą, która będzie przyjmowała 3 argumenty, czyli dokładnie to, co zwraca sortByPropName.

A gdzie w tym wszystkim jest domknięcie?

Dokładnie w tym, co zwraca sortByPropName. Wartość argumentu propName jest znana tylko wewnątrz funkcji, gdzie jest tworzona kolejna funkcja, która z tej wartości korzysta. Tę funkcję zwracamy na zewnątrz, gdzie jest przypisana do nazwy sortFunc i wykorzystana wewnątrz komponentu TableHeaderColumn. Pomimo, że sortFunc jest używana w zupełnie innym środowisku, ma dostęp do zmiennych ze środowiska, w którym została stworzona. To jest właśnie domknięcie.