niedziela, 30 czerwca 2019

JavaScript built-in functionality - be careful and read docs

JavaScript has a lot of very useful built-in functions, like isNaN(), parseInt(), isFinite() and so on. Those functions are simple and single purpose pieces of code used very often.

The isNaN() function takes only one argument - the value which should be checked and returns a boolean - true or false. The only purpose is to check if the value is NaN - not a number value which could be returned by functions parsing numbers or by arithmetical operators. There is no other way to check if the value is NaN, so this function is very important.

There is also other 'is' function - isFinite() which determines whether the passed value is a finite number. Also very important as some of the arithmetic operators can return Infinity as a result of the operation and then isNaN() returns false. It also takes only one argument - the value to be checked and returns boolean.

Another useful single purpose function is parseInt(). It checks provided string if it could be the number and returns the number or NaN.

As there are more than one numeral systems, this function accepts the radix as a second argument. There is NO DEFAULT value for radix. If radix is falsy then the value itself is taken in consideration. Because we can use Number(string) to determine whether the string is a number or not parseInt() works in a slighty different way. In general it checks if the string starts with the number and returns this number till first non-numerical value.

parseInt('1', 10); // 1
parseInt('1abc', 10); // 1
parseInt('1f0', 10); // 1

parseInt('1f0', 16); // 496

The parseFloat() built-in function works the same way, except it accepts only one argument - the value, as floats are decimals, so the numeral system is already known.

Both parseX functions are documented that the value should be a string but any value type works properly.


Array.prototype.map is a built-in Array method. It takes 2 arguments: a callback function and this. Returns the brand new array with the same number of elements as the array it was called upon with values produced by the callback function. Simple and beautiful.

It is very important to remember that callback function also accepts arguments. All of them are optional, as the logic for creating values for the new array could not depend on existing ones, but still the callback accepts arguments. The first one is a current value. For [1,7,11] it will be 1, then 7 and then 11. The second argument is index of the value - for 1 it will be 0, for 7 - 1 and for 11 - 2. The third argument is the array map was called upon.

[1,7,11].map(() => null); // [null, null, null]
[1,7,11].map((value) => value * 2); // [2, 14, 22]
[1,7,11].map((value, index) => value * index); // [0, 7, 22]

So what will happen if we want to use parseFloat() as a callback for map?

['1','7','11'].map(parseFloat); // [1, 7, 11]
['1.1','7.9876','11.444442'].map(parseFloat); // [1.1, 7.9876, 11.444442]

Works like a charm!

So why parseInt() doesn't work as expected?

['1','7','11'].map(parseInt); // [1, NaN, 3]

What is a problem here? Let's test the values:

parseInt('1'); // 1
parseInt('7'); // 7
parseInt('11'); // 11

It should work...

Everytime if something doesn't work as expected checking docs can save hours of tests. The second argument in parseInt() is radix and what passes map() as a second parameter to the callback? An index of the value...

So testing the values should look like that:

parseInt('1', 0); // 1

For index 0 value is '1'. The radix is falsy, so the value itself is taken in consideration. If value doesn't start with 0 decimal system is used.

parseInt('7', 1); // NaN

For index 1 value is '7'. Proper radix value is between 2 and 36. Any value parsed with 1 as a radix is NaN.

parseInt('11', 2); // 3

For index 2 value is '11'. Radix 2 means binary: 2^0 + 2^1 = 3.

So everything works just as it should. To make the callback work as expected a small change is required:

['1','7','11'].map((value) => parseInt(value, 10)); // [1, 7, 11]

It is not the newbie mistake. Such problems happen for seasoned developers, as they are not machines and don't store not used information like the order or all arguments for built-in functions. Using documentation during development process doesn't mean developer has no experience or knowledge. Using docs is a very good habit and the best possible practice which saves hours of bug fixing.

All recruitment processes which check the knowledge of docs are leading to such problems. General understanding of the language, what happens under the hood and problem solving skills and strategies (for example how to debug such bug) are more important than memorizing the documentation.

Good to read:
operacje arytmetyczne w JS [polish]
parseInt() - JavaScript | MDN
Array.prototype.map() - JavaScript | MDN

poniedziałek, 4 marca 2019

OOP - polimorfizm

Na wielu rozmowach kwalifikacyjnych, poza pytaniami o sam JavaScript padają pytania z ogólnej wiedzy o programowaniu. Czasami o best practices, czasami twarda wiedza, którą, jakby nie było trzeba znać, choć niektóre jej aspekty w JS są zupełnie nieużyteczne.

W zależności od doświadczenia osób pytających, można natrafić na przykład o pytanie czym jest polimorfizm i jak się go implementuje w JS. Polimorfizm łączy się z nadpisywaniem (override) metod obiektu i ich przeładowaniem (overload).

Obecnie (bo w podręcznikach z których ja się uczyłam, tego podziału nie było) dzieli się polimorfizm, ze względu na implementację, na dynamiczny i statyczny. W JS można zaimplementować polimorfizm dymamiczny i dość często się go stosuje (choć mogę się założyć, że większość programistów nie ma pojęcia, że to co robią ma taką nazwę). Jednak nie do końca on wygląda jak w definicji, choćby dlatego, że JS nie implementuje metod i klas abstrakcyjnych ani interfejsów. Do tego nie ma w nim twardego typowania. I bardzo dobrze, bo JS to nie Java czy C++. Jak ktoś nie umie żyć bez tych wynalazków, to jest mnostwo języków kompilowanych, które je oferują.

Zgodnie z definicją - polimorfizm zakłada, że obiekty różnych klas korzystają z z jednej konkretnej metody. Załóżmy, że mamy samolot, pyłek, ptaka i muchę. Te 4 obiekty mają jedną wspólną właściwość - latają. Nieważne jak, ale latają. Załóżmy, że mamy funkcję, która oczekuje na "latające" obiekty i jedyne co robi, to wywołuje na nich metodę lataj. W Javie, zazwyczaj się to implementuje za pośrednictwem interfejsu. Interfejs wymusza implementację konkretnych metod. W JS w komentarzach do prototypu lub klasy musimy napisać, że dana metoda musi być implementowana. Można również zaimplementować daną metodę by default, żeby rzucała wyjątek i przy dziedziczeniu nadpisywać ją tak, żeby przy używaniu jej w obiekcie, który umie latać nie było błędu

.

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