JavaScript quirks (bizarreries en français)

Introduction

It took ten days for Brendan Eich to create a prototype of what later became JavaScript. After it was presented to the stakeholders at a business meeting, the language was considered production ready and didn’t go through a lot of changes for many years .

Unfortunately, that made the language infamous for its oddities.

Some people didn’t even regard JavaScript as a “real” programming language, which made it a victim of many jokes and memes.

Today, the language is much friendlier than it used to be .

Nevertheless, it’s worth knowing what to avoid, since a lot of legacy JavaScript is still out there waiting to bite you.

Bogus Array

Python’s lists and tuples are implemented as arrays in the traditional sense, whereas JavaScript’s Array type has more in common with Python’s dictionary. What’s an array, then ?

In computer science, an array is a data structure that occupies a contiguous block of memory, and whose elements are ordered and have homogeneous sizes.

This way, you can access them randomly with a numerical index.

In Python, a list is an array of pointers that are typically integer numbers, which reference heterogeneous objects scattered around in various regions of memory.

Note

For low-level arrays in Python, you might be interested in checking out the built-in array module.

JavaScript’s array is an object whose attributes happen to be numbers.

They’re not necessarily stored next to each other. However, they keep the right order during iteration.

When you delete an element from an array in JavaScript, you make a gap:

> const fruits = ['apple', 'banana', 'orange'];
> delete fruits[1];
true
> console.log(fruits);
['apple', empty, 'orange']
> fruits[1];
undefined

The array doesn’t change its size after the removal of one of its elements:

> console.log(fruits.length);
3

Conversely, you can put a new element at a distant index even though the array is much shorter:

> fruits[10] = 'watermelon';
> console.log(fruits.length);
11
> console.log(fruits);
['apple', empty, 'orange', empty × 7, 'watermelon']

This wouldn’t work in Python.

Array Sorting

Python is clever about sorting data because it can tell the difference between element types. When you sort a list of numbers, for example, it’ll put them in ascending order by default:

>>> sorted([53, 2020, 42, 1918, 7])
[7, 42, 53, 1918, 2020]

However, if you wanted to sort a list of strings, then it would magically know how to compare the elements so that they appear in lexicographical order:

>>> sorted(['lorem', 'ipsum', 'dolor', 'sit', 'amet'])
['amet', 'dolor', 'ipsum', 'lorem', 'sit']

Things get complicated when you start to mix different types:

