Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
207 views

Every Crazy Thing JavaScript Does

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
207 views

Every Crazy Thing JavaScript Does

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 30

Every Crazy Thing JavaScript

Does
Coding Beauty
Introduction
JavaScript is a great language with a simple and readable syntax. It has a
large ecosystem and a great community. It is used by companies big and
small to create functional applications for a wide variety of platforms.

Still, we can’t deny that JavaScript has its quirkiness and weird parts. There
are subtle caveats you need to watch out for, to avoid seemingly invisible
bugs in places you least expect.

Hopefully, you’ll learn new and interesting things about the language by the
time you get to the end of this book.
An Array is NOT an Array
Programming languages follow the rules of logic, right? For instance, the law
of identity

A = A

definitely holds true in JavaScript:

2 == 2; // -> 4
5.5 == 5.5; // -> true

So what happens when we compare [] to []?

[] == []; // -> false

How can this be?

Explanation

Arrays work by reference, so we’re actually comparing the memory


addresses of the arrays, not that content. We instantiated two empty arrays, so
they will be stored in different locations in memory. For the result of this
comparison to be true the operands on both sides would have stored a
reference to the same object:

let arr1 = [];


let arr2 = arr1;
arr1 == arr2; // -> true

So now we know that [] == [] is false. Surely, [] != [] should be true


right? Let’s try it:

[] != []; // -> true

This works as expected.


But have a look at this:

[] == ![]; // -> true

An array is not an array!

Explanation

The abstract equality (==) by design attempts to convert both sides to


numbers before comparing, and both sides of the equality become 0 in the
conversion. Arrays are truthy, so on the right side, the opposite of a truthy
value is false, which is then coerced to 0. On the left side however, the
empty array is coerced to a number without becoming a boolean first, and
empty arrays are coerced to 0, even though they are truthy.

Here’s how the expression simplifies;

[] == ![];
Number([]) == Number(![]);
Number([]) == Number(false);
0 == 0;
true;

This is why you should always use strict inequality, to avoid surprises.

[] === ![]; // false


Banana
('b' + 'a' + + 'a' + 'a').toLowerCase() // -> banana

Explanation

The unary operator (+) has higher precedence than the addition operator (+)
so it is evaluated first in the expression. Applying the unary operator on a
string like a results in NaN, which is then coerced into a string for the
concatenation.

('b' + 'a' + + 'a' + 'a').toLowerCase()


('b' + 'a' + NaN + 'a').toLowerCase()
('baNaNa').toLowerCase()
banana
Is sort() Broken?
As its name implies, the Array sort() method sorts an array:

[7, 4, 2].sort() // -> [2, 4, 7]


[5, 3, 8].sort() // -> [3, 5, 8]

It sorts the numbers as expected. Until:

[10, 1, 4].sort() // -> [1, 10, 3]

A bug in the sort() method! Or is it?

Explanation

By default, the sort() method sorts by converting the elements into strings,
then comparing their sequences of UTF-16 code unit values. Both 1 and 10
start with the same character, but 10 has a greater length, so is regarded as
the greater value. But 1 has a greater UTF-16 code unit than 4, so both 1 and
10 come before 4 in the sorted array.

To sort the numbers by their numeric value, we’ll need to pass a compareFn
to the sort() method:

[10, 1, 4].sort((a, b) => a - b); // -> [1, 4, 10]


A Simple Math Quiz for JavaScript
Adding decimal numbers with a moderate amount of digits is pretty easy for
most of us. 0.1 + 0.1? 0.2. 0.2 + 0.2? 0.4 of course. JavaScript agrees:

0.1 + 0.1 === 0.2 // -> true


0.2 + 0.2 === 0.4 // -> true

And we all know 0.1 + 0.2 is 0.3. JavaScript doesn’t think so, however:

0.1 + 0.2 === 0.3 // -> false

So what could 0.1 + 0.2 actually be? According to JavaScript:

0.1 + 0.2 // -> 0.30000000000000004

And if we multiplied by 2:

(0.1 + 0.2) * 2 // -> 0.6000000000000001

Explanation

In languages that use floating-point math, not all decimal numbers can be
exactly represented. As it is a binary system, only fractions whose
denominator is a power of 2 can be expressed accurately. Values like 0.1
and 0.2 (1/10 and 1/5) would have repeating decimals in their internal
representation. When you perform math on these repeating decimals, there
will leftovers which carry over in the conversion of the number from the
computer’s base-2 binary representation into a more readable base-10
representation.
Object.is() vs ===
Object.is() checks if two values have the same value or not. It works like
the === operator, except for some weird cases:

