In this chapter, we’ll examine what kinds of values JavaScript has.
Supporting tool: ===
In this chapter, we’ll occasionally use the strict equality operator. a === b
evaluates to true
if a
and b
are equal. What exactly that means is explained in “Strict equality (===
and !==
)” (§15.4.2).
For this chapter, I consider types to be sets of values – for example, the type boolean
is the set { false
, true
}.
Figure 14.1 shows JavaScript’s type hierarchy. What do we learn from that diagram?
Object
. Each instance of Object
is also an object, but not vice versa. However, virtually all objects that you’ll encounter in practice are instances of Object
– for example, objects created via object literals. More details on this topic are explained in “Not all objects are instances of Object
” (§31.7.3).
The ECMAScript specification only knows a total of eight types. The names of those types are (I’m using TypeScript’s names, not the spec’s names):
undefined
with the only element undefined
null
with the only element null
boolean
with the elements false
and true
number
the type of all numbers (e.g., -123
, 3.141
)
bigint
the type of all big integers (e.g., -123n
)
string
the type of all strings (e.g., 'abc'
)
symbol
the type of all symbols (e.g., Symbol('My Symbol')
)
object
the type of all objects (different from Object
, the type of all instances of class Object
and its subclasses)
The specification makes an important distinction between values:
undefined
, null
, boolean
, number
, bigint
, string
, symbol
.
In contrast to Java (that inspired JavaScript here), primitive values are not second-class citizens. The difference between them and objects is more subtle. In a nutshell:
Other than that, primitive values and objects are quite similar: they both have properties (key-value entries) and can be used in the same locations.
Next, we’ll look at primitive values and objects in more depth.
You can’t change, add, or remove properties of primitives:
const str = 'abc';
assert.equal(str.length, 3);
assert.throws(
() => { str.length = 1 },
/^TypeError: Cannot assign to read only property 'length'/
);
Primitives are passed by value: variables (including parameters) store the contents of the primitives. When assigning a primitive value to a variable or passing it as an argument to a function, its content is copied.
const x = 123;
const y = x;
// `y` is the same as any other number 123
assert.equal(y, 123);
Observing the difference between passing by value and passing by reference
Due to primitive values being immutable and compared by value (see next subsection), there is no way to observe the difference between passing by value and passing by identity (as used for objects in JavaScript).
Primitives are compared by value: when comparing two primitive values, we compare their contents.
assert.equal(123 === 123, true);
assert.equal('abc' === 'abc', true);
To see what’s so special about this way of comparing, read on and find out how objects are compared.
Objects are covered in detail in “Objects” (§30) and the following chapter. Here, we mainly focus on how they differ from primitive values.
Let’s first explore two common ways of creating objects:
Object literal:
const obj = {
first: 'Jane',
last: 'Doe',
};
The object literal starts and ends with curly braces {}
. It creates an object with two properties. The first property has the key 'first'
(a string) and the value 'Jane'
. The second property has the key 'last'
and the value 'Doe'
. For more information on object literals, consult “Object literals: properties” (§30.3.1).
Array literal:
const fruits = ['strawberry', 'apple'];
The Array literal starts and ends with square brackets []
. It creates an Array with two elements: 'strawberry'
and 'apple'
. For more information on Array literals, consult “Creating, reading, writing Arrays” (§33.3.1).
By default, you can freely change, add, and remove the properties of objects:
const obj = {};
obj.count = 2; // add a property
assert.equal(obj.count, 2);
obj.count = 3; // change a property
assert.equal(obj.count, 3);
Objects are passed by identity (my term): variables (including parameters) store the identities of objects.
The identity of an object is like a pointer (or a transparent reference) to the object’s actual data on the heap (think shared main memory of a JavaScript engine).
When assigning an object to a variable or passing it as an argument to a function, its identity is copied. Each object literal creates a fresh object on the heap and returns its identity.
const a = {}; // fresh empty object
// Pass the identity in `a` to `b`:
const b = a;
// Now `a` and `b` point to the same object
// (they “share” that object):
assert.equal(a === b, true);
// Changing `a` also changes `b`:
a.name = 'Tessa';
assert.equal(b.name, 'Tessa');
JavaScript uses garbage collection to automatically manage memory:
let obj = { prop: 'value' };
obj = {};
Now the old value { prop: 'value' }
of obj
is garbage (not used anymore). JavaScript will automatically garbage-collect it (remove it from memory), at some point in time (possibly never if there is enough free memory).
Details: passing by identity
“Passing by identity” means that the identity of an object (a transparent reference) is passed by value. This approach is also called “passing by sharing”.
Objects are compared by identity (my term): two variables are only equal if they contain the same object identity. They are not equal if they refer to different objects with the same content.
const obj = {}; // fresh empty object
assert.equal(obj === obj, true); // same identity
assert.equal({} === {}, false); // different identities, same content
typeof
and instanceof
: what’s the type of a value?
The two operators typeof
and instanceof
let you determine what type a given value x
has:
if (typeof x === 'string') ···
if (x instanceof Array) ···
How do they differ?
typeof
distinguishes the 7 types of the specification (minus one omission, plus one addition).
instanceof
tests which class created a given value.
Rule of thumb: typeof
is for primitive values; instanceof
is for objects
typeof
x | typeof x |
---|---|
undefined | 'undefined' |
null | 'object' |
Boolean | 'boolean' |
Number | 'number' |
Bigint | 'bigint' |
String | 'string' |
Symbol | 'symbol' |
Function | 'function' |
All other objects | 'object' |
Table 14.1: The results of the typeof
operator.
Table 14.1 lists all results of typeof
. They roughly correspond to the 7 types of the language specification. Alas, there are two differences, and they are language quirks:
typeof null
returns 'object'
and not 'null'
. That’s a bug. Unfortunately, it can’t be fixed. TC39 tried to do that, but it broke too much code on the web.
typeof
of a function should be 'object'
(functions are objects). Introducing a separate category for functions is confusing.
These are a few examples of using typeof
:
> typeof undefined
'undefined'
> typeof 123n
'bigint'
> typeof 'abc'
'string'
> typeof {}
'object'
Exercises: Two exercises on typeof
exercises/values/typeof_exrc.mjs
exercises/values/is_object_test.mjs
instanceof
This operator answers the question: has a value x
been created by a class C
?
x instanceof C
For example:
> (function() {}) instanceof Function
true
> ({}) instanceof Object
true
> [] instanceof Array
true
Primitive values are not instances of anything:
> 123 instanceof Number
false
> '' instanceof String
false
> '' instanceof Object
false
Exercise: instanceof
exercises/values/instanceof_exrc.mjs
JavaScript’s original factories for objects are constructor functions: ordinary functions that return “instances” of themselves if you invoke them via the new
operator.
ES6 introduced classes, which are mainly better syntax for constructor functions.
In this book, I’m using the terms constructor function and class interchangeably.
Classes can be seen as partitioning the single type object
of the specification into subtypes – they give us more types than the limited 7 ones of the specification. Each class is the type of the objects that were created by it.
Each primitive type (except for the spec-internal types for undefined
and null
) has an associated constructor function (think class):
Boolean
is associated with booleans.
Number
is associated with numbers.
String
is associated with strings.
Symbol
is associated with symbols.
Each of these functions plays several roles – for example, Number
:
You can use it as a function and convert values to numbers:
assert.equal(Number('123'), 123);
Number.prototype
provides the properties for numbers – for example, method .toString()
:
assert.equal((123).toString, Number.prototype.toString);
Number
is a namespace/container object for tool functions for numbers – for example:
assert.equal(Number.isInteger(123), true);
Lastly, you can also use Number
as a class and create number objects. These objects are different from real numbers and should be avoided.
assert.notEqual(new Number(123), 123);
assert.equal(new Number(123).valueOf(), 123);
The constructor functions related to primitive types are also called wrapper types because they provide the canonical way of converting primitive values to objects. In the process, primitive values are “wrapped” in objects.
const prim = true;
assert.equal(typeof prim, 'boolean');
assert.equal(prim instanceof Boolean, false);
const wrapped = Object(prim);
assert.equal(typeof wrapped, 'object');
assert.equal(wrapped instanceof Boolean, true);
assert.equal(wrapped.valueOf(), prim); // unwrap
Wrapping rarely matters in practice, but it is used internally in the language specification, to give primitives properties.
There are two ways in which values are converted to other types in JavaScript:
String()
.
The function associated with a primitive type explicitly converts values to that type:
> Boolean(0)
false
> Number('123')
123
> String(123)
'123'
You can also use Object()
to convert values to objects:
> typeof Object(123)
'object'
The following table describes in more detail how this conversion works:
x | Object(x) |
---|---|
undefined | {} |
null | {} |
boolean | new Boolean(x) |
number | new Number(x) |
bigint | An instance of BigInt (new throws TypeError ) |
string | new String(x) |
symbol | An instance of Symbol (new throws TypeError ) |
object | x |
For many operations, JavaScript automatically converts the operands/parameters if their types don’t fit. This kind of automatic conversion is called coercion.
For example, the multiplication operator coerces its operands to numbers:
> '7' * '3'
21
Many built-in functions coerce, too. For example, Number.parseInt()
coerces its parameter to a string before parsing it. That explains the following result:
> Number.parseInt(123.45)
123
The number 123.45
is converted to the string '123.45'
before it is parsed. Parsing stops before the first non-digit character, which is why the result is 123
.
Exercise: Converting values to primitives
exercises/values/conversion_exrc.mjs