Data Types
JavaScript is a dynamically typed language (sometimes referred to as loosely typed). It is tempting to say that it simply “doesn’t have types,” but this is not true. Data values have types, though variables – that is, the lexical “pointers” to values – do not have types. Any given named variable may end up pointing to values of different types at different times in a program’s execution.
JavaScript defines seven data types:
Type | Example |
---|---|
undefined | let a; a = undefined; |
null | a = null; |
boolean | a = true; |
number | a = 42; |
string | a = "42"; |
object | a = { b: 42 }; |
symbol | a = Symbol(); |
Each of these types, except for object
, is a primitive – that is, they
define immutable values.
typeof
Operator
JavaScript provides the typeof
operator to dynamically inspect the type of a
data value. Taking one operand, it returns the name of the value’s type as a
string.
typeof 'abcd'; // "string"
typeof 42; // "number"
typeof true; // "boolean"
typeof { a: 42 }; // "object"
typeof undefined; // "undefined"
typeof null; // "object" <-- oops!
The strings returned from typeof
align with the types of values we just
discussed, with a few notable exceptions. For callable objects (aka
functions), typeof
will return "function"
.
typeof function() {}; // "function"
It’s important to know that functions are objects that just have some special properties (we will discuss more about functions in a later chapter).
Note that typeof
is an operator, not a function. It might be used with
parentheses to look like a function call, however, this is merely a
coincidence.
typeof 'abcd'; // "string"
typeof('abcd'); // "string"
The typeof
operator has a higher precedence than arithmetic operators, thus
the use of parentheses is necessary if trying to determine the type of a
calculated value resulting from an expression.
typeof 1 + 2; // "number2"
// evaluated as:
// -> (typeof 1) + 2
// -> "number" + 2
// -> "number2"
typeof (1 + 2); // "number"
// evaluated as:
// -> typeof (3)
Numbers
Notice that there is only one number
type; no int
, float
, or double
.
Internally, all numeric data are stored as double-precision (64-bit) IEEE-754
floating-point values. Numbers can be expressed in code as numeric literals in
a few different formats: decimal (most common), hexadecimal, octal, and binary.
For example, each of the following statements will assign the same number value
to the variable num
:
numeric literal type | example |
---|---|
Decimal |
|
Hexadecimal |
|
Octal |
|
Binary |
|
There are also three symbolic values for the number type: Infinity
,
-Infinity
, and NaN
.
0 / 42; // 0
42 / 0; // Infinity
-42 / 0; // -Infinity
0 / 0; // NaN
42 / Infinity; // 0
Infinity / Infinity; // NaN
typeof Infinity; // "number"
typeof NaN; // "number"
Number
Global Object
Some useful constants are provided as properties on the global Number
object:
Constant | Value |
---|---|
Number.MIN_VALUE |
≈ 4.941 × 10−324 |
Number.MAX_VALUE |
≈ 1.797 × 10308 |
Number.MIN_SAFE_INTEGER |
−(253 − 1) = −9,007,199,254,740,991 |
Number.MAX_SAFE_INTEGER |
+(253 − 1) = +9,007,199,254,740,991 |
Number.EPSILON |
2-52 ≈ 2.220 × 10−16 |
The Number
object also provides some useful utility methods, such as
Number.isInteger()
and Number.isFinite()
.
Number.isInteger(5); // true
Number.isFinite(5 / 0); // false
Parsing Numbers
There are two build-in global functions for parsing numbers from strings,
parseInt()
and parseFloat()
. These functions make a best-effort attempt to
parse a string into a number; they ignore any leading whitespace and will
traverse the string character by character, in order, until it reaches an
invalid character. If the first character is invalid, it will return NaN
.
Note that parseInt()
will truncate, not round, any decimal fraction.
parseInt('123'); // 123
parseInt('123abc'); // 123
parseInt('-123abc'); // -123
parseInt('abc123'); // NaN
parseInt('0.123'); // 0
parseInt('.123'); // NaN
parseFloat('.123'); // 0.123
parseFloat('-.123'); // -0.123
parseFloat('-.123abc'); // -0.123
parseFloat('123'); // 123
parseFloat('Infinity'); // Infinity
For parseInt()
, we can optionally specify a radix using a second argument,
which is base-10 by default. However, if the string starts with '0x'
(or
'0X'
), it will instead default to base-16. (The computed values output in the
comments below are in base-10.)
parseInt('123', 10); // 123
parseInt('123', 8); // 83
parseInt('10101', 2); // 21
parseInt('123abc', 16); // 1194684
parseInt('123ABC', 16); // 1194684 <-- case insensitive
parseInt('asdf', 16); // 10 <-- parsing stops at 's'
parseInt('abc'); // NaN
parseInt('0xabc'); // 2748
Unlike parseInt()
, parseFloat()
can also understand exponents (i.e.,
scientific notation).
parseInt('2e3'); // 2
parseFloat('2e3'); // 2000
parseFloat('2000E-3'); // 2
parseFloat('6.0221409e+23'); // 6.0221409e+23
There is one other way to try and get numbers from a non-number type. The global
Number
object is a function which can be invoked by itself with one argument
to be coerced to a primitive number
value. Though this can sometimes be used
interchangeably with the parse{Int|Float}()
functions, it is important to note
that this type coercion is an all-or-nothing approach; if it is passed a string
that contains any invalid characters, it will return NaN
.
Number('123'); // 123
parseInt('123abc'); // 123
Number('123abc'); // NaN
Number('0x123abc'); // 1194684
Which method you use to convert values to number primitives depends on your use
case. As stated previously, the parse
functions do a best-effort conversion,
accepting input until they read an invalid character. This may be desirable, for
example, if trying to obtain number values from arbitrary string input that may
include trailing characters. On the other hand, using explicit coercion with
Number()
may prove to be more predictable, and it brings with it the assurance
of knowing whether a given value completely represents a valid number.
NaN
You will quite likely have to deal with NaN
(Not-a-Number) in your programs,
especially if you are obtaining numeric values by parsing string input.
Unanticipated NaN
s can be especially tricky to catch in debugging, as any
later mathematical calculations using the value NaN
will execute without
throwing any errors, usually returning NaN
again.
let badInput = 'abcd'; // "abcd"
let oopsNum = parseInt(badInput); // NaN
let twiceNum = oopsNum * 2; // NaN
console.log('Twice your number is', twiceNum);
// --> "Twice your number is NaN"
And, if that weren’t tricky enough for you, NaN
is the only value in
JavaScript that is strictly not equal to itself.
NaN === NaN; // false
let a = parseFloat('asdf1234'); // NaN
a === NaN; // false
We can detect whether or not a value is NaN
by using the built-in function
Number.isNaN()
.
Number.isNaN(NaN); // true
Number.isNaN(42); // false
Number.isNaN('NaN'); // false <-- string "NaN" is not of type number
Number.isNaN(undefined); // false
There is a standalone global function isNaN()
with slightly different
behavior. Whereas Number.isNaN()
returns true
only if the argument passed is
the value NaN
itself, isNaN()
will perform type coercion on its argument
before deciding whether the value is congruent with NaN
.
typeof isNaN; // "function"
isNaN === Number.isNaN; // false
isNaN(NaN); // true
isNaN(42); // false
// NOTE: isNaN() uses type coercion!
isNaN('NaN'); // true <-- coerced 'NaN' to NaN
isNaN('wow'); // true <-- coerced 'wow' to NaN
isNaN('42'); // false <-- coerced "42" to 42
isNaN(true); // false <-- coerced true to 1
isNaN(null); // false <-- coerced null to 0
isNaN(undefined); // true <-- coerced undefined to NaN
If the coercion seems a little confusing, don’t worry – we’ll revisit coercion in greater detail in the next chapter.
Strings
JavaScript strings are sequences of characters encoded in UTF-16. Unlike in many other languages, string primitives are immutable. Strings can be constructed in a few different ways:
let myStr1 = 'Hello, "world".'; // «Hello, "world".»
let myStr2 = "Hello, 'world'."; // «Hello, 'world'.»
let myStr3 = String(42); // "42"
let myStr4 = String.raw`Hello, \n world.`; // "Hello, \n world."
let myStr5 = `"Hello", 'world'.`; // «"Hello", 'world'.»
JavaScript does not distinguish between strings created with single quotes
('abc'
) or double quotes ("abc"
) except for determining which quote
character must be escaped inside, as shown in the first two examples. The third
example uses the global String
object, invoking it as a function, to coerce a
given value to its string primitive representation. (Notice that we passed in
the numeric value 42
and were returned the string "42"
.)
As shown in the fourth example, the String.raw
method may be used, immediately
followed by a template literal string (discussed below), to produce strings
without escape sequences. In this example, the characters \n
, which would
normally be stored as a single newline character, are not escaped; they are
stored as normal \
(backslash) and n
characters.
The last example above (myStr5
) shows a string constructed using template
literal syntax, a form introduced in ES2015. Template literals begin and end
with the back-tick character (`
), which allows the use of single and
double quotes inside of them without need for escaping. As the name “template”
might suggest, template literals can contain placeholder expressions whose value
string representation will be interpolated into the rest of the string literal.
A template expression is enclosed in curly braces and preceded by a dollar sign,
as ${ /* expression */ }
.
let a = 3;
let b = 4;
`${a} plus ${b} equals ${a + b}.`; // "3 plus 4 equals 7"
Strings in normal (single or double) quotes may not have line breaks inside of
the source, which would cause a SyntaxError
during the enclosing function’s
compile time.
// throws SyntaxError: Invalid or unexpected token
let myStrBreak = " Hello,
world!";
Newline characters (amongst other special characters) can be backslash-escaped
inside of strings. Very long strings (with or without newlines) can be broken
across multiple lines using the string concatenation operator (+
);
let myStrBreak = 'Hello,\n\nworld!';
// Hello,
//
// world!
let myLongStr =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod' +
' tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim' +
' veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex' +
' ea commodo consequat.';
// myLongStr contains no line breaks
String template literals allow arbitrary line breaks in the source, which can come in handy if you’d like to use multiline strings for something like marking up a bit of HTML.
let myPage = {
title: 'hello, world',
introText: 'Lorem ipsum dolor sit amet...',
};
let myHTMLStr = `
<h1>${myPage.title}</h1>
<p class="intro-text">${myPage.introText}</p>
`;
A built-in global String
object provides standard string searching and
manipulation methods. Even though string literals are primitive values, you can
syntactically invoke methods and inspect properties on them as if they were
objects, and the JavaScript engine will implicitly coerce them to String
objects in the background.
let myStr = 'Hello, world.';
myStr.length; // 13
myStr.includes('Hello'); // true
myStr.endsWith('.'); // true
'JavaScript is FUNctional!'.indexOf('FUN'); // 14
String manipulation methods act in a similar way, but notice that they return new string primitive values instead of modifying the existing value, as string primitives are immutable.
let str = 'JavaScript is cool.';
str.replace('cool', 'awesome'); // "JavaScript is awesome."
str; // "JavaScript is cool."
When you get a chance, take some time to browse through MDN’s comprehensive and
useful list of global String
methods:
Regular Expressions
// TODO: move to different chapter
Regular expression literals are contained within forward-slash characters (/
),
optionally followed by regex modifier flags. These are commonly used in
combination with String
’s .match()
method.
let funString = 'Functions are fun.';
funString.match(/fun[\w]*/gi); // ["Functions", "fun"]
Objects
In their simplest form, objects in JavaScript are essentially collections of key-value pairs called properties. Each property’s key is a string or symbol primitive, and its value is one of the seven data types we discussed at the beginning of this section.
The shortest and most common way to create an object is by using object literal
notation, also called object initializer syntax, in which a new object is
enclosed by curly braces ({}
) and its properties are listed as comma-separated
key: value
pairs.
let emptyObj = {};
let otherObj = new Object(); // Object-oriented style; not common
let myObj = {
a: 42,
b: `some string`,
c: 100.5,
};
myObj.a; // 42
As long as a key name is a valid identifier or numeric literal, it does not have
to be given as a string literal (in quotes). Properties with valid identifier
key names can be accessed using dot notation. In the last line of the code
above, the property with key 'a'
on myObj
was accessed using dot notation.
If a key name is not a valid identifier or numeric literal, then it must be given as a string literal.
let myObj = {
a: 42,
1: 'one',
'key name with spaces': 100,
};
myObj['key name with spaces']; // 100
Here we also see the second way of accessing object properties: array or
bracket notation. Using this notation, a value – or an expression which
evaluates to a value – is given inside of the square brackets ([]
), where it
is coerced into its string representation (if needed) in order to access the
property with the matching key.
Bracket notation can also be used in object literals to define properties with
computed key names
let a = 'ans';
let b = 'wer';
let myObj = {
[a + b]: 42,
};
myObj['answer']; // 42
Remember that all properties’ key names are string or symbol primitives. Property names declared using numeric literals are implicitly coerced to their string representation. Additionally, because the key names are not valid identifiers (as they start with a number), they cannot be accessed using dot notation.
let myObj = { 1: 'one', 42: 'forty-two' };
myObj[1]; // "one"
myObj["42"]; // "forty-two"
myObj[40 + 2]; // "forty-two"
myObj.42; // would cause a SyntaxError
null
& undefined
Why does JavaScript have two types which essentially mean “nothing”? In many
ways, null
and undefined
act in much the same manner and are interchangeable
in some situations. Both values are falsy in a boolean context, and they care
considered loosely equal (==
) (more on these concepts in the next section).
Conceptually, however, null
and undefined
are very subtly different.
Loosely speaking, undefined
is usually taken to mean the unintentional or
“default” absence of a value. The ECMAScript Spec defines undefined
as the
“primitive value used when a variable has not been assigned a value.” For
example, failed property lookups will result in undefined
, and variables which
have been declared but not assigned a value will, by default, hold the value
undefined
. Named function parameters will be given the value of undefined
if
excluded at the function’s invocation, and functions without an explicit
return
statement will implicitly return undefined
at the end of their
execution.
On the other hand, null
is usually taken to represent an intentional absence
of a value. If you encounter the value null
during execution of a program, it
is likely because the value was purposefully set somewhere else in the code.
undefined == null; // true
undefined === null; // false
let a;
a; // undefined
let b = { c: 42 };
b.d; // undefined
typeof foo; // "undefined"
When coerced to a number, null
will become 0
while undefined
will coerce
to NaN
.
Number(undefined); // NaN
Number(null); // 0
Primitive Wrapper Objects
Javascript provides global “wrapper objects” that serve to extend the
functionality of primitive values, which include String
, Number
, and
Boolean
(note that each starts with an upper-case letter).
These objects contain some useful static methods or constants, as we’ve briefly seen before. They are also functions, and can be used to try to explicitly coerce any value into a primitive of a certain type.
Boolean(1); // true
Number('123'); // 123
String(123); // "123"
Symbol('wow'); // Symbol(wow)
However, when invoked as constructor functions using the new
keyword, the
global wrappers instead return a new wrapper object which “wraps” the resulting
primitive value.
Each of these wrapper objects has a .valueOf()
method which returns its
contained primitive value.
Boolean(1); // true
new Boolean(1); // [Boolean: true]
typeof Boolean(1); // "boolean"
typeof new Boolean(1); // "object"
let a = new Boolean(1);
a.valueOf(); // true
typeof a.valueOf(); // "boolean"
String
wrapper objects especially offer a great deal of useful functionality,
as we saw earlier.
let str = new String('wow!');
typeof str; // "object"
typeof str.valueOf(); // "string"
str.length; // 4
str[0]; // "w"
str[3]; // "!"