Some data types such as numbers and strings (Chapter 2), objects (Chapter 5) and arrays (Chapter 6) are so fundamental to JavaScript that we can consider them to be part of the language itself. This chapter covers other important but less fundamental APIs that can be thought of as defining the “standard library” for JavaScript: these are useful classes and functions that are built in to JavaScript and available to all JavaScript programs both in web browsers and in Node. (Not everything documented here is defined by the JavaScript language specification: some of the classes and functions documented here were first implemented in web browsers and then adopted by Node, making them de-facto members of the JavaScript standard library.)
The sections of this chapter are independent of one another and you can read them in any order. They cover:
The Set and Map classes for representing sets of values and mappings from one set of values to another set of values.
Array-like objects known as TypedArrays that represent arrays of binary data, along with a related class for extracting values from non-array binary data.
Regular expressions and the RegExp class which define textual patterns and are useful for text processing. This section also covers regular expression syntax in detail.
The Date class for representing and manipulating dates and times
The Error class and its various subclasses, instances of which are thrown when errors occur in JavaScript programs
The JSON
object whose methods support serialization and
deserialization of JavaScript data structures composed of objects,
array, strings, numbers and booleans.
The Intl object and the classes it defines that can help you to localize your JavaScript programs
The console
object whose methods output strings in ways that are
particularly useful for debugging programs and logging the behavior
of those programs.
The URL class which simplifies the task of parsing and manipulating URLs. This section also covers global functions for encoding and decoding URLs and their component parts.
setTimeout()
and related functions for specifying code to be
executed after a specified interval of time has elapsed.
Some of the sections in this chapter–notably the sections on Typed Arrays and Regular Expressions–are quite long, because there is significant background information you need to understand before you can use those types effectively. Many of the other sections, however, are short: they simply introduce a new API and show some examples of its use.
JavaScript’s Object type is a versatile data structure that can be
used to map strings (the object’s property names) to arbitrary
values. And when the value being mapped to is something fixed like
true
, then the object is effectively a set of strings.
Objects are actually used as maps and sets fairly routinely in JavaScript programming, but this is limited by the restriction to strings, and complicated by the fact that Objects normally inherit properties with names like “toString” which are not typically intended to be part of the map or set.
For this reason, ES6 introduces true Set and Map classes, which we’ll cover in the sub-sections below.
A set is a collection of values, like an array is. Unlike arrays, however, sets are not indexed, and they do not allow duplicates: a value is either a member of a set or it is not a member; it is not possible to ask how many times a value appears in a set.
Create a Set object with the Set()
constructor:
let
s
=
new
Set
();
// A new, empty set
let
t
=
new
Set
([
1
,
s
]);
// A new set with two members
The argument to the Set()
constructor need not be an array: any
iterable object (including other Set objects) is allowed:
let
t
=
new
Set
(
s
);
// A new set that copies the elements of s.
let
unique
=
new
Set
(
"Mississippi"
);
// 4 elements: "M", "i", "s" and "p"
The size
property of a Set is like the length
property of an
Array: it tells you how many values the set contains:
unique
.
size
// => 4
Sets don’t need to be initialized when you create them. You can add
and remove elements at any time with add()
, delete()
, and
clear()
. Remember that sets can not contain duplicates, so adding a
value to a set when it already contains that value has no effect:
let
s
=
new
Set
();
// Start empty
s
.
size
// => 0
s
.
add
(
1
);
// Add a number
s
.
size
// => 1; now the set has one member
s
.
add
(
1
);
// Add the same number again
s
.
size
// => 1; the size does not change
s
.
add
(
true
);
// Add another value; note that it is fine to mix types
s
.
size
// => 2
s
.
add
([
1
,
2
,
3
]);
// Add an array value
s
.
size
// => 3; the array was added, not its elements
s
.
delete
(
1
)
// => true: successfully deleted element 1
s
.
size
// => 2: the size is back down to 2
s
.
delete
(
"test"
)
// => false: "test" was not a member, deletion failed
s
.
delete
(
true
)
// => true: delete succeeded
s
.
delete
([
1
,
2
,
3
])
// => false: the array in the set is different
s
.
size
// => 1: there is still that one array in the set
s
.
clear
();
// Remove everything from the set
s
.
size
// => 0
There are a few of important points to note about the code above:
The add()
method takes a single argument, if you pass an array, it
adds the array itself to the set, not the individual array
elements. add()
always returns the Set it is invoked on, however,
so if you want to add multiple values to a set, you can used chained
method calls like s.add('a').add('b').add('c');
The delete()
also only deletes a single set element at a
time. Unlike add()
, however, delete()
returns boolean value. If
the value you specify was actually a member of the set, then
delete()
removes it and returns true
. Otherwise, it does nothing
and returns false
.
Finally, it is very important to understand that Set membership is
based on strict equality checks, like the ===
operator
performs. A set can contain both the number 1
and the string
"1"
, because it considers them to be distinct values. When the
values are objects (or arrays or functions), the are also compared
as if with ===
. This is why we were unable to delete the array
element from the Set in the code above. We had added an array to the
Set, and then were trying to remove that array by passing a
different array (albeit with the same elements) to the delete()
method. In order for this to work we would have had to pass a
reference to exactly the same array.1
In practice, the most important thing we do with sets is not to add
and remove elements from them, but to check to see whether a specified
value is a member of the Set. We do this with the has()
method:
let
oneDigitPrimes
=
new
Set
([
2
,
3
,
5
,
7
]);
oneDigitPrimes
.
has
(
2
)
// => true: 2 is a one-digit prime number
oneDigitPrimes
.
has
(
3
)
// => true: so is 3
oneDigitPrimes
.
has
(
4
)
// => false: 4 is not a prime
oneDigitPrimes
.
has
(
"5"
)
// => false: "5" is not even a number
The most important thing to understand about sets is that they are
optimized for membership testing, and no matter how many members the
set has, the has()
method will be very fast. The includes()
method
of an Array also performs membership testing, but the time it takes is
proportional to the size of the array, and using an array as a set can
be much, much slower than using a real Set object.
The Set class is iterable, which means that you can use a for/of
loop to enumerate all of the elements of a set:
let
sum
=
0
;
for
(
let
p
of
oneDigitPrimes
)
{
// Loop through the one digit primes
sum
+=
p
;
// and add them up
}
sum
// => 17: 2 + 3 + 5 + 7
Because Set objects are iterable, you can convert them to arrays and
argument lists with the ...
spread operator:
[...
oneDigitPrimes
]
// => [2,3,5,7]: the set converted to an Array
Math
.
max
(...
oneDigitPrimes
)
// => 7: set elements passed as function arguments
Sets are often described as “unordered collections”. This isn’t exactly true for the JavaScript Set class, however. A JavaScript set is un-indexed: you can’t ask for the first or third element of a Set they way you can with an array. But the JavaScript Set class always remembers the order that elements were inserted in, and it always uses this order when you iterate a set: the first element inserted will be the first one iterated (assuming you haven’t deleted it first), and the most recently inserted element will be the last one iterated 2.
In addition to being iterable, the Set class also implements a
forEach()
method that is similar to the Array method of the same name:
let
product
=
1
;
oneDigitPrimes
.
forEach
(
n
=>
{
product
*=
n
;
});
product
// => 210: 2 * 3 * 5 * 7
The forEach()
of an array passes array indexes as the second
argument to the function you specify. Sets don’t have indexes, so the
Set class’s version of this method simply passes the element value as
both the first and second argument.
A Map object represents a set of values known as keys where each key has another value associated with (or “mapped to”) it. In a sense, a Map is like an array, but instead of using a set of sequential integers as the keys, Maps allow us to use arbitrary values as “indexes”. Like arrays, Maps are fast: looking up the value associated with a key will be fast (though not as fast as indexing an array) no matter how large the Map is.
Create a new Map with the Map()
constructor:
let
m
=
new
Map
();
// Create a new, empty Map
let
n
=
new
Map
([
// A new Map initialized with string keys mapped to numbers
[
"one"
,
1
],
[
"two"
,
2
]
]);
The optional argument to the Map()
constructor should be an iterable
object that yields two element [key, value]
arrays. In practice,
this means that if you want to initialize a Map when you create it,
you’ll typically write out the desired keys and associated values as
an array of arrays. But you can also use the Map()
constructor to
copy other maps or to copy the property names and values from an
existing object:
let
copy
=
new
Map
(
n
);
// A new map with the same keys and values as map n
let
o
=
{
x
:
1
,
y
:
2
};
// An object with two properties
let
p
=
new
Map
(
Object
.
entries
(
o
));
// Same as new Map([["x", 1], ["y", 2]])
Once you have created a Map object, you can query the value associated
with a given key with get()
, and can add a new key/value pair with
set()
. Remember, though that a Map is a set of keys, each of which
has an associated value. This is not quite the same as a set of
key/value pairs. If you call set()
with a key that already exists in
the map, you will change the value associated with that key, not add a
new key/value mapping. In addition to the get()
and set()
, the Map
class also defines methods that are like Set methods: use has()
to
check whether a Map includes the specified key; use delete()
to
remove a key (and its associated value) from the Map; use clear()
to
remove all key/value pairs from the Map; and use the size
property
to find out how many keys a Map contains.
let
m
=
new
Map
();
// Start with an empty map
m
.
size
// => 0: empty maps have no keys
m
.
set
(
"one"
,
1
);
// Map the key "one" to the value 1
m
.
set
(
"two"
,
2
);
// And the key "two" to the value 2.
m
.
size
// => 2: the map now has two keys
m
.
get
(
"two"
)
// => 2: return the value associated with key "two"
m
.
get
(
"three"
)
// => undefined: this key is not in the set
m
.
set
(
"one"
,
true
);
// Change the value associated with an existing key
m
.
size
// => 2: the size doesn't change
m
.
has
(
"one"
)
// => true: the map has a key "one"
m
.
has
(
true
)
// => false: the map does not have a key true
m
.
delete
(
"one"
)
// => true: the key existed and deletion succeeded
m
.
size
// => 1
m
.
delete
(
"three"
)
// => false: failed to delete a non-existent key
m
.
clear
();
// Remove all keys and values from the map
Like the add()
method of Set, the set()
method of Map can be
chained, which allows Maps to be initialized without using arrays of
arrays:
let
m
=
new
Map
().
set
(
"one"
,
1
).
set
(
"two"
,
2
).
set
(
"three"
,
3
);
m
.
size
// => 3
m
.
get
(
"two"
)
// => 2
As with Set, any JavaScript value can be used as a key or a value in a
Map. This includes null
, undefined
, NaN
, as well as reference
types like objects and arrays. And as with the Set class, Map compares
keys by identity, not by equality, so if you use an object or array as
a key, it will be considered different from every other object and
array, even those with exactly the same properties or elements:
let
m
=
new
Map
();
// Start with an empty map.
m
.
set
({},
1
);
// Map one empty object to the number 1.
m
.
set
({},
2
);
// Map a different empty object to the number 2.
m
.
size
// => 2: there are two keys in this map
m
.
get
({})
// => undefined: but this empty object is not a key
m
.
set
(
m
,
undefined
);
// Map the map itself to the value undefined.
m
.
has
(
m
)
// => true: m is a key in itself
m
.
get
(
m
)
// => undefined: same value we'd get if m wasn't a key
Map objects are iterable, and each iterated value is a two-element
array where the first element is a key and the second element is the
value associated with that key. If you use the spread operator with a
map object you’ll get an array of arrays like the ones that we passed
to the Map()
constructor. And when iterating a Map with a for/of
loop, it is idiomatic to use destructuring assignment to assign the
key and value to separate variables:
let
m
=
new
Map
([[
"x"
,
1
],
[
"y"
,
2
]]);
[...
m
]
// => [["x", 1], ["y", 2]]
for
(
let
[
key
,
value
]
of
m
)
{
// On the first iteration key will be "x" and value will be 1
// On the second iteration key will be "y" and value will be 2
}
Like the Set class, the Map class iterates in insertion order. The first key/value pair iterated will be the one least recently added to the map and the last pair iterated will be the one most recently added.
If you want to iterate just the keys or just the associated values of
a Map, use the keys()
and values()
methods: these return iterable
objects that iterate keys and values, in insertion order. (The
entries()
method returns an iterable object that iterates key/value
pairs, but this is exactly the same as iterating the Map directly.)
[...
m
.
keys
()]
// => ["x", "y"]: just the keys
[...
m
.
values
()]
// => [1, 2]: just the values
[...
m
.
entries
()]
// => [["x", 1], ["y", 2]]: same as [...m]
Map objects can also be iterated using the forEach()
method that was
first implemented by the Array class.
m
.
forEach
((
value
,
key
)
=>
{
// note value, key NOT key, value
// On the first invocation value will be 1 and key will be "x"
// On the first invocation value will be 2 and key will be "y"
});
It may seem strange that the value parameter comes before the key
parameter in the code above, since with for/of
iteration the key
comes first. As noted at the start of this section, you can think of a
map as a generalized array in which integer array indexes are replaced
with arbitrary key values. The forEach()
method of arrays passes the
array element first and the array index second, so, by analogy the
forEach()
method of a map passes the map value first and the map key
second.
The WeakMap class is a variant (but not an actual subclass) of the Map class that does not prevent its key values from being garbage collected. Garbage collection is the process by which the JavaScript interpreter reclaims the memory of objects that are no longer “reachable” and can not be used by the program. A regular Map holds “strong” references to its key values and they remain reachable through the Map, even if all other references to them are gone. The WeakMap, by contrast, keeps “weak” references to its key values so that they are not reachable through the WeakMap and their presences in the map does not prevent their memory from being reclaimed.
There are some important differences between WeakMap and Map:
WeakMap keys must be objects or arrays; primitive values are not subject to garbage collection and cannot be used as keys.
WeakMap implements only the get()
, set()
, has()
and delete()
methods. In particular, WeakMap is not iterable and does not define
keys()
, values()
, or forEach()
. If WeakMap was iterable, then
its keys would be reachable and it wouldn’t be weak.
Similarly, WeakMap does not implement the size
property because
the size of a WeakMap could change at any time as objects are
garbage collected.
The intended use of WeakMap is to allow you to associate values with objects without causing memory leaks. Suppose, for example, that you are writing a function that takes an object argument and needs to perform some time-consuming computation on that object. For efficiency you’d like to cache the computed value for later reuse. If you use a Map object to implement the cache you will prevent any of the objects from ever being reclaimed, but by using a WeakMap you avoid this problem. (You can often achieve a similar result using a private Symbol property to cache the computed value directly on the object. See §5.10.3.)
WeakSet implements a set of objects that does not prevent those
objects from being garbage collected. It implements add()
, has()
,
and delete()
methods. Like WeakMap it is not iterable and does not
have a size
property. WeakSet is not frequently used: its use cases
are like those for WeakMap. If you want to mark (or “brand”) and
object as having some special property or type, for example, you could
add it to a WeakSet. Then, elsewhere when you want to check for that
property or type you can test for membership in that WeakSet. Doing
this with a regular Set would prevent all marked objects from being
garbage collected, but this is not a concern when using WeakSet.
Regular JavaScript arrays can have elements of any type, and can grow
or shrink dynamically. JavaScript implementations perform lots of
optimizations so that typical uses of JavaScript arrays are very
fast. Nevertheless, they are still quite different from the array
types of lower-level languages like C and Java. Typed arrays, which
are new in ECMAScript 6,3 are much closer to the low-level arrays
of those languages. Typed arrays are not technically arrays
(Array.isArray()
returns false
for them) but they implement all of
the array methods described in §6.8 plus a few more of
their own. They differ from regular arrays in some very important
ways, however:
The elements of a typed array are all numbers. Unlike regular JavaScript numbers, however, typed arrays allow you to specify the type (signed and unsigned integers and IEEE-754 floating point) and size (8 bits to 64 bits) of the numbers to be stored in the array.
You must specify the length of a typed array when you create it, and that length can never change.
The elements of a typed array are always initialized to 0 when the array is created.
JavaScript does not define a TypedArray
class. Instead, there are
eleven kinds of typed arrays, each with a different element type and
constructor:
Constructor | Numeric type |
---|---|
|
signed bytes |
|
unsigned bytes |
|
unsigned bytes without rollover |
|
signed 16-bit short integers |
|
unsigned 16-bit short integers |
|
signed 32-bit integers |
|
unsigned 32-bit integers |
|
signed 64-bit BigInt values |
|
unsigned 64-bit BigInt values |
|
32-bit floating-point value |
|
64-bit floating-point value: a regular JavaScript number |
The types whose names begin with Int
hold signed integers, of 1, 2,
or 4 bytes (8, 16, or 32 bits). The types whose names begin with
Uint
hold unsigned integers of those same lengths. The “BigInt” and
“BigUint” types hold 64 bit integers, represented in JavaScript as
BigInt values (see §2.2.5). The types that
begin with Float
hold floating point numbers. The elements of a
Float64Array
are the same type as regular JavaScript numbers. The
elements of a Float32Array
have lower precision and a smaller range,
but only require half the memory. (This type is called float
in C
and Java.)
Uint8ClampedArray
is a special-case variant on Uint8Array
. Both of
these types hold unsigned bytes, and can represent numbers between 0
and 255. With Uint8Array
, of you store a value larger than 255 or
less than zero into an array element, it “wraps around” and you get
some other value. This is how computer memory works at a low level, so
this is very fast. With Uint8ClampedArray
, does some extra
type-checking so that, if you store a value greater than 255 or less
than 0 it “clamps” to 255 or 0 and does not wrap around. (This
clamping behavior is required by the HTML <canvas> element’s low-level
API for manipulating pixel colors.)
Each of the typed array constructors has a BYTES_PER_ELEMENT
property with the value 1, 2, 4, or 8, depending on the type.
The simplest way to create a typed array is to call the appropriate constructor with one numeric argument that specifies the number of elements you want in the array:
let
bytes
=
new
Uint8Array
(
1024
);
// 1024 bytes
let
matrix
=
new
Float64Array
(
9
);
// A 3x3 matrix
let
point
=
new
Int16Array
(
3
);
// A point in 3D space
let
rgba
=
new
Uint8ClampedArray
(
4
);
// A 4-byte RGBA pixel value
let
sudoku
=
new
Int8Array
(
81
);
// A 9x9 sudoku board
When you create typed arrays in this way, the array elements are all
guaranteed to be initialized to 0
, 0n
, or 0.0
. But if you know
the values you want in your typed array, you can also specify those
values when you create the array. Each of the typed array constructors
has static from()
and of()
factory methods that work like
Array.from()
and Array.of()
:
let
white
=
Uint8ClampedArray
.
of
(
255
,
255
,
255
,
0
);
// RGBA opaque white
Recall that the Array.from()
factory method expects an array-like or
iterable object as its first argument. The same is true for the typed
array variants, except that in addition the iterable or array-like
object must have numeric elements. Strings are iterable, for example,
but it would make no sense to pass them to the from()
factory method
of a typed array.
If you are just using the one argument version of from()
, you can
drop the .from
and pass your iterable or array-like object directly
to the constructor function, which behaves exactly the same. Note that
both the constructor and the from()
factory method allow you to copy
existing typed arrays, while possibly changing the type:
let
ints
=
Uint32Array
.
from
(
white
);
// The same 4 numbers, but as ints
When you create a new typed array from an existing array, iterable, or array-like object, the values may be truncated in order to fit the type constraints of your array. There are no warnings or errors when this happens:
// Floats truncated to ints, longer ints truncated to 8 bits
Uint8Array
.
of
(
1.23
,
2.99
,
45000
)
// => new Uint8Array([1, 2, 200])
Finally, there is one more way to create typed arrays that involves the ArrayBuffer type. An ArrayBuffer is an opaque reference to a chunk of memory. You can create one with the constructor; just pass in the number of bytes of memory you’d like to allocate:
let
buffer
=
new
ArrayBuffer
(
1024
*
1024
);
buffer
.
byteLength
// => 1024*1024; one megabyte of memory
The ArrayBuffer class does not allow you to read or write any of the bytes that you have allocated. But you can create typed arrays that use the buffer’s memory, and do allow you to read and write that memory. To do this, call the typed array constructor with an ArrayBuffer as the first argument, a byte offset within the array buffer as the second argument, and the array length (in elements, not in bytes) as the third argument. The second and third arguments are optional. If you omit both, then the array will use all of the memory in the array buffer. If you omit only the length argument, then your array will use all of the available memory between the start position and the end of the array. Given the ArrayBuffer created above, you could create typed arrays like these:
let
asbytes
=
new
Uint8Array
(
buffer
);
// Viewed as bytes
let
asints
=
new
Int32Array
(
buffer
);
// Viewed as 32-bit signed ints
let
lastK
=
new
Uint8Array
(
buffer
,
1023
*
1024
);
// Last kilobyte as bytes
let
ints2
=
new
Int32Array
(
buffer
,
1024
,
256
);
// 2nd kilobyte as 256 integers
These four typed arrays offer four different views into the memory (or
regions of the memory) represented by the ArrayBuffer. One more thing
to bear in mind about this form of the typed array constructor: arrays
must be memory aligned, so if you specify a byte offset the value
should be a multiple of the size of your type. The Int32Array()
constructor requires a multiple of 4, for example, and the
Float64Array()
requires a multiple of 8.
Once you have created a typed array, you can read and write its elements with regular square-bracket notation, just as you would with any other array-like object:
// Return the largest prime smaller than n, using the sieve of Eratosthenes
function
sieve
(
n
)
{
let
a
=
new
Uint8Array
(
n
+
1
);
// a[x] will be 1 if x is composite
let
max
=
Math
.
floor
(
Math
.
sqrt
(
n
));
// Don't do factors higher than this
let
p
=
2
;
// 2 is the first prime
while
(
p
<=
max
)
{
// For primes less than max
for
(
let
i
=
2
*
p
;
i
<=
n
;
i
+=
p
)
// Mark multiples of p as composite
a
[
i
]
=
1
;
while
(
a
[
++
p
])
/* empty */
;
// The next unmarked index is prime
}
while
(
a
[
n
])
n
--
;
// Loop backward to find the last prime
return
n
;
// And return it
}
The function above computes the largest prime number smaller than the
number you specify. The code is exactly the same as it would be with a
regular JavaScript array, but using Uint8Array()
instead of
Array()
makes the code run more than 4 times faster and use 8 times
less memory in my testing.
Typed arrays are not true arrays, but they re-implement most array methods, so you can use them pretty much just like you’d use regular arrays:
let
ints
=
new
Int16Array
(
10
);
// 10 short integers
ints
.
fill
(
3
).
map
(
x
=>
x
*
x
).
join
(
""
)
// => "9999999999"
Remember that typed arrays have fixed lengths, so the length
property is read-only, and methods that change the length of the array
(such as push()
, pop()
, unshift()
, shift()
, and splice()
)
are not implemented for typed arrays. Methods that alter the contents
of an array without changing the length (such as sort()
, reverse()
and fill()
) are implemented. Methods like map()
and slice()
that
return new arrays return a typed array of the same type as the one
they are called on.
In addition to standard array methods, typed arrays also implement a
few methods of their own. The set()
method sets multiple elements of
a typed array at once by copying the elements of a regular or typed
array into a typed array:
let
bytes
=
new
Uint8Array
(
1024
);
// A 1K buffer
let
pattern
=
new
Uint8Array
([
0
,
1
,
2
,
3
]);
// An array of 4 bytes
bytes
.
set
(
pattern
);
// Copy them to the start of another byte array
bytes
.
set
(
pattern
,
4
);
// Copy them again at a different offset
bytes
.
set
([
0
,
1
,
2
,
3
],
8
);
// Or just copy values direct from a regular array
bytes
.
slice
(
0
,
12
)
// => new Uint8Array([0,1,2,3,0,1,2,3,0,1,2,3])
The set()
method takes an array or typed array as its first
argument, and an element offset as its optional second argument, which
defaults to 0 if left unspecified. If you are copying values from one
typed array to another, the operation will likely be extremely fast.
Typed arrays also have a subarray
method that returns a portion of the array
on which it is called:
let
ints
=
new
Int16Array
([
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
]);
// 10 short integers
let
last3
=
ints
.
subarray
(
ints
.
length
-
3
,
ints
.
length
);
// Last 3 of them
last3
[
0
]
// => 7: this is the same as ints[7]
subarray()
takes the same arguments as the slice()
method and
seems to work the same way. But there is an important
difference. slice()
returns the specified elements in a new,
independent typed array that does not share memory with the original
array. subarray()
does not copy any memory; it just returns a new
view of the same underlying values:
ints
[
9
]
=
-
1
;
// Change a value in the original array and...
last3
[
2
]
// => -1: it also changes in the subarray
The fact that the subarray()
method returns a new view of an existing array
brings us back to the topic of ArrayBuffers. Every typed array has three
properties that relate to the underlying buffer:
last3
.
buffer
// The ArrayBuffer object for a typed array
last3
.
buffer
===
ints
.
buffer
// => true: both are views of the same buffer
last3
.
byteOffset
// => 14: this view starts at byte 14 of the buffer
last3
.
byteLength
// => 6: this view is 6 bytes (3 16-bit ints) long
last3
.
buffer
.
byteLength
// => 20: but the underlying buffer has 20 bytes
The buffer
property is the ArrayBuffer of the array. byteOffset
is
the starting position of the array’s data within the underlying
buffer. And byteLength
is the length of the array’s data in
bytes. For any typed array, a, this invariant should always be true:
a
.
length
*
a
.
BYTES_PER_ELEMENT
===
a
.
byteLength
// => true
ArrayBuffers are just opaque chunks of bytes. You can access those bytes with typed arrays, but an ArrayBuffer is not itself a typed array. Be careful, however: you can use numeric array indexing with ArrayBuffers just as you can with any JavaScript object. Doing so does not give you access to the bytes in the buffer, but it can cause confusing bugs:
let
bytes
=
new
Uint8Array
(
8
);
bytes
[
0
]
=
1
;
// Set the first byte to 1
bytes
.
buffer
[
0
]
// => undefined: buffer doesn't have index 0
bytes
.
buffer
[
1
]
=
255
;
// Try incorrectly to set a byte in the buffer
bytes
.
buffer
[
1
]
// => 255: this just sets a regular JS property
bytes
[
1
]
// => 0: the line above did not set the byte
We saw above that you can create an ArrayBuffer with the
ArrayBuffer()
constructor and then create typed arrays that use that
buffer. Another approach is to create an initial typed array, then use
the buffer of that array to create other views:
let
bytes
=
new
Uint8Array
(
1024
);
// 1024 bytes
let
ints
=
new
Uint32Array
(
bytes
.
buffer
);
// or 256 integers
let
floats
=
new
Float64Array
(
bytes
.
buffer
);
// or 128 doubles
Typed arrays allow you to view the same sequence of bytes in chunks of 8, 16, 32, or 64 bits. This exposes the “endianness”: the order in which bytes are arranged into longer words. For efficiency, typed arrays use the native endianness of the underlying hardware. On little-endian systems, the bytes of a number are arranged in an ArrayBuffer from least significant to most significant. On big-endian platforms, the bytes are arranged from most significant to least significant. You can determine the endianness of the underlying platform with code like this:
// If the integer 0x00000001 is arranged in memory as 01 00 00 00, then
// we're on a little endian platform. On a big-endian platform we'd get
// get bytes 00 00 00 01 instead.
let
littleEndian
=
new
Int8Array
(
new
Int32Array
([
1
]).
buffer
)[
0
]
===
1
;
Today, the most common CPU architectures are little-endian. Many network protocols, and some binary file formats, require big-endian byte ordering however. If you’re using typed arrays with data that came from the network or from a file, you can’t just assume that the platform endianness matches the byte order of the data. In general, when working with external data, you can use Int8Array and Uint8Array to view the data as an array of individual bytes, but you should not use the other typed arrays with multi-byte word sizes. Instead, you can use the DataView class, which defines methods for reading and writing values from an ArrayBuffer with explicitly specified byte ordering:
// Assume we have a typed array bytes of binary data to process. First
// we create a DataView object so we can flexibly read and write
// values from those bytes
let
view
=
new
DataView
(
bytes
.
buffer
,
bytes
.
byteOffset
,
bytes
.
byteLength
);
let
int
=
view
.
getInt32
(
0
);
// Read big-endian signed int from byte 0
int
=
view
.
getInt32
(
4
,
false
);
// Next int is also big-endian
int
=
view
.
getUint32
(
8
,
true
);
// Next int is little-endian and unsigned
view
.
setUint32
(
8
,
int
,
false
);
// Write it back in big-endian format
DataView defines ten get
methods for each of the ten typed array
classes (excluding Uint8ClampedArray). They have names like
getInt16()
, getUint32()
, getBigInt64()
, and getFloat64()
. The
first argument is the byte offset within the ArrayBuffer at which the
value begins. All of these getter methods, other than getInt8()
and
getUint8()
, accept an optional boolean value as their second
argument. If the second argument is omitted or is false, big-endian
byte ordering is used. If the second argument is true
, little-endian
ordering is used.
DataView also defines ten corresponding set
methods that write values
into the underlying ArrayBuffer. The first argument is the offset at
which the value begins. The second argument is the value to
write. Each of the methods, except setInt8()
and setUint8()
,
accepts an optional third argument. If the argument is omitted or is
false
, the value is written in big-endian format with most
significant byte first. If the argument is true
, the value is
written in little-endian format with the least significant byte first.
Typed arrays and the DataView class give you all the tools you need to process binary data and enable you to write JavaScript programs that do things like decompressing zip files or extracting metadata from JPEG files.
A regular expression is an object that describes a textual pattern. The JavaScript RegExp class represents regular expressions, and both String and RegExp define methods that use regular expressions to perform powerful pattern-matching and search-and-replace functions on text. In order to use the RegExp API effectively, however, you must also learn how to describe patterns of text using the regular expression grammar, which is essentially a mini programming language of its own. Fortunately, the JavaScript regular expression grammar is quite similar to the grammar used by many other programming languages, so you may already be familiar with it. (And if you are not, the effort you invest in learning JavaScript regular expressions will probably be useful to you in other programming contexts as well.)
The subsections that follow describe the regular expression grammar first, and then, after explaining how to write regular expressions, they explain how you can use them with methods of the String and RegExp classes.
In JavaScript, regular expressions are represented by RegExp objects.
RegExp objects may be created with the RegExp()
constructor, of
course, but they are more often created using a special literal syntax.
Just as string literals are specified as characters within quotation
marks, regular expression literals are specified as characters within a
pair of slash (/
) characters. Thus, your JavaScript code may
contain lines like this:
let
pattern
=
/s$/
;
This line creates a new RegExp object and assigns it to the variable
pattern
. This particular RegExp object matches any string that ends
with the letter “s.” This regular expression could have equivalently
been defined with the RegExp()
constructor like this:
let
pattern
=
new
RegExp
(
"s$"
);
Regular-expression pattern specifications consist of a series of
characters. Most characters, including all alphanumeric characters,
simply describe characters to be matched literally. Thus, the regular
expression /java/
matches any string that contains the
substring “java”. Other characters in regular expressions are not
matched literally but have special significance. For example, the
regular expression /s$/
contains two characters. The first,
“s”, matches itself literally. The second, “$”, is a special
meta-character that matches the end of a string. Thus, this regular
expression matches any string that contains the letter “s” as its
last character.
As we’ll see, regular expressions can also have one or more flag
characters that affect how they work. Flags are specified following
the second slash character in RegExp literals, or as a second string
argument to the RegExp()
constructor. If we wanted to match strings
that end with “s” or “S”, for example we could use the i
flag
with our regular expression to indicate that we want case-insensitive
matching:
let
pattern
=
/s$/i
;
The following sections describe the various characters and meta-characters used in JavaScript regular expressions.
All alphabetic characters and digits match themselves
literally in regular expressions. JavaScript regular-expression syntax
also supports certain non-alphabetic characters through escape sequences
that begin with a backslash (). For example, the sequence
matches a literal newline character in a string.
Table 9-1 lists these characters.
Character | Matches |
---|---|
Alphanumeric character |
Itself |