Fundamentals of Web Application Development · DRAFTFreeman

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:

Data types in JavaScript
TypeExample
undefinedlet a;
a = undefined;
nulla = null;
booleana = true;
numbera = 42;
stringa = "42";
objecta = { b: 42 };
symbola = 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 typeexample
Decimal
let num = 42;
num = 42.0;
num = 4.2e1; // scientific notation
Hexadecimal
let num = 0x2a;
Octal
let num = 0o52;
Binary
let num = 0b101010;

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:

global numeric constants
ConstantValue
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 NaNs 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]; // "!"