>>> sorted([42, 'not a number'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'str' and 'int'

By now, you know that Python is a strongly typed language and doesn’t like mixing types. JavaScript, on the other hand, is the opposite.

It’ll eagerly convert elements of incompatible types according to some obscure rules .

You can use .sort() to do the sorting in JavaScript:

> ['lorem', 'ipsum', 'dolor', 'sit', 'amet'].sort();
['amet', 'dolor', 'ipsum', 'lorem', 'sit']

It turns out that sorting strings works as expected. Let’s see how it copes with numbers:

> [53, 2020, 42, 1918, 7].sort();
[1918, 2020, 42, 53, 7]

What happened here is that the array elements got implicitly converted to strings and were sorted lexicographically.

To prevent that, you have to provide your custom sorting strategy as a function of two elements to compare, for example:

> [53, 2020, 42, 1918, 7].sort((a, b) => a - b);
[7, 42, 53, 1918, 2020]

The contract between your strategy and the sorting method is that your function should return one of three values:

  • Zero when the two elements are equal

  • A positive number when elements need to be swapped

  • A negative number when the elements are in the right order

This is a common pattern present in other languages, and it was also the old way of sorting in Python.

Automatic Semicolon Insertion

At this point, you know that semicolons in JavaScript are optional because the interpreter will insert them automatically at the end of each instruction if you don’t do so yourself.

This can lead to surprising results under some circumstances:

function makePerson(name) {
  return
    ({
      fullName: name,
      createdAt: new Date()
    })
}

In this example, you might expect the JavaScript engine to insert a missing semicolon at the very end of your function, right after the closing parenthesis of the object literal.

However, when you call the function, this happens:

> const jdoe = makePerson('John Doe');
> console.log(jdoe);
undefined

Your function changed the intended action by returning an undefined because two semicolons were inserted instead of one:

function makePerson(name) {
  return;
    ({
      fullName: name,
      createdAt: new Date()
    });
}

As you can see, relying on the fact that semicolons are optional introduces some risk of errors in your code.

On the other hand, it won’t help if you start putting semicolons everywhere.

To fix this example, you need to change your code formatting so that the returned value begins on the same line as the return statement:

function makePerson(name) {
  return {
    fullName: name,
    createdAt: new Date()
  };
}

In some situations, you can’t rely on automatic semicolon insertion and you need to put one explicitly instead.

For example, you can’t leave out the semicolon when you start a new line with a parenthesis:

const total = 2 + 3
(4 + 5).toString()

This produces a runtime error due to the lack of a semicolon, which makes the two lines collapse into one:

const total = 2 + 3(4 + 5).toString();

A numeric literal can’t be called like a function.

Confusing Loops

Loops in JavaScript are particularly confusing because there are so many of them and they look alike, whereas Python has just two.

The primary type of loop in JavaScript is the for loop, which was transplanted from Java:

const fruits = ['apple', 'banana', 'orange'];
for (let i = 0; i < fruits.length; i++) {
  console.log(fruits[i]);
}

It has three parts, all of which are optional:

  • Initialization: let i = 0

  • Condition: i < fruits.length

  • Cleanup: i++

The first part executes only once before the loop starts, and it typically sets the initial value for the counter.

Then, after each iteration, the cleanup part runs to update the counter.

Right after that, the condition is evaluated to determine if the loop should continue.

This is roughly equivalent to iterating over a list of indices in Python:

fruits = ['apple', 'banana', 'orange']
for i in range(len(fruits)):
    print(fruits[i])

Notice how much work Python does for you.

On the other hand, having the loop internals exposed gives you a lot of flexibility. This type of loop is generally deterministic because you know how many times it’ll iterate from the beginning.

In JavaScript, you can make the conventional for loop non-deterministic and even infinite by omitting one or more of its parts:

for (;;) {
  // An infinite loop
}

However, a more idiomatic way to make such an iteration would involve the while loop, which is quite similar to the one you’d find in Python:

while (true) {
  const age = prompt('How old are you?');
  if (age >= 18) {
    break;
  }
}

In addition to this, JavaScript has a do…while loop, which is guaranteed to run at least once because it checks the condition after its body. You can rewrite this example in the following way:

let age;
do {
  age = prompt('How old are you?');
} while (age < 18);

Apart from stopping an iteration midway with the break keyword, you can skip to the next iteration using the continue keyword as you would in Python:

for (let i = 0; i < 10; i++) {
  if (i % 2 === 0) {
    continue;
  }
  console.log(i);
}

What you can’t do, though, is use the else clause on loops.

You might be tempted to try out the for…in loop in JavaScript, thinking it would iterate over values like a Python for loop.

Although it looks similar and has a similar name, it actually behaves very differently!

A for…in loop in JavaScript iterates over attributes of the given object, including the ones found in the prototype chain:

> const object = {name: 'John Doe', age: 42};
> for (const attribute in object) {
…   console.log(`${attribute} = ${object[attribute]}`);
… }
name = John Doe
age = 42

Should you want to exclude attributes attached to the prototype, you can call hasOwnProperty().

It will test whether a given attribute belongs to an object instance.

When you feed the for…in loop with an array, it’ll iterate over the array’s numeric indices. As you know by now, arrays in JavaScript are just glorified dictionaries:

> const fruits = ['apple', 'banana', 'orange'];
… for (const fruit in fruits) {
…   console.log(fruit);
… }
0
1
2

On the other hand, arrays expose .forEach(), which can substitute for a loop:

const fruits = ['apple', 'banana', 'orange'];
fruits.forEach(fruit => console.log(fruit));
apple
banana
orange

This is a higher-order function that accepts a callback that will run for every element in the array.

This pattern fits a bigger picture since JavaScript takes a functional approach to iteration in general.

Finally, when the ES2015 specification introduced the iterable and iterator protocols, it allowed the implementation of a long-awaited loop that would iterate over sequences.

However, since the for…in name was already taken, they had to come up with a different one.

The for…of loop is the closest relative to the for loop in Python.

With it, you can iterate over any iterable object, including strings and arrays:

const fruits = ['apple', 'banana', 'orange'];
for (const fruit of fruits) {
  console.log(fruit);
}
apple
banana
orange

This is probably the most intuitive way for a Python programmer to iterate in JavaScript.

Constructor Without new

Let’s go back to the Person type defined earlier:

function Person(name) {
  this.name = name;
  this.sayHi = function() {
    console.log(`Hi, my name is ${this.name}.`);
  }
}

If you forget to call that constructor correctly, with the new keyword in front of it, then it’ll fail silently and leave you with an undefined variable:

> let bob = Person('Bob');
> console.log(bob);
undefined

There’s a trick to protect yourself against this mistake.

When you omit the new keyword, there won’t be any object to bind to, so the this variable inside the constructor will point to the global object, such as the window object in the web browser ( globalThis with ES2020 )

You can detect that and delegate to a valid constructor invocation:

> function Person(name) {
…   if (this === window) {
…     return new Person(name);
…   }
…   this.name = name;
…   this.sayHi = function() {
…     console.log(`Hi, my name is ${this.name}.`);
…   }
… }
> let person = Person('John Doe');
> console.log(person);
Person {name: 'John Doe', sayHi: sayHi()}

This is the only reason you might want to include a return statement in your constructor.

Note: The triple equals sign (===) is intentional and has to do with the weak typing in JavaScript. You’ll learn more about it below.

Global Scope by Default

Unless you’re already at the global scope, your variables automatically become global when you don’t precede their declarations with one of these keywords:

  • var

  • let

  • const

It’s easy to fall into this trap, especially when you’re coming from Python.

For example, such a variable defined in a function will become visible outside of it:

> function call() {
…   global = 42;
…   let local = 3.14
… }
> call();
> console.log(global);
42
> console.log(local);
ReferenceError: local is not defined

Interestingly, the rules determining whether you declare a local or a global variable in Python are much more complicated than this.

There are also other kinds of variable scope in Python.

Function Scope

This quirk is only present in legacy code , which uses the var keyword for variable declaration.

You’ve learned that when a variable is declared like that, it won’t be global. But it isn’t going to have a local scope either.

No matter how deep in the function a variable is defined, it’ll be scoped to the entire function:

> function call() {
…   if (true) {
…     for (let i = 0; i < 10; i++) {
…       var notGlobalNorLocal = 42 + i;
…     }
…   }
…   notGlobalNorLocal--;
…   console.log(notGlobalNorLocal);
… }
> call();
50

The variable is visible and still alive at the top level of the function right before exiting.

However, nested functions don’t expose their variables to the outer scope:

> function call() {
…   function inner() {
…     var notGlobalNorLocal = 42;
…   }
…   inner();
…   console.log(notGlobalNorLocal);
… }
> call();
ReferenceError: notGlobalNorLocal is not defined

It works the other way around, though.

Inner functions can see the variables from the outer scope, but it gets even more interesting when you return the inner function for later use.

This creates a closure .

Illusory Function Signatures (…arguments)

Function signatures don’t exist in JavaScript.

Whichever formal parameters you declare, they have no impact on function invocation.

Specifically, you can pass any number of arguments to a function that doesn’t expect anything, and they’ll just be ignored:

> function currentYear() {
…   return new Date().getFullYear();
… }
> currentYear(42, 'foobar');
2020

You can also refrain from passing arguments that are seemingly required:

> function truthy(expression) {
…   return !!expression;
… }
> truthy();
false

Formal parameters serve as a documentation and allow you to refer to arguments by name.

Otherwise, they’re not needed.

Within any function, you have access to a special arguments variable, which represents the actual parameters that were passed:

> function sum() {
…   return [...arguments].reduce((a, x) => a + x);
… }
> sum(1, 2, 3, 4);
10

arguments is an array-like object that is iterable and has numeric indices, but unfortunately it doesn’t come with .forEach().

To wrap it in an array, you can use the spread operator .

This used to be the only way of defining variadic functions in JavaScript before the rest parameter in ES6.

Implicit Type Coercion

JavaScript is a weakly typed programming language, which is manifested in its ability to cast incompatible types implicitly.

This can give false positives when you compare two values:

if ('2' == 2) { // Evaluates to true

In general, you should prefer the strict comparison operator ( === ) to be safe:

> '2' === 2;
false
> '2' !== 2;
true

This operator compares both the values and the types of their operands.

No Integer Type

Python has a few data types to represent numbers:

  • int

  • float

  • complex

The previous Python generation also had the long type, which was eventually merged into int.

Other programming languages are even more generous, giving you fine-grained control over memory consumption, value range, floating-point precision, and the treatment of sign.

JavaScript has just one numeric type: the Number, which corresponds to Python’s float data type .

Under the hood, it’s a 64-bit double-precision number that conforms to the IEEE 754 specification. This was simple and sufficient for early web development, but it can cause a few problems today.

Note

To get the integer part of a floating-point number in JavaScript, you can use the built-in parseInt().

First of all, it’s remarkably wasteful in most situations.

If you were to represent pixels of a single FHD video frame with JavaScript’s Number, then you’d have to allocate about 50 MB of memory. In a programming language with support for a stream of bytes, such as Python, you’d need a fraction of that amount of memory.

Secondly, floating-point numbers suffer from a rounding error due to how they’re represented in computer memory.

As such, they’re unsuitable for applications requiring high precision, such as monetary calculations :

> 0.1 + 0.2;
0.30000000000000004

They’re unsafe in representing very big and very small numbers:

> const x = Number.MAX_SAFE_INTEGER + 1;
> const y = Number.MAX_SAFE_INTEGER + 2;
> x === y;
true

But that’s not the worst part .

After all, computer memory is getting cheaper by the day, and there are ways to circumvent the rounding error.

When Node.js became popular, people started using it to write back-end applications.

They needed a way of accessing the local file system.

Some operating systems identify files by arbitrary integer numbers. Occasionally, these numbers wouldn’t have an exact representation in JavaScript, so you couldn’t open the file, or you’d read some random file without knowing.

To address the problem of handling big numbers in JavaScript, there’s going to be another primitive type that can reliably represent integer numbers of any size.

Some web browsers already support this proposal ( BigInt Arbitrary precision integers in JavaScript (https://github.com/tc39/proposal-bigint) ):

const x = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
> const y = BigInt(Number.MAX_SAFE_INTEGER) + 2n;
> x === y;
false

Since you can’t mix the new BigInt data type with regular numbers, you have to either wrap them or use a special literal:

> typeof 42;
'number'
> typeof 42n;
'bigint'
> typeof BigInt(42);
'bigint'

Apart from that, the BigInt number will be compatible with two somewhat-related typed arrays for signed and unsigned integers:

  • BigInt64Array

  • BigUint64Array

While a regular BigInt can store arbitrarily large numbers, the elements of these two arrays are limited to just 64 bits.

null vs undefined

Programming languages provide ways to represent the absence of a value.

Python has None, Java has null, and Pascal has nil, for example.

In JavaScript, you get not only null but also undefined .

It may seem odd to have more than one way to represent missing values when one is already too much:

I call it my billion-dollar mistake.
It was the invention of the null reference in 1965. (…)

This has led to innumerable errors, vulnerabilities, and system
crashes, which have probably caused a billion dollars of pain and
damage in the last forty years.

— Tony Hoare

The difference between null and undefined is quite subtle.

Variables that are declared but uninitialized will implicitly get the value of undefined .

The null value, on the other hand, is never assigned automatically:

let x; // undefined
let y = null;

At any time, you can manually assign the undefined value to a variable:

let z = undefined;

This distinction between null and undefined was often used to implement default function arguments before ES6.

One of the possible implementations was this:

function fn(required, optional) {
  if (typeof optional === 'undefined') {
    optional = 'default';
  }
  // ...
}

If—for whatever reason—you wanted to keep an empty value for the optional parameter, then you couldn’t pass undefined explicitly because it would get overwritten by the default value again.

To differentiate between these two scenarios, you would pass a null value instead:

fn(42);            // optional = "default"
fn(42, undefined); // optional = "default"
fn(42, null);      // optional = null

Apart from having to deal with null and undefined, you may sometimes experience a ReferenceError exception:

> foobar;
ReferenceError: foobar is not defined

This indicates that you’re trying to refer to a variable that hasn’t been declared in the current scope, whereas undefined means declared but uninitialized, and null means declared and initialized but with an empty value.

Scope of this

Methods in Python must declare a special self parameter unless they’re static or class methods.

The parameter holds a reference to a particular instance of the class.

Its name can be anything because it’s always passed as the first positional argument.

In JavaScript, like in Java, you can take advantage of a special this keyword, which corresponds to the current instance.

But what does current instance mean ? It depends on how you invoke your function.

Recall the syntax for object literals:

> let jdoe = {
      name: 'John Doe',
      whoami: function() {
         console.log(this);
      }
  };
> jdoe.whoami();
{name: "John Doe", whoami: ƒ}

Using this in a function lets you refer to a particular object that owns that function without hard-coding a variable name.

It doesn’t matter if the function is defined in place as an anonymous expression or if it’s a regular function like this one:

function whoami() {
   console.log(this);
}
> let jdoe = {name: 'John Doe', whoami};
> jdoe.whoami();
{name: "John Doe", whoami: whoami()}

What matters is the object that you’re calling the function on:

> jdoe.whoami();
{name: "John Doe", whoami: whoami()}
> whoami();
https://realpython.com/python-vs-javascript/#javascript-quirks

In the first line, you call whoami() through an attribute of the jdoe object.

The value of this is the same as the jdoe variable in that case.

However, when you call that same function directly, this becomes the global object instead.

What’s Next ?

As a Pythonista, you know that mastering a programming language and its ecosystem is only the beginning of your path to success.

There are more abstract concepts to grasp along the way.

Document Object Model (DOM)

If you’re planning to do any sort of client-side development, then you can’t escape getting familiar with the DOM.

To allow for manipulating HTML documents in JavaScript, web browsers expose a standard interface called the DOM, which is comprised of various objects and methods.

When a page loads, your script can gain access to the internal representation of the document through a predefined document instance:

const body = document.body;

It’s a global variable available to you anywhere in your code.

Every document is a tree of elements.

To traverse this hierarchy, you can start at the root and use the following attributes to move in different directions:

  • Up: .parentElement

  • Left: .previousElementSibling

  • Right: .nextElementSibling

  • Down: .children, .firstElementChild, .lastElementChild

These attributes are conveniently available on all elements in the DOM tree, which would be perfect for recursive traversal:

const html = document.firstElementChild;
const body = html.lastElementChild;
const element = body.children[2].nextElementSibling;

Most attributes will be null if they don’t lead to an element in the tree .

The only exception is the .children property, which always returns an array-like object that can be empty.

Frequently, you won’t know where an element is.

The document object, as well as every other element in the tree, has a few methods for element lookup.

You can search elements by tag name, ID attribute, CSS class name, or even using a complex CSS selector .

You can look for one element at a time or multiple elements at once.

For example, to match elements against a CSS selector, you’d call one of these two methods:

.querySelector(selector)
.querySelectorAll(selector)

The first one returns the first occurrence of the matching element or null, while the second method always returns an array-like object with all the matching elements.

Calling these methods on the document object will cause the entire document to be searched.

You can restrict the scope of the search by calling the same methods on a previously found element:

const div = document.querySelector('div'); // The 1st div in the whole document
div.querySelectorAll('p'); // All paragraphs inside that div

Once you have a reference to an HTML element, you can do a bunch of things with it, such as:

  • Attach data to it

  • Change its style

  • Change its content

  • Change its placement

  • Make it interactive

  • Remove it altogether

You can also create new elements and add them to the DOM tree:

const parent = document.querySelector('.content');
const child = document.createElement('div');
parent.appendChild(child);

The most challenging part about using DOM is getting skilled at building accurate CSS selectors.

You can practice and learn using one of many interactive playgrounds available online .

Vanilla Javascript

Modern web browsers are much better in terms of consistency and support for emerging web standards.

So much so, in fact, that some people choose to develop client-side code in vanilla JavaScript without the help of any front-end framework.