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.5.1).
For this chapter, I consider types to be sets of values – for example, the type boolean
is the set { false
, true
}.
Figure 14.1: A partial hierarchy of JavaScript’s types. Missing are the classes for errors, the classes associated with primitive types, and more.
Figure 14.1 shows JavaScript’s type hierarchy:
JavaScript distinguishes two kinds of values: primitive values and objects. We’ll soon see what the difference is.
The diagram hints at an important fact: Some objects are not instances of the class Object
(more information). However, such objects are rare. Virtually all objects we’ll encounter are indeed instances of Object
.
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.
We 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 identity
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, see “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, see “Creating, reading, writing Arrays” (§34.3.1).
By default, we 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 (new term): Variables (including parameters) store the identities of objects. The identity of an object is a transparent reference (think pointer) to the object’s actual data on the heap (the shared main memory of a JavaScript process). 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).
Objects are compared by identity (new 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
If a parameter is passed by reference, it points to a variable and assigning to the parameter changes the variable – e.g., in the following C++ code, the parameters x
and y
are passed by reference. The invocation in line A affects the variables a
and b
of the invoker.
void swap_ints(int &x, int &y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 1;
int b = 2;
swap_ints(a, b); // (A)
// Now `a` is 2 and `b` is 1
return 0;
}
If a parameter is passed by identity (which is a new, new term), the identity of an object (a transparent reference) is passed by value. Assigning to the parameter only has a local effect. This approach is also called passing by sharing.
Acknowledgement: The term passing by identity was suggested by Allen Wirfs-Brock in 2019.
The ECMAScript specification uses the term identity as follows (source):
Values without identity are equal to other values without identity if all of their innate characteristics are the same – characteristics such as the magnitude of an integer or the length of a sequence.
In contrast, each value with identity is unique and therefore only equal to itself. Values with identity are like values without identity but with an additional unguessable, unchangeable, universally-unique characteristic called identity.
At the language level:
Symbol()
Symbol.for()
typeof
and instanceof
: what’s the type of a value?
The two operators typeof
and instanceof
let us 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
For more information on this operator, see “The instanceof
operator in detail (advanced)” (§31.7.3).
Exercise:
instanceof
exercises/values/instanceof_exrc.mjs
JavaScript’s original factories for objects are constructor functions: ordinary functions that return “instances” of themselves if we 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 types 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
:
We 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, we can also use Number
as a class and create number objects. These objects are different from real numbers and should be avoided. They virtually never show up in normal code. See the next subsection for more information.
If we new-invoke a constructor function associated with a primitive type, it returns a so-called wrapper object. This is the standard way of converting a primitive value to an object – by “wrapping” it.
The primitive value is not an instance of the wrapper class:
const prim = true;
assert.equal(typeof prim, 'boolean');
assert.equal(prim instanceof Boolean, false);
The wrapper object is not a primitive value:
const wrapper = Object(prim);
assert.equal(typeof wrapper, 'object'); // not 'boolean'
assert.equal(wrapper instanceof Boolean, true);
We can unwrap the wrapper object to get back the primitive value:
assert.equal(wrapper.valueOf(), prim); // unwrap
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'
We 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