Some programming languages are the result of years of careful design and planning.[1] In contrast, Netscape hired Brendan Eich in 1995 to prototype the JavaScript language in just ten days.
There’s nothing wrong with creating a language in ten days. The short timeline is likely to be why the language has such a pragmatic design. This pragmatic design is perhaps the reason for its incredible success. However, more time spent planning might have reduced the idiosyncrasies in the language.
In this section, I will explore JavaScript in greater depth. Mastering the details will help you write simpler and clear code:
-
You can exploit the underlying simplicity of JavaScript.
-
You can avoid encountering the idiosyncrasies of JavaScript.
Values
In practice, there are six data types in JavaScript that you need to know about: [2]
-
undefined
-
string (e.g.,
'hello'
,"hello"
and`hello`
) -
number (e.g.,
1
,3.14
,2.99792e8
) [3] -
boolean (
true
andfalse
) -
function
-
Object (e.g.,
{name: 'AIP', chapter: 5}
) andnull
Undefined and null
undefined
and null
are special values that typically indicate no result or a missing value.
In JavaScript, the value of any variable is initially undefined
:
$ node
Welcome to Node.js
Type ".help" for more information.
> let place;
undefined
> place
undefined
> typeof place
'undefined'
> place = 'Sydney';
'Sydney'
> place
'Sydney'
> typeof place
'string'
In the example above, the variable place
was undefined
until it is set to be the string 'Sydney'
.
JavaScript also has a null
value that is similar to undefined
. In practice, you might use null
to represent values that are known to be empty but undefined
for unknown or uninitialized values. For example, fax_number === undefined
might indicate that it is not known whether you have or do not have a fax number, whereas fax_number === null
might indicate it is known that you do not have a fax number.
Strings
In JavaScript, strings typically represent textual data (e.g., "This is a string!"
). Behind the scenes, strings are sequences of 16-bit values. Text is Unicode data encoded into 16-bit values using an encoding called UTF-16 (see Chapter 11 for more information).
The length of a string is the number of 16-bit values. Usually, the length turns out to be the same as the number of characters (e.g., "Hello".length === 5
and "你好".length === 2
). However, this is not always the case with Emoji and other rare characters (e.g., '😺'.length === 2
).
Numbers
In JavaScript, all numbers are 64-bit floating-point numbers. In other words, JavaScript makes no distinction between integers (such as 1
) and floating-point numbers (such as 1.0
).
[4]
The IEEE 754 standard defines the representation of 64-bit floating-point numbers. One bit stores the sign of a number (i.e., whether the number is positive or negative), 52 bits store the digits of the number (the significand) and 11 bits store the exponent (the mantissa).
64-bit floating-point provides ample precision for most applications. The 11 bit exponent means that JavaScript can represent numbers as large as 1.7976931348623157x10308 or as small as 5x10-324. The 52-bit significand means that JavaScript can exactly represent any integer from -9007199254740992 to 9007199254740992. However, JavaScript does not have unlimited precision. In the following transcript, you can see that numbers have limited precision.
$ node
Welcome to Node.js
Type ".help" for more information.
> 9007199254740992 + 2
9007199254740994
> 9007199254740992 + 1
9007199254740992
> 1.0000000000000002
1.0000000000000002
> 1.00000000000000012
1.0000000000000002
> 1.00000000000000011
1
>
Booleans
Boolean values in JavaScript are either true
or false
.
JavaScript can convert any value to a boolean. This conversion occurs automatically in any expression expecting a boolean value (e.g., the condition in if
, while
, do … while
and … ? … : …
).
For example, the following code is valid:
if (100) {
console.log('100 is equivalent to true');
}
JavaScript uses the following rules to convert to boolean:
Value |
Boolean equivalent |
---|---|
|
|
the number |
|
empty string |
|
|
|
any non- |
|
any non-empty string (e.g., |
|
any object (e.g., |
|
JavaScript developers sometimes informally describe this conversion as 'truthiness': false
, undefined
, null
, ""
, 0
and NaN
are said to be 'falsy' values; everything else is equivalent to boolean true
and is said to be 'truthy'.
Functions
JavaScript allows you to define your own functions:
function sayHello() {
console.log('hello');
}
function square(x) {
return x * x;
}
A function definition declares a variable (sayHello
), creates a JavaScript function object and then assigns it to the variable.
This assignment can be performed explicitly by using a function expression:
let sayHello = function () {
console.log('hello');
};
let square = function (x) {
return x * x;
}
Since 2015 (the ECMAScript 6 standard), JavaScript has supported the “arrow” notation as a shorthand for function expressions:
let sayHello = () => { console.log('hello'); };
let square = (x) => x * x;
Tip
|
Some programming languages distinguish between functions and methods. The term method is used in object-oriented programming to refer to a function associated with an object. However, JavaScript does not make any fundamental distinction between functions and methods. Functions can be attached or detached to an object as required, so the terms function and method are interchangeable in JavaScript. |
Objects
In JavaScript, objects are dictionaries (i.e., lookup tables) that map from keys to values:
let obj = {name: "Alice", birth: 1994};
The key name
can be examined using a property accessor (dot notation):
obj.name
The use of dot notation (above) is identical and equivalent to using bracket notation (below):
obj["name"]
The value of a property can be anything: booleans, numbers, strings, other objects or even functions:
obj.greeting = () => { console.log("hello, world"); };
obj.greeting(); // This will print hello, world
In JavaScript, the keys of an object are always strings. If you use a boolean, a number or even an object as a key, it will first be converted into a string. In the transcript below, 1
is converted into the string "1"
when used as a key. Similarly, both {}
and {different: true}
are converted into the string "[object Object]"
when used as keys of obj
:
$ node
Welcome to Node.js
Type ".help" for more information.
> obj = {name: "Alice", birth: 1994}
{ name: 'Alice', birth: 1994 }
> obj[1] = "Hello"
'Hello'
> obj["1"]
'Hello'
> obj[{}] = "Hello, again"
'Hello, again'
> obj[{different: true}]
'Hello, again'
> Object.keys(obj)
[ '1', 'name', 'age', '[object Object]' ]
>
Classes and inheritance
JavaScript has a class
keyword. The following example defines and instantiates a Person
:
// Define the Person class
class Person {
constructor(firstName, lastName) {
this.name = firstName + " " + lastName;
}
showName() {
console.log("My name is " + this.name);
}
}
// Use the person class
let alice = new Person('Alice', 'Nelson');
alice.showName();
JavaScript’s approach to classes and inheritance is unusual. Its approach is known as prototypal inheritance. JavaScript simulates inheritance using chains of objects linked by a prototype property.
The class
keyword is known as syntactic sugar. There is no class type in JavaScript. Instead, a class
definition is superficial syntax that makes the language easier to use and therefore “sweeter”. JavaScript translates the syntactic sugar into simpler features.
Internally, the syntactic sugar of the Person class is translated into function and prototype definitions:
|
…is translated into… |
|
This translation has two parts:
-
the
constructor
method of theclass
is translated into an ordinary JavaScriptfunction
, -
then the methods of the
class
are translated into properties of the.prototype
of the function.
First, I will examine the constructor. In JavaScript, object constructors are functions and, vice versa, all functions are object constructors. The Person
function can be invoked with or without the new
keyword. Invoking Person
with the new
keyword instantiates a new object:
$ node
Welcome to Node.js
Type ".help" for more information.
> function Person(firstName, lastName) {
... this.name = firstName + " " + lastName;
... }
undefined
> new Person('Alice', 'Nelson')
Person { name: 'Alice Nelson' }
> Person('Bobby', 'Brady')
undefined
>
When the Person('Bobby', 'Brady')
is invoked without new
, the code is run as an ordinary function. The function has no return
statement, so the result is undefined
.
The JavaScript interpreter internally converts a new
expression, as in new Person('Alice, 'Nelson')
, into something equivalent to the following:
|
…is translated into… |
|
In summary: the new
keyword creates a new empty object ({}
); the empty object is passed to the function via the this
keyword.
The next aspect of the class
translation is the .prototype
. This is how JavaScript achieves prototypal inheritance.
Every function
declared in JavaScript has a property .prototype
that is initially an empty object.
The prototype can be configured in any way you wish:
Person.prototype.showName = function () {
console.log(this.name);
}
Person.prototype.species = "Human";
Person.prototype["home"] = "Earth";
Normal invocation of the function does not use the prototype. The prototype is only used when the function is invoked as a constructor (i.e., using new
).
Every object in JavaScript has a hidden internal property named [[Prototype]]
. If JavaScript cannot find a property on an object, then it will keep searching for that prototype by loading [[Prototype]]
and then searching for the property on the ‘prototype’.
Consider, for example, a simple object:
let bobby = {name: "Bobby Brady"};
What happens if we attempt to access bobby.species
? There is no species property of bobby
, so bobby.species
is undefined
.
Now, suppose we have some background information that is true for every person:
let person = {species: "Human", home: "Earth"};
Prototypal inheritance uses an internal link between the two objects. Instead of bobby.species
returning undefined
, the internal linkage is searched to check .species
in the person
object.
Object.setPrototypeOf(bobby, person)
manually sets the prototype linkage. The setPrototypeOf
function sets the hidden internal [[Prototype]]
property of bobby
. The following transcript demonstrates prototypal inheritance:
$ node
Welcome to Node.js
Type ".help" for more information.
> let bobby = {name: "Bobby Brady"}
undefined
> let person = {species: "Human", home: "Earth"};
undefined
> bobby.name
'Bobby Brady'
> bobby.species
undefined
> Object.setPrototypeOf(bobby, person)
{ name: 'Bobby Brady' }
> bobby.name
'Bobby Brady'
> bobby.species
'Human'
>
Note that bobby.species
is no longer undefined after establishing the prototype chain. The prototype chain ensures that bobby.species
is "Human"
(via its prototype: the person
object).
The prototype chain can be as deep as you wish. Following example shows a prototype chain configured so that bobby.breathes
will search bobby
, then person
and finally animal
for a match to the key "breathes"
.
$ node
Welcome to Node.js
Type ".help" for more information.
> let bobby = {name: "Bobby Brady"}
undefined
> let person = {species: "Human", home: "Earth"};
undefined
> let animal = {breathes: "Air"};
undefined
> bobby.breathes
undefined
> Object.setPrototypeOf(bobby, person)
{ name: 'Bobby Brady' }
> bobby.breathes
undefined
> Object.setPrototypeOf(person, animal)
{ species: 'Human', home: 'Earth' }
> bobby.breathes
"Air"
>
This brings me to the significance of .prototype
on a function. Calling new
in JavaScript automatically sets the prototype of the new instance to be the value of the function’s .prototype
property.
Therefore, a more accurate translation of new
to the underlying logic should include setting the prototype of the newly created instance to be the prototype:
|
…is translated into… |
|
Now, consider how JavaScript handles alice.showName()
.
First, showName
is resolved. It isn’t part of alice
, but it is found in the prototype chain as Person.prototype.showName
. JavaScript invokes that method, using the original object (alice
) as the value of this
:
|
…is translated into… |
|
The simple syntax for classes hides a great deal of underlying complexity and subtlety. As a developer, it is your choice to write simple code. By understanding how the mechanism works, you can avoid unnecessary complexity and understand why things break.
The internal mechanisms for classes and inheritance in JavaScript can be quite complex. However, in practice, you do not need to know these internals. The details of prototypal inheritance are not relevant in most ordinary situations that involve the JavaScript class
keyword.
Equality
Equality (==
) in JavaScript is particularly complex.
In JavaScript, 1 == 1
and "1" == "1"
because these values are equal. However, equality is complicated when the types differ (addition/concatenation +
in JavaScript has a similar problem).
Here is the table of outcomes for equality (tick-marks ✓ indicate true
, blank cells indicate false
):
== |
false |
0 |
"" |
true |
1 |
"1" |
"x" |
NaN |
null |
undefined |
{} |
[] |
---|---|---|---|---|---|---|---|---|---|---|---|---|
false |
✓ |
✓ |
✓ |
✓ |
||||||||
0 |
✓ |
✓ |
✓ |
✓ |
||||||||
"" |
✓ |
✓ |
✓ |
✓ |
||||||||
true |
✓ |
✓ |
✓ |
|||||||||
1 |
✓ |
✓ |
✓ |
|||||||||
"1" |
✓ |
✓ |
✓ |
|||||||||
"x" |
✓ |
|||||||||||
NaN |
||||||||||||
null |
✓ |
✓ |
||||||||||
undefined |
✓ |
✓ |
||||||||||
{} |
||||||||||||
[] |
✓ |
✓ |
✓ |
Here are the rules for equality (you might note the similarity to the rules for +
from Chapter 4):
-
Nothing is equal to
NaN
-
null
andundefined
are equal to themselves and each other, but nothing else -
If both operands are objects, arrays, compare the object identities (i.e., are they the same object instance in memory)
-
Otherwise, convert any objects or arrays into a "primitive" value (i.e.,
undefined
,null
, boolean, number or string). In practice, this means that the interpreter uses thetoString()
method to convert objects and arrays into strings.-
The default
toString()
method for an object ({}
) returns"[object Object]"
-
The default
toString()
method for a list ([]
,[1,2,3]
,["a","b","c"]
) converts the elements into strings (""
,"1,2,3"
,"a,b,c"
)
-
-
Then, if both primitives are strings, perform a string comparison
-
Otherwise, convert both sides to numbers and perform a numeric comparison
-
true
is equivalent to 1 -
false
is equivalent to 0 -
the empty string (
""
) or a string of whitespace (" "
) is equivalent to 0 -
other strings are interpreted as a number if possible (e.g.,
"33.5"
is 33.5) andNaN
otherwise (e.g.,"x"
isNaN
)
-
Equality is difficult and, in many cases, counter-intuitive. However, as a professional developer, you can choose not to rely on these obscure possibilities. For example, you can ensure that you only use ==
to compare values of the same type.
A better option is to use the ===
strict equality operator. It was introduced in 1999 to resolve the confusion of the original ==
equality operator. The following table demonstrates strict equality comparisons:
[5]
=== |
false |
0 |
"" |
true |
1 |
"1" |
"x" |
NaN |
null |
undefined |
{} |
[] |
---|---|---|---|---|---|---|---|---|---|---|---|---|
false |
✓ |
|||||||||||
0 |
✓ |
|||||||||||
"" |
✓ |
|||||||||||
true |
✓ |
|||||||||||
1 |
✓ |
|||||||||||
"1" |
✓ |
|||||||||||
"x" |
✓ |
|||||||||||
NaN |
||||||||||||
null |
✓ |
|||||||||||
undefined |
✓ |
|||||||||||
{} |
||||||||||||
[] |
You will notice that it is vastly easier to remember and understand strict equality. There are no surprises. For this reason, professional JavaScript developers prefer the use of ===
strict equality over ==
equality.