Object.is(NaN, NaN); // -> true


NaN === NaN; // -> false

Object.is(-0, 0); // -> false


-0 === 0; // -> true

Object.is(NaN, 0 / 0); // -> true


NaN === 0 / 0; // -> false

Explanation

In JavaScript, NaN and NaN are the same value, but they’re not strictly equal.

In the same way, -0 and 0 are strictly equal, but they’re not the same value.
Plus and Minus
Testing the minus operator (-) here:

'6' - 2 // -> 4

JavaScript understands that the string '6' can be the number 6. However, if
we try something similar with the plus operator (+):

'6' + 2 // -> '62'

Explanation

When we try to use the + operator String and a Number, JavaScript sees it
as a concatenation operator, instead of a numeric addition. So it converts the
Number (2 here) into a `String`` and performs the concatenation.

To perform numeric addition involving a String, convert it to a Number


first:

Number('6') + 2 // -> 8
Strange parseInt() Behaviour
Sometimes parseInt() behaves in ways you might not expect:

parseInt('f*ck'); // -> NaN


parseInt('f*ck', 16) // -> 15

Explanation

parseInt() parses each character of the string one after the other until it
meets a character that doesn’t exist in the radix. In the first call to
parseInt(), the default base of 10 is used and the first character (f) does
not exist in this base, so it returns NaN right away. In the second call, the base
is 16, so f is parsed to 15, before the function stops when it hits the *
character.

This is also why parseInt() parses Infinity to quite different values in


various bases:

// ...
parseInt('Infinity', 10); // -> NaN
// ...
parseInt('Infinity', 18); // -> NaN
parseInt('Infinity', 19); // -> 18
// ...
parseInt('Infinity', 23); // -> 18
parseInt('Infinity', 24); // -> 151176378
// ...
parseInt('Infinity', 29); // -> 385849803
parseInt('Infinity', 30); // -> 13693557269
// ...
parseInt('Infinity', 34); // -> 28872273981
parseInt('Infinity', 35); // -> 1201203301724
parseInt('Infinity', 36); // -> 1461559270678
parseInt('Infinity', 37); // -> NaN

Watch out when you’re parsing null:


parseInt(null, 24) // -> 23
parseInt(null, 31) // -> 714695
parseInt(null, 37) // -> NaN

Explanation

null is coerced to the string 'null' before the parsing. This string has no
characters that existing from base 0 to base 23, so parseInt() returns NaN
for them. Starting from base 24, the letter n is used in the numeral system and
it is parsed to 23 in this base. Starting from base 31, the letter u is used in the
numeral system and the entire string can be parsed. None of the characters of
'null' exist in base 37, so parseInt() returns NaN for this base.

Be careful when parsing strings that start with 0:

parseInt('06');
parseInt('08'); // ES5+
parseInt('08'); // Before ES5

Explanation
If the input string starts with 0, the base might default to 8 or 10, depending
on the JavaScript version. ECMAScript 5 specifies that base 10 should be
used, but not all browsers support it yet. This is why you should always
explicitly set a base when using parseInt().

parseInt() always converts its input to a string with toString():

parseInt({ toString: () => 5, valueOf: () => 8 }); // -> 5

This is different from other methods like the Number() constructor, which
uses the valueOf() method to convert the string:

Number({ toString: () => 5, valueOf: () => 8 }); // -> 8

parseInt() can also behave wierdly when parsing floating point values:

parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5

Explanation
parseInt() takes a string argument and returns an integer of the specified
base. parseInt() also stops parsing once it meets a non-digit character in
the string. So 0.000001 is coerced to the string '0.000001', and
parseInt() stops parsing once it meets the period, leaving only the string
'0', which gets converted to the number 0. 0.0000001 is interpreted as 1e-
7, which is the equivalent scientific notation . parseInt() stops parsing the
string '1e-7' at the non-digit character 'e', leaving only the string '1' to be
converted. A similar thing happens for the last statement, where 1 /
1999999 is interpreted as 5.00000250000125e-7 which results in 5 when
passed to parseInt().
parseInt() and map()
Using the Array map() method and parseInt() can produce interesting
results:

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

Explanation

Here’s the syntax for the Array map() method:

map((element, index, array) => { /* ... */ }, thisArg);

map() passes three arguments in its callback function.

Here’s the syntax for parseInt():

parseInt(string, radix);

So this is what is happening:

['1', '6', '11'].map((value, index, arr) =>


parseInt(value, index, arr)
);

Because we passed parseInt() to the map() method directly, it parses each


array element using the index of the element as the radix.

parseInt('1', 0, ['1', '6', '11']) // -> 1


parseInt('6', 1, ['1', '6', '11']) // -> NaN
parseInt('11', 2, ['1', '6', '11']) // -> 3

To ensure parseInt() only gets a single argument and properly parses each
string to a number:

['1', '6', '11'].map((str) => parseInt(str)) // -> [1, 6, 11]


var vs let
Using var can lead to subtle bugs, like in this example:

function func() {
var num = 10;
if (true) {
var num = 20;
console.log(num); // 20
}
console.log(num); // 20
}

func();

Explanation

Both the var and let keywords in JavaScript are used to declare a variable,
but there is an important difference between them that you should be aware
of. A variable declared with var exists in the entire scope of the innermost
function where it is declared, while one declared with let exists only in the
innermost block where it is declared.

With var, num exists everywhere within this function:

function func() {
{
var num = 10;
}
console.log(num); // 10
}

func();

But with let, it doesn’t exist outside the innermost enclosing block:
function func() {
{
let num = 10;
}
console.log(num); // ReferenceError: num is not defined
}

func();

So creating a new num variable in the body of the if statement with var
unexpectedly modified the num variable that we declared in the outer scope.
Using let prevents this sort of issue:

function func() {
let num = 10;
if (true) {
let num = 20;
console.log(num); // 20
}
console.log(num); // 10
}

func();

Closures in Loops
Creating closures in loops that access var variables can produce strange
results.

Example
var funcs = [];
// create 3 functions
for (var i = 0; i < 3; i++) {
// store them in funcs
funcs[i] = function () {
// each logs a value
console.log('My value: ' + i);
};
}
// Run each function to test
for (var j = 0; j < 3; j++) {
funcs[j]();
}

Output
My value: 3
My value: 3
My value: 3

Explanation
In JavaScript, a closure “remembers” the value of any variable existing
outside the closure, even if the variable changes:

var num = 1;
var closure = function () {
console.log(num);
};
num = 3;
closure(); // 3

Since we used var to declare the loop counter (i), it exists outside the for
loop scope, and all the closures added to the funcs array are each bound to
the same i variable.

We can use let in place of var in the for loop to make the code work as
expected:

Example
for (let i = 0; i < 3; i++) {
// store them in funcs
funcs[i] = function () {
// each logs a value
console.log('My value: ' + i);
};
}
Output
My value: 0
My value: 1
My value: 2
Insane Writing
JavaScript offers an interesting way to print letters and numbers:

(![] + [] + [][[]])[+!![] + [+[]]] +


([][[]] + [])[+!![]] +
(![] + [])[!+[] + !![] + !![]] +
(![] + [])[+!![]] +
([][[]] + [])[+!![]] +
([] + {})[+!![] + [+!![]]]
// -> 'insane'

Explanation

Breaking down the symbol units:

![]; // -> false


!![] // -> true

This is because arrays are truthy and the ! operator coerces the empty array
to the boolean false. We can convert this Boolean to a string by adding []
to it:

![] + []; // -> 'false'

This happens because the right operand is coerced to a string due to some
internal function calls (binary + operator -> ToPrimitive ->
[[DefaultValue]].)

Also:

+!![] // -> 1

Because the unary plus operator (+) coerces the Boolean to a Number (true
to 1).
By combining a few JavaScript symbols in different ways, we can generate
strings to output almost any word without explicitly using string literals in the
code.

+![] // -> 0
+!![] // -> 1
!![] // -> true
![] // -> false
[][[]] // -> undefined
+!![] / +![] // -> Infinity
[] + {} // -> '[object Object]'
+{} // -> NaN

So the code in the example evaluates like this:

('false' + 'undefined')[10] +
'undefined'[1] +
'false'[3] +
'false'[1]
+ 'undefined'[1] +
'[object Object]'[11]

'i' +
'n' +
's' +
'a' +
'n' +
'e'

'insane'
The null Object
typeof []; // -> 'object'
typeof null; // -> 'object'

// however
null instanceof Object; // false

Explanation

The behaviour of the typeof operator is defined in this section of the


specification:

13.5.3 The typeof Operator

According to the specification, the typeof operator returns a string


according to the Table 37: typeof Operator Results. For null and objects
that don’t implement [[Call]], it returns 'object'.

Tip

You can check for the type of an object with the toString method:

// -> '[object Array]'


Object.prototype.toString.call([]);

// -> '[object Date]'


Object.prototype.toString.call(new Date());

// -> '[object Null]'


Object.prototype.toString.call(null);
Magically Increasing Numbers
999999999999999; // -> 999999999999999
9999999999999999; // -> 10000000000000000

10000000000000000; // -> 10000000000000000


10000000000000000 + 1; // -> 10000000000000000
10000000000000000 + 1.1; // -> 10000000000000002

Explanation

This is caused by IEEE 754-2008 standard for Binary Floating-Point


Arithmetic. At this scale, it rounds to the nearest even number. Read more:

6.1.6 The Number Type from the specification.


IEEE 754 on Wikipedia.
Minimum Value Greater than Zero
Number.MIN_VALUE is supposed to be the smallest number in JavaScript, but
yet:

Number.MIN_VALUE > 0; // -> true

Explanation

Number.MIN_VALUE is 5e-324, i.e. the smallest positive number that can be


represented within float precision, i.e., that’s as close as you can get to zero.

The overall smallest value is Number.NEGATIVE_INFINITY, although it isn’t


really numeric in a strict sense.
constructor Magic
const c = 'constructor';
c[c][c]('console.log("How?")')(); // > How?

Explanation

Taking this example step-by-step:

// Declare a new constant which is a string 'constructor'


const c = 'constructor';

// c is a string
c; // -> 'constructor'

// Access the 'constructor' property of a String object


c[c]; // -> [Function: String]

// Get a constructor of the constructor function


c[c][c]; // -> [Function: Function]

// Call the Function constructor and passing the body


// of a new function as an argument
c[c][c]('console.log("How?")'); // -> [Function: anonymous]

// And then call this anonymous function, which results in


// logging the string 'How' to the console
c[c][c]('console.log("How?")')() // > How?

The constructor property of an instance object returns a reference to the


constructor function that created the object. For strings it is String, for
numbers it is Number, and so on.

Object.prototype.constructor at MDN
19.1.3.1 Object.prototype.constructor from the specification
undefined and Number()
If we call the Number() constructor without passing any arguments, we’ll get
0. In JavaScript, undefined is passed as an argument to function parameters
when no argument is explicitly specified, so you might expect that Number()
without arguments takes undefined as a value of its parameter. In reality,
passing undefined to Number() results in NaN.

Number(); // -> 0
Number(undefined); // -> NaN

Explanation

According to the specification:

1. If no arguments were passed to this function’s invocation, let n be +0.


2. Else, let n be ToNumber(value).
3. For undefined, ToNumber(undefined) should return NaN

21.1.1 The Number Constructor, ECMAScript specification.


7.1.4 ToNumber(argument), ECMAScript specification.
Adding Arrays
What happens when you try to add two arrays in JavaScript?

[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'

Explanation

The string representations of the arrays get concatenated:

[1, 2, 3] + [4, 5, 6]

[1, 2, 3].toString() + [4, 5, 6].toString()

'1,2,3' + '4,5,6'

'1,2,34,5,6'
Valid HTML Comments
Did you know that HTML comments (starting with <!--) are valid comments
in JavaScript? Your IDE might complain, but they work:

// valid comment
<!-- valid comment too

Explanation

HTML-like comments were included to allow earlier browsers


(e.g. Netscape 1.x) that didn’t understand the <script> tag to degrade
gracefully. These browsers are not widely used anymore, so there isn’t any
reason to put such comments in script tags anymore.

Since Node.js is based on the V8 engine, HTML-like comments are


supported by the Node.js runtime too. Moreover, they’re a part of the
specification:

B.1.3 HTML-like Comments


NaN is Not a Number
The type of NaN is a ‘number’:

typeof NaN; // -> 'number'

Explanation

The details of how the typeof operator works is in the specification:

13.5.3 The typeof operator.


Double Dots
Let’s try to convert a number to a string:

15.toString(); // > Uncaught SyntaxError: Invalid or unexpect

What if we try with two dots instead?

15..toString(); // -> '15'

But why doesn’t the first example work?

Explanation

This is a language grammar limitation.

The . character can mean different things in JavaScript. It can be seen as the
member operator, or a decimal, depending on its placement.

You have to use parenthesis or an additional dot to make the expression


valid.

(15).toString(); // -> '15'


// or
15..toString(); // -> '15'
Let’s Make this Book Better
Together
We’d love to get your suggestions on how to improve this book. Send us
feedback at feedback@codingbeautydev.com

To get more helpful resources and stay updated with our latest tips and
tutorials, become a subscriber by signing up here.

You might also like