JavaScript data types

JavaScript data types are an essential piece to understanding modern JavaScript. Although JavaScript data types can be a rather dry subject, the topic holds an important key to making better JavaScript design choices, which includes discovering techniques to achieve results with faster execution times and less typing.

Similar to JavaScript key concepts & concepts explored in the previous chapter, JavaScript data types have evolved considerably throughout the various ECMAScript versions. In this chapter you'll learn about JavaScript data types and their dynamically typed behavior, how there are only a few core JavaScript data types, how these JavaScript data types have evolved over time, as well as how more sophisticated JavaScript data types have been incorporated into later ECMAScript versions to support more advanced constructs.

Dynamically typed behavior and data types

JavaScript was conceived as a dynamically typed language. This means data types get determined automatically at run-time and it isn't necessary to declare variable data types. Although this creates a much quicker development process, it also introduces subtle data type bugs, that would otherwise be easily detectable in statically typed languages (e.g. Java, C#) by means of a compiler. Listing 4-1 illustrates JavaScript's dynamically typed behavior.

Listing 4-1. JavaScript dynamically typed behavior
let x = 1;
console.log(typeof(x));
x = ["one"];
console.log(typeof(x));
x = "1";
console.log(typeof(x));
let y = 2;
console.log(x + y);

As you can see in listing 4-1, the x variable is first interpreted as a number, then as an array and finally as a string. Although this cuts down on the amount of typing, notice the last operation x + y that involves the y = 2 variable. Because the last x assignment is a string -- x = "1" -- the x + y operation does not result in a mathematical operation, but rather a string concatenation, therefore x + y results in 12, not in 3. This can seem like a trivial issue to detect in this case, but in projects spanning hundreds or thousands of lines it can result in odd bugs that are difficult to diagnose.

This dynamically typed behavior is one of the main value propostions of TypeScript, a JavaScript superset that supports static typing and is eventually transpiled into JavaScript. There's a dedicated chapter on TypeScript and a dedicated section TypeScript - Static type syntax which expands on this statically typed feature.

Primitive data types

JavaScript like most programming languages has a group of data types called primitives. JavaScript supports seven primitives data types:

As you can see from the examples in this list, primitives are easy to identify because they can't be de-constructed. Which is to say, a number primitive is a number and a null primitive is null, no matter how you look at them. This is in contrast to other data types, like objects, that have more elborate structures that can be de-constructed into smaller parts.

Primitive data types by themselves don't have any functionality besides representing their value. However, five of the seven primitive data types -- string, number, boolean, symbol and bigint -- harness the functionalities from their equivalent full-fledged JavaScript object data types.

The string primitive data type and String object data type  

A string primitive can look like a pretty boring data type that holds a series of characters. But behind the scenes, a JavaScript string primitive provides the same functionalities -- methods and properties -- available in the more robust String object data type. So one upside of string primitives is that you don't need to do anything special for them to gain access to the more than 60+ properties and methods in the String object data type.

A string primitive is typically created through a literal string value declared in a pair of '', "" or ``, see working with text: ' and "; plus template literals with backticks ` and ${} for additional details. However, it's also possible to create a string primitive through the String() constructor of the String object data type. The outcome of using a literal string or the String() constructor is the same, so long as the String() constructor is called as a plain function.

This means the String() constructor of the String object data type can be used in one of two ways: called as a plain function to create a string primitive or called with the new keyword to create a full-fledged String object data type. The difference between these two approaches is subtle but very important, since they produce different results.

Listing 4-2 illustrates the different ways to create a string primitive, as well as the implications of using the new keyword in the creation process.

Listing 4-2. string primitive creation and implications of new on String object data type
// string primitive with literal string
let x = "JavaScript";
// string primitive, with String() constructor as plain function
let y = String("JavaScript");
// String object, with String() constructor and "new"
let z = new String("JavaScript");

// Check types
console.log("x is a %s", typeof x);
console.log("y is a %s", typeof y);
console.log("z is a %s", typeof z);

// Validate equivalencies 
if (x == y) { 
  console.log("x == y is True");
} else {
  console.log("x == y is False");
}

if (x === y) { 
  console.log("x === y are True");
} else { 
  console.log("x === y are False");
}

// Validate equivalencies
if (x == z) { 
  console.log("x == z is True");
} else {
  console.log("x == z is False");
}

if (x === z) { 
  console.log("x === z is True");
} else {
  console.log("x === z is False");
}

// Leverage properties and methods in all 
console.log(x.length);
console.log(x.indexOf("S"));
console.log(x.substring(4));
console.log(y.length);
console.log(y.indexOf("S"));
console.log(y.substring(4));
console.log(z.length);
console.log(z.indexOf("S"));
console.log(z.substring(4));


Listing 4-2 begins with the creation of three variables: let x = "JavaScript"; that uses a literal string value; let y = String("JavaScript"); that uses the String() constructor as a plain function; and let z = new String("JavaScript"); that uses the String() constructor with the new keyword. As you can see the "JavaScript" value is the same for all three, but are they really the same ?

The second set of statements in listing 4-2 uses the typeof operator to inspect the JavaScript data type for all three variables. Notice that x and y both output string indicating a string primitive, whereas z outputs object indicating a String object data type. To further confirm equivalencies between variables, the next set of the statements compares them.

Behind the scenes, the String("JavaScript"); statement calls String.toString("JavaScript"), so you end up with an identical value to using the literal string "JavaScript" syntax. This is confirmed with the equality == operator and strict equality === operator (a.k.a. identity operator), indicating both x and y have the same value and data type, as described in equality symbols ==, !=, === and !==. The equivalency operations for the z variable show a different story, because although the loose equality == operator confirms z has the same "JavaScript" value as x (and by implication y), the strict equality === operator confirms the z data type is different than x (and by implication y).

While the new operator is perfectly valid JavaScript OOP (Object Orientated Programming) syntax, its use is discouraged in data types that have equivalent primitive data types, since it allows access to the prototype functionalities in a data type and its prototype chain, opening the door to altering data type behaviors. So essentially, you shouldn't use the new keyword with String, Number, Boolean, Symbol and BigInt constructors. The next chapter contains the section to new or not to new with additional details about the influence of using the new keyword with data types.

Finally, the last section in listing 4-2 illustrates how both string primitives x and y, as well as the String object z have access to properties and methods in the String object data type.

The String object property to determine string size: .length  

A String object and by implication a string primitive, has access to a single property: .length. The .length property is a read only property that returns the length of a string in code units. In most cases, a code unit equals a character, but sometimes it doesn't. To understand when this can happen, it's necessary to explore how JavaScript engines handle text.

JavaScript engines use UTF-16[1] internally to represent text. UTF-16 is a method to encode characters as one or two 16-bit elements or code units. The first 16-bit element or code unit is capable of supporting 216 or 65,536 characters, which includes characters and symbols used in most modern languages. For characters beyond this threshold, UTF-16 requires a second 16-bit element or code unit to represent characters, hence the existence of characters that use two code units. Fortunately, characters that require two code units are limited to more specialized characters, such as: emoji symbols, musical symbols, historic characters and less used Chinese, Japanese and Korean (CJK) characters.

Listing 4-2-A illustrates various examples of the .length property, including characters that require two code units resulting in the character count being different than the .length property count.

Listing 4-2-A. String object .length property
// strings where characters are one code unit 
let x = "JavaScript";
console.log(x.length); // 10
let y = String("àèìòù");
console.log(y.length); // 5
let z = ".length!=characters";
console.log(z.length); // 19

// strings with characters that use two code units
let clef = "𝄞"; // "\u{1d11e}";
console.log(clef.length); // 2
let emoji = ".length!=characters😅"; // "\u{1F605}"
console.log(emoji.length); // 21

As you can see in listing 4-2-A, although for most cases a string's character count matches its code unit count, for cases where a string uses more specialized characters, JavaScript's .length property can produce a count that's different than a string's character count.

Tip The text and escape sequences section in the previous chapter discuss more details about UTF-16 and code points that use one or two code units.

The String object static methods to output strings: String.fromCharCode()  ; String.fromCodePoint() and String.raw()  

JavaScript static methods are designed to be called directly as part of the String data type, like so String.<static_method>(input). This means such methods are not intended to be used directly on pre-existing String objects or by implication pre-existing string primitives. As such, String object static methods produce strings vs. modifying pre-existing strings.

The String object supports the following static methods:

The String.fromCharCode() static method is a predecessor to the String.fromCodePoint() static method introduced in ES6 (ES2015). Both static methods are capable of producing the same results, but the earlier String.fromCharCode() static method doesn't validate input values and requires more effort to input certain code points vs. the String.fromCodePoint() static method which validates input values and supports more straightforward code point input values. Inclusively, both static methods mirror the unicode escape sequences \u and \u{} -- described in the escape sequences section in the previous chapter -- with the latter introduced in ES6 (ES2015) to support more straightforward input versus the former.

Listing 4-2-B illustrates various examples of the String.fromCharCode() static method and String.fromCodePoint() static method.

Listing 4-2-B. Static methods String.fromCharCode() & String.fromCodePoint()
// Code points < 65,536 or < 0xFFFF
// Text with String.fromCharCode()
let xDec = String.fromCharCode(74, 97, 118, 97, 83, 99, 114, 105, 112, 116);
let xHex = String.fromCharCode(0x4A, 0x61, 0x76, 0x61, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74);
// Text with String.fromCodePoint()
let yDec = String.fromCodePoint(74, 97, 118, 97, 83, 99, 114, 105, 112, 116);
let yHex = String.fromCodePoint(0x4A, 0x61, 0x76, 0x61, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74);

console.log("xDec value is: %s", xDec);
console.log("xHex value is: %s", xHex);
console.log("yDec value is: %s", yDec);
console.log("yHex value is: %s", yHex);

// Code points > 65,536 or > 0xFFFF
// String.fromCharSet accepts but truncates code point decimals > 65,536
// and code point hex > 0xFFFF
let clefDecChar = String.fromCharCode(119070);
let emojiDecChar = String.fromCharCode(128517);
let clefHexChar = String.fromCharCode(0x1D11E);
let emojiHexChar = String.fromCharCode(0x1F605);

console.log("clefDecChar value is: %s", clefDecChar); // truncated output
console.log("emojiDecChar value is: %s", emojiDecChar); // truncated output
console.log("clefHexChar value is: %s", clefHexChar); // truncated output
console.log("emojiHexChar value is: %s", emojiHexChar); // truncated output

// code point decimals truncated with 65,536 until valid range is reached
// 119070 - 65536 = 53534
if (clefDecChar == String.fromCharCode(53534)) { 
     console.log("String.fromCharCode(119070) == String.fromCharCode(53534)")
}
// 128517 - 65536 = 62981
if (emojiDecChar == String.fromCharCode(62981)) { 
     console.log("String.fromCharCode(128517) == String.fromCharCode(62981)")
}
// code point hex truncated to four hex digits
// 0x1D11E truncated to 0xD11E
if (clefHexChar == String.fromCharCode(0xD11E)) { 
     console.log("String.fromCharCode(0x1D11E) == String.fromCharCode(0xD11E)");
}
// 0x1F605 truncated to 0xF605
if (emojiHexChar == String.fromCharCode(0xF605)) { 
     console.log("String.fromCharCode(0x1F605) == String.fromCharCode(0xF605)");
}

// String.fromCharSet can handle code point hex > 0xFFFF as surrogate pairs
let clefHexSurrogateChar = String.fromCharCode(0xD834, 0xDD1E);
let emojiHexSurrogateChar = String.fromCharCode(0xD83D, 0xDE05);

console.log("clefHexSurrogateChar value is: %s", clefHexSurrogateChar);
console.log("emojifHexSurrogateChar value is: %s", emojiHexSurrogateChar);

// String.fromCodePoint handles code point decimals up to 1,114,111
// and code point hex from 0x0000 to 0x10FFFF
let clefDecCP = String.fromCodePoint(119070);
let emojiDecCP = String.fromCodePoint(128517);
let clefHexCP = String.fromCodePoint(0x1D11E);
let emojiHexCP = String.fromCodePoint(0x1F605);

console.log("clefDecCP value is: %s", clefDecCP);
console.log("emojiDecCP value is: %s", emojiDecCP);
console.log("clefHexCP value is: %s", clefHexCP);
console.log("emojiHexCP value is: %s", emojiHexCP);

// Code points > 1,114,111 or > 0x10FFFF 
// String.fromCodePoint detects out of range values
try {
  let hexCPOver = String.fromCodePoint(2500000);
} catch (error) {
   console.error("Error is %s : %s", error.name, error.message);
}
try {
  let decCPOver = String.fromCodePoint(0x20FFFF);
  } catch (error) {
   console.error("Error is %s : %s", error.name, error.message);
}

The first set of definitions in listing 4-2-B use code points below the 65,536 decimal / 0xFFFF hexadecimal thresholds, which in unicode are said to belong to the "Basic multilingual plane" (BMP) and are code points representing characters and symbols used in most modern languages. You can see the xDec, xHex, yDec and yHex variables all have the "JavaScript" string value assigned to them, using the String.fromCharCode() method and String.fromCodePoint() method, as well as using decimal code point input values (i.e. base 10) and hexadecimal code point input values (i.e. base 16). Because all characters in the "JavaScript" string are represented with code points below 65,536 decimal / 0xFFFF hexadecimal, there's no difference in how the String.fromCharCode() and String.fromCodePoint() methods work.

Note The hexadecimal code point values used in listing 4-2-B (e.g. 0x4A) are the same ones used in listing 3-10 to represent escape sequences (e.g. \x4A, \u004A, \u{004A})

The second set of definitions in listing 4-2-B use code points above the 65,536 decimal / 0xFFFF hexadecimal thresholds, which in unicode are said to belong to "supplementary planes" to handle code points for more specialized characters and symbols up to 1,114,111 decimal / 0x10FFFF hexadecimal. In this case, you can see the musical clef symbol 𝄞 and the emoji symbol 😅 are output with both the String.fromCharCode() and String.fromCodePoint() methods, netting different results and illustrating the differences between both static methods.

The clefDecChar, emojiDecChar, clefHexChar and emojiHexChar variables attempt to use the String.fromCharCode() method to output the musical clef symbol 𝄞 and the emoji symbol 😅 using their decimal and hexadecimal code points. In all cases, you can see the String.fromCharCode() method happily accepts the code points even though they're out of range. For decimal code points that are out of range the String.fromCharCode() method subtracts 65,536 until it reaches a valid decimal code point (i.e. below 65,536) and for hexadecimal code points that are out of range the String.fromCharCode() method truncates the value to reach a valid four hexadecimal code point (i.e. below 0xFFFF) so a code point like 0x2F123 becomes 0xF123. You can see in the various conditionals from listing 4-2-B, the net result of passing out of range code points to the String.fromCharCode() method are truncated/random characters (e.g. String.fromCharCode(119070) doesn't produce the clef symbol, it produces the same output as String.fromCharCode(53534) which is an entirely different symbol).

However, all is not lost with the String.fromCharCode() method, the clefHexSurrogateChar and emojiHexSurrogateChar variables show how it's possible to output the musical clef symbol 𝄞 and the emoji symbol 😅 with surrogate pairs. While the String.fromCharCode(0xD834, 0xDD1E) and String.fromCharCode(0xD83D, 0xDE05) statements use a pair of code points, the output for each of these code point pairs is a single character, in this case, the musical clef symbol 𝄞 and the emoji symbol 😅 , respectively. The reason this works is because two hexadecimal values can be used as a pseudo-representation to output characters or symbols whose code points are in reality above the permitted 0xFFFF hexadecimal threshold. Pairs of hexadecimal values that can achieve this are called surrogate pairs and you can see the escape sequences section in the previous chapter for additional context about surrogate pairs and their use.

Finally, the last statements in listing 4-2-B illustrate how the String.fromCodePoint() method generates an error in case it's given code points that aren't inside its range 0 to 1,114,111 decimal / 0x0000 to 0x10FFFF hexadecimal.

Why does String.fromCodePoint() support decimal code points up to 1,114,112 (0 to 1,114,111) ?
And unicode supports up to 1,112,064 character code points ?

This difference is due to reserved surrogate pairs. There are 2048 reserved surrogate code points (1024 high-surrogate, 1024 low-surrogate), which makes 1,114,112-2048 = 1,112,064. Since surrogate code points are used for pseudo-representation, they're not contemplated as code points that generate unicode characters.

The String.raw() static method is typically called with a template literal immediately after its name (e.g. String.raw``) to produce a string made from the template with its substitutions, but ignoring all escape sequences in the template. A less used technique for the String.raw() static method is to use it as a tagged template, where the first argument is an object with the raw key and value as template parts, plus the remaining arguments work as substitutes for the template.

Listing 4-2-C illustrates to two ways you can use String.raw() static method.

Listing 4-2-C. Static method String.raw`` or String.raw()
let today = new Date();
// template literal with escape sequences
let lorem = `"\u{2460} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n \u{2461} Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n \u{2462} Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n \u{2463} Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n ${today}";`

console.log(lorem);

// template literal with escape sequences ignored due to String.raw
let loremRaw = String.raw`"\u{2460} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n \u{2461} Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n \u{2462} Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n \u{2463} Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n ${today}";`

console.log(loremRaw);

// tagged template with String.raw
let email = String.raw({raw:["Hi ", ",\n\nI read...\n\nThanks!\n", ""]}, "Jen", "Sally");

console.log(email);

Listing 4-2-C starts by declaring a plain template literal in the lorem variable with the ${today} substitution and multiple escape sequences (e.g. \n, \u{2460}, \u{2461}). Next, you can see how the lorem variable is rendered -- the default for all template literals -- with its substitutions replaced and escape sequences rendered based on their purpose (e.g. \n as a new line, \u{2460} as the ① character).

Next in listing 4-2-C is the loremRaw variable which uses the String.raw`` static method with the same template literal as the lorem variable. Notice that while the output of the loremRaw replaces the ${today} substitution, the escape sequences in the template literal are output verbatim.

Finally, the email variable is used to store a string from the String.raw() static method used as a tagged template. In this case, the first argument {raw:["Hi ", ",\n\nI read...\n\nThanks!\n", ""]} represents the template parts, where the {raw: } object is a requirement and the object value are the template parts. The remaining arguments "Jen" and "Sally" work as substitutes to be added in between the template parts. The resulting value assigned to the email variable is "Hi Jen,\n\nI read...\n\nThanks!\nSally". Note that unlike the String.raw`` static method technique, the escape sequences remain unchanged when the String.raw() static method is used in this manner. It's also worth pointing out this tagged template example that uses the String.raw() static method is the same one presented in listing 3-9 that uses plain tagged templates.

The number primitive data type, Number object data type and their limits  

A number primitive is used to hold numerical values and it's created and operates like the string primitive. A number primitive is generally created with a literal number (e.g. 30, 30.0), a number primitive also provides the same functionalities -- methods and properties -- available in the more robust Number object data type[2], and a number primitive can also be created using the Number() constructor as a plain function.

Listing 4-3 illustrates the different ways to create a number primitive, as well as the different values it's able to accept.

Listing 4-3. number primitive creation
// number primitive, with literal decimal number
let a = 30;
let b = 30.0;
// number primitive, with literal exponential number
let c = 3e1;
let d = 300e-1;
// number primitive, with literal binary number
let e = 0B00000000000000000000000000011110;
let f = 0b00000000000000000000000000011110;
// number primitive, with literal octal number
let g = 0O036;
let h = 0o036;
// number primitive, with literal hexadecimal number
let i = 0X1E;
let j = 0x1E;
// number primitive, with Number() constructor as plain function
let k = Number(30);
let l = Number("30");

console.log("a is %s with value: %s", typeof a, a);
console.log("b is %s with value: %s", typeof b, b);
console.log("c is %s with value: %s", typeof c, c);
console.log("d is %s with value: %s", typeof d, d);
console.log("e is %s with value: %s", typeof e, e);
console.log("f is %s with value: %s", typeof f, f);
console.log("g is %s with value: %s", typeof g, g);
console.log("h is %s with value: %s", typeof h, h);
console.log("i is %s with value: %s", typeof i, i);
console.log("j is %s with value: %s", typeof j, j);
console.log("k is %s with value: %s", typeof k, k);
console.log("l is %s with value: %s", typeof l, l);

Tip You can use the underscore _ character to separate units, hundreds or thousands and make it easier to read number values. See

Listing 4-3 declares twelve variables a through l, ten using literal numbers and two using the Number() constructor as a plain function. The interesting aspect of these statements is in the second half of listing 4-3 which outputs the type and value for each variable: all twelve variables are number types and have a value of 30.

That all twelve variables in listing 4-3 are number primitives is likely not surprising, since it falls in line with the creation process of string primitives, that a number primitive can be created using either a literal number or the Number() constructor as a plain function.

What may be surprising from listing 4-3 is that all the values equal 30. There are two reason for this behavior.

The first reason is that just like a literal string can be declared in a pair of '', "" or ``, a literal number can also be declared in various ways. A JavaScript literal number can be declared in five formats: decimal (e.g. 30, 30.0); exponential with an e symbol (e.g. 3e1, 300e-1); binary with a leading 0B or 0b (e.g. 0B00000000000000000000000000011110, 0b00000000000000000000000000011110); octal with a leading 0O or 0o (e.g. 0O036, 0o036); or hexadecimal with a leading 0X or 0x (e.g. 0X1E, 0x1E).

The second reason is that number primitives store their value as a double-precision (64-bit) floating point number -- per the IEEE 754 standard. So while the literal numbers in listing 3-4 might all look different, they all represent 30 as a double-precision (64-bit) floating point number. This underlying storage behavior as you can imagine has implications when performing operations on number primitives.

These are the behaviors you should be aware about number primitives storing values as double-precision (64-bit) floating point numbers:

Listing 4-4 illustrates various examples of number primitives and their limits presented in this last list.

Listing 4-4. number primitives and their limits
// Decimal precision up to 15 positions
let a = 1;
let b = 1.000000000000001;
let c = 1.0000000000000001;

if (a == b) {
   console.log("a == b")
   console.log("%s == %s", a, b);
} else { 
   console.log("a != b")
   console.log("%s != %s", a, b);
}

if (a == c) {
   console.log("a == c")
   console.log("%s == %s", a, c);
} else { 
   console.log("a == c")
   console.log("%s == %s", a, c);
}

// Integer precision
let maxInt         =  9007199254740991; // Number.MAX_SAFE_INTEGER
let maxIntLarge    =  9007199254740992;
let maxIntLarger   =  9007199254740993;
let minInt         =  -9007199254740991; // Number.MIN_SAFE_INTEGER
let minIntSmall    =  -9007199254740992;
let minIntSmaller  =  -9007199254740993;

console.log("maxInt = %s", maxInt);
console.log("maxIntLarge = %s", maxIntLarge);
console.log("maxIntLarger = %s", maxIntLarger);
console.log("minInt = %s", minInt);
console.log("minIntSmall = %s", minIntSmall);
console.log("minIntSmaller = %s", minIntSmaller);

// Max and min values
let maxValue = 1.7976931348623157e+308; // Number.MAX_VALUE
let minValue = 5e-324; // Number.MIN_VALUE

console.log("maxValue + 1e+308 = %s", maxValue + 1e+308); // Number.POSITIVE_INFINITY
console.log("minValue - 1e+324 = %s", minValue - 1e+324); // Number.NEGATIVE_INFINITY

// NaN value
let fooNumber = Number("Foo"); // Number.NaN
console.log("fooNumber = %s", fooNumber);

Tip You can use the underscore _ character to separate units, hundreds or thousands and make it easier to read number values. See

The first set of variables in listing 4-4 a, b, c are all ones with different decimals. Next, notice the results of the equality operations. Comparing a that's 1 without decimals and b that's a 1 with fifteen decimals the last one being one, results in them being different, as expected. However, comparing a that's 1 without decimals and c that's a 1 with sixteen decimals the last one being one, results in them being equal ! Confirming that decimal precision for number primitives can be up to fifteen decimals.

The second set of variables in listing 4-4 illustrates the maximum and minimum integer values a number primitive can handle. The maximum and minimum values are hardcoded in this example, but you can also obtain these values through the Number object data type properties Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER. In this case, you can see there are four additional integers, two above and two below each threshold. If you look at the log statements, you'll notice two surprising results: maxIntLarger = 9007199254740992 which was declared as 9007199254740993 and minIntSmaller = -9007199254740992 which was declared -9007199254740993. This confirms integers above these established thresholds -- Number.MAX_SAFE_INTEGER & Number.MIN_SAFE_INTEGER -- aren't safe and it also illustrates why the bigint primitive data type came into existence.

The third set of variables in listing 4-4 illustrates the maximum and minimum overall values supported by a number primitive. In this example, the maximum and minimum values are hardcoded, but you can also obtain these values through the Number object data type properties Number.MAX_VALUE and Number.MIN_VALUE. The statements that follow attempt to increment and decrement each of these limits, which results in the maximum value being converted to Infinity and the minimum value being converted to -Infinity, confirming that attempting to go beyond the supported number range of a number primitive, results in it being given a value of Infinity or -Infinity. It's worth pointing out that if you want to assign a variable this Infintiy or -Infinity value directly, you can do so with the Number object data type properties Number.POSITIVE_INFINITY and Number.NEGATIVE_INFINITY.

The final statements in listing 4-4 illustrate how it's possible to create a number primitive without an actual number. In this case, the Number() constructor used as a plain function is given the "Foo" string, since the Number object data type doesn't know how to represent this, it creates a number primitive with a value of NaN, confirming that anything that can't be converted to a number is given a value of NaN. It's worth pointing out that if you want to assign a variable this NaN value directly, you can do so with the Number object data type property Number.NaN.

The boolean primitive data type and Boolean object data type  

A boolean primitive is used to hold boolean values and follows in the steps of the string primitive and number primitive in how it's created and operates. A boolean primitive can be created with a literal boolean -- true or false -- and it also provides the same functionalities -- methods and properties -- available in the Boolean object data type[3], in addition, a boolean primitive can also be created using the Boolean() constructor as a plain function.

The boolean primitive is one of the simplest primitives to understand, since it's doesn't get much simpler than storing a true or false value. Where things can get non-obvious is if you use the Boolean() constructor as a plain function to create a boolean primitive, since it can accept a wide range of values, that although each one ends up producing a true or false value, they can be non-obvious.

Listing 4-5 illustrates the different ways to create a boolean primitive, as well as a series of values passed to the Boolean() constructor with the boolean primitive they produce.

Listing 4-5. boolean primitives creation
// boolean primitive, with literal booleans
let a = true;
let b = false;
console.log("a is %s with value: %s", typeof a, a);
console.log("b is %s with value: %s", typeof b, b);

// boolean primitive, with Boolean() constructor as plain function
// with values that produce true
let c = Boolean(true);
let d = Boolean(1);
let e = Boolean(100);
let f = Boolean("true");
let g = Boolean("foo");
let h = Boolean("false");
let i = Boolean([]);
let j = Boolean({});
console.log("c is %s with value: %s", typeof c, c);
console.log("d is %s with value: %s", typeof d, d);
console.log("e is %s with value: %s", typeof e, e);
console.log("f is %s with value: %s", typeof f, f);
console.log("g is %s with value: %s", typeof g, g);
console.log("h is %s with value: %s", typeof h, h);
console.log("i is %s with value: %s", typeof i, i);
console.log("j is %s with value: %s", typeof j, j);

// boolean primitive, with Boolean() constructor as plain function
// with values that produce false
let k = Boolean(false);
let l = Boolean();
let m = Boolean(0);
let n = Boolean("");
let o = Boolean(null);
let p = Boolean(undefined);
let q = Boolean(NaN);
console.log("k is %s with value: %s", typeof k, k);
console.log("l is %s with value: %s", typeof l, l);
console.log("m is %s with value: %s", typeof m, m);
console.log("n is %s with value: %s", typeof n, n);
console.log("o is %s with value: %s", typeof o, o);
console.log("p is %s with value: %s", typeof p, p);
console.log("q is %s with value: %s", typeof q, q);

The first two variable in listing 4-5 a and b use the most obvious technique with boolean literals to create boolean primitives. The remaining examples in listing 4-5 use the Boolean constructor as a plain function to create boolean primitives.

The Boolean constructor as a plain function produces a true boolean primitive when its argument is: the true boolean literal; any number literal except 0; any string literal except an empty value; an empty array []; or an empty object literal {}. The Boolean constructor as a plain function produces a false boolean primitive when its argument is: the false boolean literal; no argument is used; the 0 number literal; an empty string literal; the null primitive; the undefined primitive; or the NaN value.

The symbol primitive data type and Symbol object data type  

The purpose of the symbol primitive is to support unique JavaScript values, just like symbols in other programming languages[4].

The symbol primitive differs slightly from previous primitives, in that it doesn't have a literal value to create it (e.g. like a string primitive can use a literal string in '',"" or ``; a number primitive can use a literal number like 30 or 100; or a boolean can use a literal boolean true or false). The symbol primitive is similar to previous primitives in that it provides the same functionalities -- methods and properties -- available in the more robust Symbol object data type[5] and it can be created through the Symbol() constructor of the Symbol object data type.

symbol primitives in JavaScript work with the concept of a registry, to ensure all values are unique. There are two ways to create symbol primitives: symbols where you don't care what their key value in the registry is and symbols where you explicitly assign a key value to use in the registry. Irrespective of the approach to create symbol primitives, a symbol primitive can also be given a description value which is used for human consumption vs. a key value which is used by the registry.

Listing 4-6 illustrates the different ways to create a symbol primitive with the Symbol() constructor and the Symbol.for() method of the Symbol object data type.

Listing 4-6. symbol primitives creation
// Symbol constructor accepts optional description (to debug)
let a = Symbol();
let b = Symbol("consonant");
let c = Symbol("consonant");

console.log("a is %s. String rep: %s. Description: %s", typeof a, a.toString(), a.description);
console.log("b is %s. String rep: %s. Description: %s", typeof b, b.toString(), b.description);
console.log("c is %s. String rep: %s. Description: %s", typeof c, c.toString(), c.description);

// Symbols are never equal, even if they have the same description
if (b == c) { 
   console.log("b == c");
} else { 
   console.log("b != c");
}
if (b === c) { 
   console.log("b === c");
} else { 
   console.log("b !== c");
}

// Symbol.for() creates symbol with key or gets it if exists,
// key value is also asigned as the symbol description value
let height = Symbol.for("height");
let width = Symbol.for("width");
let diameter = Symbol.for("width");

console.log("height is %s. String rep: %s. Description: %s", typeof height, height.toString(), height.description);
console.log("width is %s. String rep: %s. Description: %s", typeof width, width.toString(), width.description);
console.log("diameter is %s. String rep: %s. Description: %s", typeof diameter, diameter.toString(), diameter.description);

// symbols with same key created/fetched with Symbol.for() are the same
if (width == diameter) { 
   console.log("width == diameter");
} else { 
   console.log("width != diameter");
}
if (width === diameter) { 
   console.log("width === diameter");
} else { 
   console.log("width !== diameter");
}

// symbol keys can be fetched wtih Symbol.keyFor()
console.log("Symbol.keyFor(\"height\"): %s", Symbol.keyFor(height));
console.log("Symbol.keyFor(\"width\"): %s", Symbol.keyFor(width));
console.log("Symbol.keyFor(\"diameter\"): %s", Symbol.keyFor(diameter));

// symbol keys created with plain Symbol() constructor can't be fetched
console.log("Symbol.keyFor(a): %s", Symbol.keyFor(a));
console.log("Symbol.keyFor(b): %s", Symbol.keyFor(b));
console.log("Symbol.keyFor(c): %s", Symbol.keyFor(c));

// Creates a new symbol with key "consonant" since key not in registry yet
let d = Symbol.for("consonant");
console.log("Symbol.keyFor(d): %s", Symbol.keyFor(d));

// symbol d has the same "consonant" description as let b and let c 
console.log("d is %s. String rep: %s. Description: %s", typeof d, d.toString(), d.description);


// let b, let c and let d are all different symbols with the same description
if (b == d) { 
   console.log("b == d");
} else { 
   console.log("b != d");
}
if (b === d) { 
   console.log("b === d");
} else { 
   console.log("b !== d");
}
if (c == d) { 
   console.log("c == d");
} else { 
   console.log("c != d");
}
if (c === d) { 
   console.log("c === d");
} else { 
   console.log("c !== d");
}

The first three statements in listing 4-6 create symbol primitives with the Symbol() constructor. Next, you can see the output for each symbol. All three symbol references output typeof symbol. Since the a reference uses no arguments on the Symbol constructor its description is undefined. The b and c references use the "consonant" argument on the Symbol constructor, therefore their description is consonant.

The set of conditionals that confirm b and c are different illustrate an important behavior of the Symbol() constructor. The Symbol() constructor always creates a unique symbol primitive irrespective of its input for a description value. So what's the key value of a symbol in this case ? You don't know, but the symbol key is unique in the registry. Now, if you do care about the key value of a symbol primitive in the registry -- to retrieve it or do something else -- then you must use the Symbol.for() method.

Next, listing 4-6 shows three symbol primitives created with the Symbol.for() method. The Symbol.for() method requires a single argument to represent the key and description values of a symbol primitive. In addition, the Symbol.for() method has a built-in if/else behavior, if the provided argument key already exists in the registry it fetches the pre-existing symbol, if the provided argument key doesn't exist it's created with the provided key value. That's the reason why the symbol primitive for the diameter and width references are equal, since the first Symbol.for("width"); statement creates a symbol primitive with the width key and the second Symbol.for("width"); statement gets the pre-existing symbol with the width key and assigns it to the diameter reference.

Listing 4-6 then illustrates how to use the Symbol.keyFor() method to obtain the key for given symbol primitive. Notice, the Symbol.keyFor() method works when using symbol references created with the Symbol.for() method -- height, width & diameter -- but it doesn't work when attempting to fetch symbol references created with the Symbol() constructor -- a, b, c. This confirms the Symbol() constructor process is oblivious to the concept of custom key values.

Finally, listing 4-6 creates a third symbol primitive with a consonant description. In this case though, the Symbol.for() method is used to ensure the key value in the registry in consonant. The last set of conditionals confirm all three symbol primitives with a consonant description are different in the eyes of the registry, with d having a key value of consonant and the b & c references having built-in key values assigned by the Symbol() constructor.

The bigint primitive data type and BigInt object data type  

The purpose of the bigint primitive is to support large integers that can't be handled by the number primitive, which are those above 9007199254740991 == Number.MAX_SAFE_INTEGER or below -9007199254740991 == Number.MIN_SAFE_INTEGER, as shown in listing 4-4.

The bigint primitive is very similar to the number primitive in how it's created and operates. A bigint primitive can be created with a literal number and a trailing n (e.g. 100n), it also provides the same functionalities -- methods and properties -- available in the BigInt object data type[6], in addition, a BigInt primitive can also be created using the BigInt() constructor as a plain function.

Listing 4-7 illustrates the different ways to create bigint primitives, as well as some behaviors to be aware of, if mixing bigint primitives with number primitives.

Listing 4-7. bigint primitives creation
// bigint primitive, with literal number
let a              =  100n;
let b              =  9007199254740993n; // Number.MAX_SAFE_INTEGER + 2
// bigint primitive, with BigInt() constructor as plain function
let c              =  BigInt("100");
let d              =  BigInt("9007199254740993"); // Number.MAX_SAFE_INTEGER + 2

console.log("a is a %s with value: %s", typeof a, a);
console.log("b is a %s with value: %s", typeof b, b);
console.log("c is a %s with value: %s", typeof c, c);
console.log("d is a %s with value: %s", typeof d, d);

let w = 100;
console.log("w is a %s with value: %s", typeof w, w);


if (a == w) { 
  console.log("a == w");
  console.log("%s == %s", a, w);
} else { 
  console.log("a != w");
  console.log("%s != %s", a, w);
}

if (a === w) { 
  console.log("a === w");
  console.log("%s === %s", a, w);
} else { 
  console.log("a !== w");
  console.log("%s !== %s", a, w);
}

// Have to cast bigint to number to do operation with other number 
let x = Number(a) * w;
console.log("x is a %s with value: %s", typeof x, x);

// bigint cast can result in loss of precesion 
let y = Number(b) + 2; // Should be 9007199254740995
// Outputs 9007199254740994 which is wrong
console.log("y is a %s with value: %s", typeof y, y); 

// Keep bigint, bigint for safety
let z = b + 2n; // Should be 9007199254740995
// Outputs 9007199254740995 which is right
console.log("z is a %s with value: %s", typeof z, z);

Tip You can use the underscore _ character to separate units, hundreds or thousands and make it easier to read bigint values. See

The first two statements in listing 4-7 create two bigint primitives with a trailing n, while the following two statements create equivalent bigint primitives with the BigInt() constructor as a plain function. Next, you can see the log messages for all four statements output a typeof bigint and all values have a trailing n indicating a bigint primitive. The most important aspect of these messages is the correct handling of the integer value 9007199254740993, which is mishandled as a JavaScript number primitive in listing 4-4.

Next, you can see the number primitive statement let w = 100;, followed by a series of conditional statements that compare this 100 number primitive to the a reference with a 100n bigint primitive. In this case, the loose equality == operation is true because the underlying values (i.e. ignoring data types) are 100, whereas the strict equality === operator that takes into consideration data types is false, since the references are different primitive data types.

The next statements in listing 4-7 illustrate that in order to perform operations with bigint primitives, all values must be either bigint primitives or they must be converted to number primitives. The let x = Number(a) * w; statement casts the bigint primitive a to a number primitive in order to be multiplied with the number primitive w. While this process to cast bigint primitives to number primitives solves the problem of working across primitives, the let y = Number(b) + 2; statement shows this can be unsafe when you're working with integers near the number primitive integer limit. In this case, notice that casting the bigint primitive b with a value of 9007199254740993n and attempting to add a 2 integer primitive results in 9007199254740994, which is wrong. The last statement let z = b + 2n; in listing 4-7 illustrates that in order to make safe operations with bigint primitives, it's best to keep them as such and be aware that casting to a number primitive can result in a loss of precision.

The null and undefined primitive data types

The null and undefined primitive data types don't have equivalent full-fledged object data types like the string, number, boolean, symbol and bigint primitive data types. This means the null and undefined primitive data types are truly limited to just representing their value and have no extra functionalities. Where the null and undefined primitives become interesting is in their behavior and places you can encounter them.

The null primitive is used to indicate an absent value, so it has a variety of practical uses. One is for functions to return a null primitive for cases when it can't determine a resulting value such as an object or another primitive like a string or number. Another alternative is to initialize variables with a null primitive instead of ad-hoc initializations like let x = ""; for strings or let y = 0; for numbers, since having a null primitive makes it easier to compare and validate values vs. doing it with an empty string "" or a neutral integer like 0.

The undefined primitive is used to indicate a pending definition. Although it's completely valid to explicitly use an undefined primitive as a function return statement or assign it to a variable, the undefined primitive is the only one that's used by default in certain scenarios. Therefore, it's most often not used explicitly like other primitives and left to surface in its default scenarios, which are the following:

With an understanding of the purpose and where you can encounter the undefined primitive, as well as the purpose of the null primitive, listing 4-8 illustrates a series of behaviors for the null and undefined primitives.

Listing 4-8. null and undefined primitive behaviors
// Assign null primitive
let a = null;
// Defaults to undefined primitive
let b;

console.log("a is %s with value: %s", typeof a, a); // a is typeof object, due to legacy design
console.log("b is %s with value: %s", typeof b, b); 


if (a == b) { 
  console.log("a == b");
  console.log("%s == %s", a, b);
} else { 
  console.log("a != b");
  console.log("%s != %s", a, b);
}

if (a === b) { 
  console.log("a === b");
  console.log("%s === %s", a, b);
} else { 
  console.log("a !== b");
  console.log("%s !== %s", a, b);
}

Listing 4-8 begins by declaring the a and b variables, the first one with an explicit value of null and the second without a value so it defaults to a value of undefined.

The first interesting behavior of the null primitive is the typeof output is an object. This behavior isn't so much interesting as it's wrong, it turns out the null behavior with typeof is a widely known issue that was overlooked in the initial JavaScript versions and was never fixed due to the potential for breaking other functionalities. Therefore typeof null should really output null, but it never will, a quick search on the web can confirm this with various sources. The typeof output for an undefined primitive shows no surprises showing undefined.

The second interesting behavior of the null primitive shown in listing 4-8 is the underyling value for both the null primitive and the undefined primitive is considered equal, as you can see in the loose equality operation a == b. The strict equality a === b operation though -- which takes into account the data type & not just the value -- does confirm the null primitive and undefined primitive are different.

Object data types

Now that you have a firm understanding of JavaScript primitive data types, it's time to take a closer look at the second group of JavaScript data types: object data types. Simply put, what isn't a JavaScript primitive data type is a JavaScript object data type.

JavaScript has many built-in object data types. From the most generic Object object data type, to the object data types you learned about in the previous sections that are leveraged by primitive data types to gain functionalities (e.g. String object data type, Number object data type), to even more specialized object data types like Date, Map and Reflect.

The purpose of each JavaScript object data type -- like in any other programming language -- is to provide properties and methods to solve problems specific to the circumstances where they're used. So a generic Object object data type offers more general purpose functionalities, whereas something like the Array object data type offers functionalities to manipulate arrays.

Because there are many JavaScript object data types, it's important to be able to distinguish between different object data types, so you know what properties and methods can be used on a given object. However, the typeof operator you used in previous sections to identify JavaScript primitives doesn't work to identify different objects, in fact, if you use the typeof operator on a JavaScript object it simply outputs object.

In order to identify an object data type you can use the Object.prototype.toString.call() method, as shown in listing 4-9.

Listing 4-9. JavaScript object data types identified with Object.prototype.toString.call()
// Array
let numbers = [1,2]; 
// Date
let today = new Date(); 
// Object
let languages = {JavaScript:
                    {
                      versions:[
                            {ES5: "ECMAScript 5"},
                            {ES6: "ECMAScript 6"},
                            {ES7: "ECMAScript 7"},
                            {ES8: "ECMAScript 8"},
                            {ES9: "ECMAScript 9"},
                            {ES10: "ECMAScript 10"},
                            {ES11: "ECMAScript 11"},
                      ]
                    },
		 TypeScript:
                    {
                      versions:[
                            {TS1: "TypeScript 1"},
                            {TS2: "TypeScript 2"},
                            {TS3: "TypeScript 3"},
                            {TS4: "TypeScript 4"},
                      ]
                    }
}

// typeof operator outputs objects for all object variatios
console.log("numbers is %s with value %s", typeof numbers, numbers);
console.log("today is %s with value %s", typeof today, today);
console.log("languages is %s with value %o", typeof languages, languages)

// Object.prototype.toString.call outputs specific object data type
console.log("numbers is %s", Object.prototype.toString.call(numbers)); // [object Array]
console.log("today is %s", Object.prototype.toString.call(today)); // [object Date]
console.log(Object.prototype.toString.call(languages)); // [object Object]

The first lines in listing 4-9 declare three different objects: an Array, a Date and an Object. Next, you can see the typeof operator outputs an object value for all three references, confirming the typeof operator can't tell the difference between object types.

The last console statements in listing 4-9 illustrate how the Object.prototype.toString.call() method is capable of determining an object type. Notice the type for the numbers reference is [object Array], the type for the today reference is [object Date] and the type for the languages reference is [object Object].

The sections that follow describe the various JavaScript object data types and their purposes.

The immutability of primitives and mutability of objects

Before moving on to explore JavaScript object data types in greater depth, it's important to highlight a key difference between JavaScript object data types and JavaScript primitive data types. JavaScript primitives are immutable, whereas JavaScript objects are mutable. Immutability refers to the fact that something cannot be modified after it's created. Listing 4-10 illustrates this behavior.

Listing 4-10. JavaScript primitive immutability and object mutability
// primitives
let x = "JavaScript";
let a = 30.33333;

// objects 
let numbers = [1,2];
let languages = {JavaScript: { versions:[]}}

// Let's apply some logic to primitives
console.log("x.toUpperCase(): %s ", x.toUpperCase());
console.log("a.toFixed(): %s ", a.toFixed(2));

// Confirm primitives are immutable 
console.log("x : %s ", x);
console.log("a : %s ", a);

// Let's apply some logic to objects
console.log("numbers.push(3): %s", numbers.push(3));
console.log('numbers.versions.push({ES5: "ECMAScript 5"}): %s', languages.JavaScript.versions.push({ES5: "ECMAScript 5"}));

// Confirm objects are mutable 
console.log("numbers : %s ", numbers);
console.log("languages : %o ", languages);

// primitive references can of course be reassigned
x = "Python";
a = 20;

// Confirm primitive references can be reassigned
console.log("x : %s ", x);
console.log("a : %s ", a);

The initial statements in listing 4-10 create two primitives let x = "JavaScript" (a string) & let a = 30.33333; (a number), as well as two objects let numbers = [1,2]; (an Array) & let languages = {JavaScript: { versions:[]}} (an Object).

Next, you can see the x primitive reference uses the toUpperCase() method to output the string as JAVASCRIPT, whereas as the a reference uses the toFixed(2) method to trim the number to 30.33. You might expect that applying these methods alters the underlying values, but they don't because both values are primitives, so the console statements that follow confirm the x and a values remain unchanged and as such are immutable.

Next, you can see a similar technique to alter the numbers and languages objects is performed with the push() method to add a new element. Here you can confirm that after applying the push() method, both the numbers output value (an Array) and the languages output value (an Object) are modified, confirming objects are mutable.

Finally, the last statements in listing 4-10 illustrate that although primitives are immutable, their references can be given new values. In this case, you can see the x reference that holds a string primitive is given the "Python" value and the a reference that holds a number primitive is given the 20 value, the final console statements confirm both references hold the new primitive values.

The Object data type

The JavaScript Object data type is the most general purpose of all JavaScript object data types and is designed to store key-value pairs which are often referred to as properties. The strcture held by a JavaScript Object, also known as associative array, dictionary or map in other programming languages, is very flexible in terms of the keys and values it can hold.

An Object object is typically created through a literal {}, but it can also be created with the Object() constructor of the Object object data type.

Listing 4-11. Object data type creation
// Empty object literal
let stuff = {};
// Empty object with constructor
let goods = new Object();

// Display contents of objects
console.log("stuff : %o ", stuff);
console.log("goods : %o ", goods);

// Add properties to objects
stuff.language = "JavaScript";
goods.language = "JavaScript";
stuff["versions"] = [{ES5: "ECMAScript 5"}];
goods["versions"] = [{ES5: "ECMAScript 5"}];

// Display contents of objects
console.log("stuff : %o ", stuff);
console.log("stuff['language'] : %s ", stuff['language']);
console.log("stuff.versions : %s ", stuff.versions);
console.log("goods : %o ", goods);
console.log("goods.language : %s ", goods.language);
console.log("goods['versions'] : %s ", goods['versions']);


// Create populated object literal
let languages = {JavaScript:
                    {
                      versions:[
                            {ES5: "ECMAScript 5"},
                            {ES6: "ECMAScript 6"},
                            {ES7: "ECMAScript 7"},
                            {ES8: "ECMAScript 8"},
                            {ES9: "ECMAScript 9"},
                            {ES10: "ECMAScript 10"},
                            {ES11: "ECMAScript 11"},
                      ]
                    },
		             TypeScript:
                    {
                      versions:[
                            {TS1: "TypeScript 1"},
                            {TS2: "TypeScript 2"},
                            {TS3: "TypeScript 3"},
                            {TS4: "TypeScript 4"},
                      ]
                    }
}


console.log("languages : %o ", languages);

// Create populated object literal with method 
var wonderful = {
       flag: true,
       constant: "Object literal",
       render: function() { 
           console.log("Hello from wonderful.render()!");
          }
}

console.log("wonderful : %o ", wonderful);


// Object.prototype.toString.call outputs specific object data type
console.log("stuff is %s", Object.prototype.toString.call(stuff)); // [object Object]
console.log("goods is %s", Object.prototype.toString.call(goods)); // [object Object]
console.log("languages is %s", Object.prototype.toString.call(languages)); // [object Object]
console.log("wonderful is %s", Object.prototype.toString.call(wonderful)); // [object Object]

The first step in listing listing 4-11 creates an empty object with the literal {}. Next, you can see another empty object created with the new keyword and Object constructor. Then the contents of both objects are output with console statements as {}, indicating they're empty. Next, using a dot notation and bracket notation, the language and version properties are added to each object, with a string value and array object value, respectively. The console statements that follow show the updated contents for each object, as well as how it's possible to also use the dot notation or bracket notation to access a specific property.

The other two object statements in listing 4-11 illustrate how it's possible to declare inline values as part of an object declaration, as well as how it's possible to use different object property values, including literals and functions, the last of which are technically called methods in the context of objects. Finally, the console statements in listing 4-11 output the contents of each object and its data types, which confirm all three cases are [object Object] data types.

The Object object property descriptors: configurable, enumerable, value, writable, get, set and the static methods to set them Object.defineProperty() & Object.defineProperties()  

Adding properties to an Object object like it's shown in listing 4-11 with either dot notation or bracket notation, although quick and easy, is done with a lot of default behaviors. Depending on what you plan to do with a given Object object property, it can be convenient to modify some of these default behaviors, also known as property descriptors. The following is a list of the six Object object property descriptors, including their purpose.

There are two ways to update or create an Object object's property descriptors and it's through two Object static methods: Object.defineProperty() or Object.defineProperties(). The only difference between these two methods is Object.defineProperty() is designed to update/create one property at a time, while Object.defineProperties() can update/create one or more properties at a time.

Just like defining an Object object's property with dot notation or bracket notation sets its property descriptors to certain default values, both the Object.defineProperty() and Object.defineProperties() methods have their own default property descriptors values when they're not explicitly specified.

Listing 4-11-A illustrates various examples of property descriptor behaviors, including how to modify them.

Listing 4-11-A. Object property descriptor behaviors
// Create populated object literal with method 
var wonderful = {
       flag: true,
       constant: "Object literal",
       render: function() { 
           console.log("Hello from wonderful.render()!");
          }
}

// Access wonderful properties
console.log("wonderful.flag : %s ", wonderful.flag);
console.log("wonderful.constant : %s", wonderful.constant);
wonderful.render();

// Change wonderful.constant
wonderful.constant = "Object literal update";
console.log("wonderful.constant : %s ", wonderful.constant);

// Update wonderful.constant to original value and set writable false
Object.defineProperty(wonderful, "constant", {
  value: "Object literal",
  writable: false
});

// Attempt to change wonderful.constant
// "use strict" generates an error with this attempt 
// Cannot assign to read only property 'constant' of object '#<Object>'
wonderful.constant = "Object literal one more update";
// No changes to wonderful.constant due to writable false
console.log("wonderful.constant : %s ", wonderful.constant);

// Loop over wonderful properties
for (const property in wonderful) { 
   console.log("property '%s' is enumerable in wonderful", property);
}


let fantastic = {};
// Access empty fantastic object
console.log("fantastic : %o ", fantastic);

// Define multiple properties
Object.defineProperties(fantastic,{
flag:{
  value: true,
  configurable: true,
},
constant: { 
  value: "Object literal",
  writable: true,
},
render: { 
   value: function() { 
           console.log("Hello from fantastic.render()!");
   },
   enumerable: true,
}
});

// Access fantastic properties
console.log("fantastic.flag : %s ", fantastic.flag);
console.log("fantastic.constant : %s", fantastic.constant);
fantastic.render();

// Loop over fantastic properties
for (const property in fantastic) { 
   console.log("property '%s' is enumerable in fantastic", property);
}

The first statement in listing 4-11-A is a populated Object object literal assigned to the wonderful variable, followed by a series of log statements that output the object's properties using a dot notation. Next, the constant property of the wonderful object is updated with a new value, followed by a log statement confirming the new property value.Then you can see the Object.defineProperty() method is used to update the constant property of the wonderful object to its original value, as well as set the property's writable descriptor to false. Notice the input for the Object.defineProperty() method is: the object reference, in this case wonderful; the object property to update/create, in this case constant; followed by an object with property descriptors, in this case just the value and writable property descriptors.

Next, in listing 4-11-A you can see an attempt is made to update the constant property of the wonderful object. However, the log statement confirms this update fails, on account of the object property descriptor writable set to false. In this case, the failed attempt is silent, however, if you use the "use strict" statement such attempts would throw an explicit error Cannot assign to read only property '<name>' of object '#<Object>'. Finally, a for in loop is applied to the wonderful object, illustrating all properties of the object are enumerable (i.e. they have their enumerable property descriptor set to true by default).

The let fantastic = {}; statement in listing 4-11-A creates an empty Object object with a literal, followed by a log statement that outputs the empty object. Next, the Object.defineProperties() method is used to populate the object with a series of properties, just like the wonderful object. Notice the input for the Object.defineProperties() method is: the object reference to update, in this case fantastic; and another object describing the properties to update/create, with each property pointing toward another object with property descriptors. In this case, notice the various property descriptors define a value and some set an explicit value for configurable, writable and enumerable.

Next, in listing 4-11-A you can see log statements that output the fantastic object's properties using a dot notation and result in identical results to the wonderful object. Finally, the last for in loop shows the only enumerable property of the fantastic object is render. The reason the fantastic object only has one enumerable property, is because the default enumerable property descriptor is false if not explicitly defined when using the Object.defineProperty() or Object.defineProperties() methods, therefore, only the render property is enumerable on account of its enumerable property descriptor being explicitly set to true.

Tip See the prototype-based getters and setters with get and set keywords & property descriptors section, for an example that defines get and set property descriptors.

The delete operator to remove Object object properties 

The Object literal shorthand syntax  

Although declaring Object object instances with literal values -- that is, in curly brackets {} -- represents the most common approach to create Object object instances, the syntax required to declare certain object constructs can become verbose. To that end, ECMAScript ES6 (ES2015) introduced a series of shorthand syntax techniques to make it easier to declare Object object instances with literal values. Listing 4-11-C illustrates these shorthand syntax technique to the right, with their equivalent older syntax to the left.

Listing 4-11-C. Object literal shorthand syntax
// In early JavaScript (ES5) you had to do...                         // In modern JavaScript (ES6) you can do...
var name = "Python";                                                  let name = "Python";
var version = "3.10";                                                 let version = "3.10";

// Explictly declare property name/value                              // Single reference is enough to set property name/value
var literalLanguage = {                                               let literalLanguage = {
    name: name,                                                                       name,
    version: version                                                                  version,
    // Method properties require to explicitly                                        // Properties with () {} are implied methods 
    // declare the function() keyword                                                 // & don't require the function keyword
    hello: function() { return "Hello from " + this.name}       		      hello() { return "Hello from " + this.name}
                                                                                      
};                                                                       };

console.log(literalLanguage);                                          console.log(literalLanguage);
console.log(literalLanguage.hello());                                  console.log(literalLanguage.hello());

var javascriptCreator = "Brendan Eich";                                let javascriptCreator = "Brendan Eich";
var pythonCreator = "Guido van Rossum";                                let pythonCreator = "Guido van Rossum";
var placeholder = "echo";                                              let placeholder = "echo";


// Properties cannot be dynamically assigned at initialization         // Properties can be dynamically assigned at initialization with []
var creatorNames = { }                                                 let creatorNames = { 
                                                                                        [pythonCreator]: "Python",
// Properties can be dynamically assigned after initialization                          [javascriptCreator]: "JavaScript",
creatorNames[pythonCreator] = "Python";                                                 [placeholder](message) { return message} 
creatorNames[javascriptCreator] = "JavaScript";                                    }
creatorNames[placeholder] = function(message) { return message }

console.log(creatorNames);                                             console.log(creatorNames);
console.log(creatorNames.echo("Hello there!"));                        console.log(creatorNames.echo("Hello there!"));


var vowels = {                                                         let vowels = { 
  "a": "First vowel",                                                    "a": "First vowel",
  "e": "Second vowel",                                                   "e": "Second vowel",
  "i": "Third vowel",                                                    "i": "Third vowel",
  "o": "Fourth vowel",                                                   "o": "Fourth vowel",
  "u": "Fifth vowel"                                                     "u": "Fifth vowel"
}                                                                      }

// Object must be deconstructed explictly                             // Object can be deconstructed and assigned to variables by property name
var a = vowels.a, e = vowels.e, i = vowels.i,                         let { a, e, i, o, u } = vowels;
     o = vowels.o, u=vowels.u;

// Variables available after object deconstruction                    // Variables available after object deconstruction
console.log(a,e,i,o,u);                                               console.log(a,e,i,o,u);

                                                                      // Object can also be deconstructed and assigned to
								      // custom variables assigned by property name
								      let { a: lettera, e: lettere, i: letteri, o: lettero, u: letteru} = vowels;

                                                                      // Custom variables available after object deconstruction
                                                                      console.log(lettera,lettere,letteri,lettero,letteru);

The first improvement to object literals shown in listing 4-11-C is the shorthand syntax to declare properties and their values. You can see in the literalLanguage object declaration to right hand side, it's sufficient to use a variable to create an object property and its value vs. the older approach on the left hand side which requires explicitly declaring both an object property and its value. Another improvement present in the literalLanguage object declaration in listing 4-11-C is the shorthand notation for functions, notice the hello property which is a function doesn't require the explicit function keyword on the right side, where as in the older syntax to the left side it's a requirement.

The third improvement to object literals shown in listing 4-11-C is the creation of object properties with variables. Although it was possible to create object properties with variables, this could only be done after the object literal was created. But notice how in the right hand side version it's possible to declare object literal properties at creation using brackets ([]) that get substituted for the variable value. It's also worth pointing out the creatorNames object in listing 4-11-C that illustrates the creation of object properties with variables, does so for both fixed property values and a function property value with an argument, the last of which also uses shorthand function notation.

Finally, the last improvement to object literals shown in listing 4-11-C is the deconstruction of objects and assignement to variables. You can see that in order to assign the values of the vowels literal object to variables using the old syntax on the left side, it's necessary to explicitly access each of the object properties and assign them to different variables. Using the newer syntax on the right side, it's possible to deconstruct objects by creating variables based on an object's property names, in this case, you can see in listing 4-11-C the variable statement let { a, e, i, o, u } corresponds to the property names of the vowels object, therefore the a, e, i, o, u variables are assigned the values of the corresponding object properties. The last lines in listing 4-11-C also illustrate how it's possible to deconstruct an object and assign its values to differently named variables than an object's properties, in this case, the variable statement let { a: lettera, e: lettere, i: letteri, o: lettero, u: letteru} indicates to assign the vowels a propertry to the lettera variable, the vowels e propertry to the lettere variable and so on.

The global object detour: A top level Object object available in all JavaScript environments

All JavaScript environments have what's called a global object, which is an Object data type like the one you just learned about in this past section.

The purpose of a global object in JavaScript environments is twofold: to allow access to a series of pre-defined properties & methods from anywhere, as well as serve as a placeholder to allow access to other properties and methods you want to access from anywhere.

Table 4-1 illustrates the various JavaScript global object properties and functions.

Table 4-1. Built-in global object properties and functions
Properties/methodsDescription
InfinityA numeric value that represents infinity
NaNA value that doesn't represent a number, NaN is an acronym for Not A Number
undefinedA value that represents an undefined reference
globalThisA value that represents the global this value (a.k.a. global object); technically a reference to itself
eval()Evaluates the input string value as JavaScript code
isFinite()Evaluates whether the input value is a finite number.
isNaN()Evaluates whether the input value is a NaN (Not a number).
parseFloat()Parses the input value into a floating point number.
parseInt()Parses the input value into an integer.
decodeURI()Decodes a URI, the opposite of encodeURI()
decodeURIComponent()Decodes a URI component, the opposite of encodeURIComponent()
encodeURI()Encodes a URI, the opposite of decodeURI()
encodeURIComponent()Encodes a URI component, the opposite of decodeURIComponent()

The most important takeaway of the properties and functions in table 4-1 is they're all available out of the box in any JavaScript environment. For example, you can use an eval() or parseInt() statement anywhere in a JavaScript application and it will happily work because it's available in the global object.

Although the properties and functions in table 4-1 are generally called without any additional syntax, sometimes the JavaScript global object itself is explicitly referenced with the this keyword (e.g.this.eval() is equivalent to eval() and this.parseInt() is equivalent to parseInt()).

In order to understand when it can be necessary to explicitly reference the JavaScript global object with the this keyword, it's inevitable to revisit the topic of JavaScript scope and hoisting explored in the previous chapter.

The global scope and access to the global object with this

By default, the JavaScript global object starts off with the properties and functions presented in table 4-1. Because the global object is accesible from anywhere, its contents -- properties and methods -- are said to be in the global scope since you can reach them from anywhere in an application.

It turns out, any statement you add to the global scope of a JavaScript application also gets added automatically to this same global object, which is the underlying reason why global variables work, they're available through the global object. This means that if you add a statement like var x = "JavaScript"; to the top level of a JavaScript application, it gets added to the JavaScript global object. While in most cases it's enough to simply use the x reference instead of this.x -- similar to the properties and functions in table 4-1 -- there are cases when explicitly referencing the JavaScript global object is necessary, one such case is presented in listing 4-12.

Listing 4-12. Global object, global scope and this syntax
var primeNumbers = [2,3,5,7,11];

console.log(primeNumbers);
console.log(this.primeNumbers);

var that = this;

function testing() { 
     console.log("Inside testing");
     var primeNumbers = [13, 17, 19, 23, 23, 29];
     console.log(that.primeNumbers);
}

testing();

You can see the top level statement primeNumbers in listing 4-12 is initially accessed using either primeNumbers or this.primerNumbers, even though the latter syntax is redundant.

However, notice the logic inside the testing() function contains another variable named primeNumbers, which presents a problem, how do you access variables with the same name in different scopes ? In order to access the top level primeNumbers variable inside the function, you can see the function in listing 4-12 uses the auxiliary that reference to hold the this global object, so the that.primeNumbers syntax inside the function gives access to the JavaScript global object -- if you forgot why the var that = this placeholder is used, review the section lexical scope, the this execution context in the previous chapter.

While the workarounds in listing 4-12 are valid, having to access the global object with the this reference or having to use another variable to access the contents of this inside of function, can become very confusing. To top it all off, the this keyword is a very overloaded JavaScript term which holds special meaning in JavaScript object-orientated and prototype-based programming, as well as in JavaScript's lexical scope and execution context.

So given the importance of the global object to hold properties and methods available in the global scope, it's important to be able to reference the global object in a more precise way than relying on the this keyword.

Global object access without the this keyword: Window interface & window keyword and Global object & global keyword, the early years

JavaScript engines have come to rely on either the window or global keywords to reference the JavaScript global object. The window keyword has become the norm to reference the JavaScript global object in browser-based JavaScript engines, whereas the global keyword has been adopted as the JavaScript global object reference for JavaScript engines in non-UI environments like Node JS.

In this sense, both the window and global keywords are in fact references to full-fledged JavaScript objects. With the window reference being a Window interface[7] and the global reference being a global namespace object[8].

Because the window and global references point to full-fledged objects and are synonyms with the JavaScript global object, it's also the reason why JavaScript environments have automatic access to many properties and functions without needing to qualify their access (i.e. use window.<property>). For example, the window reference has access to the the console object to access a browser's debugging console and methods like alert() to display an alert dialog, all of which become automatically available in browsers -- with or without using the window reference.

Still, relying on the window and global keyword references to access the JavaScript global object has its drawbacks. First, the window and global keyword references provide access to multiple things: the default global object properties and methods presented in table 4-1; plus properties and methods added to the global scope like it's described in listing 4-12; plus access to properties and methods of the Window object or Global object itself. Second, the window and global keyword references aren't streamlined/common across all JavaScript environments.

Therefore, in order to keep the window and global keyword references and their functionalities to themselves, while keeping the actual JavaScript global object and its contents separate, as well as streamline access to the JavaScript global object through a common keyword, ECMAScript introduced the globalThis keyword.

Global object access without the this keyword: globalThis property, the modern years  

If you look back a table 4-1, you'll see the globalThis property is available as part of the JavaScript global object. This means it's accesible in JavaScript from anywhere and it allows access to the JavaScript global object itself. In turn, the globalThis property allows easy access to the JavaScript global object without having to use any of the workarounds presented in the previous sections: the this reference, the window reference or the global reference.

Listing 4-13 illustrates a refactored version of listing 4-12 that makes use of the globalThis property to access a variable added to the global scope.

Listing 4-13. Global object, global scope and globalThis syntax
var primeNumbers = [2,3,5,7,11];

console.log(primeNumbers);
console.log(this.primeNumbers);

function testing() { 
     console.log("Inside testing");
     var primeNumbers = [13, 17, 19, 23, 23, 29];
     console.log(globalThis.primeNumbers);
}

testing();

Notice the logic inside the testing() function in listing 4-13 now makes use of the globalThis property to access the top level global primeNumbers variable inside the function.

The Map and WeakMap data types  

It's fairly common for software applications to require data structures to keep track of key-value pairs. To that end, the JavaScript Object data type supports this functionality through a series of versatile properties and methods. However, as flexible as the Object data type can appear, it has several shortcomings compared to dedicated data types designed for this purpose in other programming language.

Similar to other JavaScript data types you've learned about that provide a more comprehensive set of features over a plain Object data type for certain scenarios, the Map data type[9] offers some of the following advantages for storing key-value pairs:

Listing 4-14 illustrates several use cases of the Map data type.

Listing 4-14. JavaScript Map object data type
// Empty map
let stuff = new Map();

// Add elements to Map
stuff.set("vowels",["a","e","i","o","u"]);
stuff.set("numbers", [1,2,3,4,5]);
stuff.set("pi","3.14159265359");

// Loop over Map
stuff.forEach(function(value, key) {
  console.log(key + " = " + value);
});

// Get specific Map value
console.log(stuff.get("numbers"));

// Get an array with keys in Map
console.log(Array.from(stuff.keys()));

// Get an array with values in Map
console.log(Array.from(stuff.values()));

// Populate Map on creation 
let languageCreators = new Map([["JavaScript", "Brendan Eich"], ["Python", "Guido van Rossum"]]);

console.log("JavaScript was created by " + languageCreators.get("JavaScript"));
console.log("Python was created by " + languageCreators.get("Python"));

Listing 4-14 begins by creating an empty Map instance assigned to the stuff reference. Next, the Map's set() method is used to assign three key-value pairs to the stuff reference and immediatly after the Map's forEach() method is used to loop over the key-value pairs and output them to the console. Next, you can see the Map's get() method is used to extract a value based on a key and the keys() and values() methods are used to extract a Map's keys and values in the form of Array data structures. Finally, listing 4-14 illustrates how it's possible to create a Map instance with an initial set of values using Array data structures.

A WeakMap[10] data type is a more specialized kind of Map data type designed for better performance. The weak in WeakMap refers to the keys of the data structure being weakly referenced, a technique that inevitably requires some talk about JavaScript memory usage. In very simple terms, when a JavaScript object reference ceases to be used, the JavaScript engine kicks in and attempts to recover the memory used by said object -- a process known as garbage collection. However, there are circumstances when even stale JavaScript objects aren't able to be cleaned up and their underlying memory is never released, leading to what's called a memory leak. Memory leaks are problematic because they mean memory that could be put to good use is held up until the JavaScript engine finishes or is shut down, a problem that can lead to a JavaScript engine or an underlying system becoming sluggish or crashing due to the starvation of memory resources caused by memory leaks.

And it's in a regular Map data type that uses Object data type keys that JavaScript memory leaks can occur. For example, given an object reference a and a Map reference named languages, an assignment like languages[a] = "This is the value for language a" will cause a memory leak in the event the a object reference is garbage collected, because the memory is never recovered, something that isn't an issue with a WeakMap data type. For example, given an object reference a and a WeakMap reference named weakLanguages, an assignment like weakLanguages[a] = "This is the value for language a" causes the WeakMap value associated with the a object to be removed in case the a object reference is garbage collected.

So does this mean you should always use a WeakMap data type over a Map data type to make better memory use ? Not really, as great as the WeakMap data sounds it has certain behaviors you need to be aware of. For starters, a WeakMap data type only works with Object data type keys, so if you need to use keys that are strings, integers or something else that isn't object references you won't be able to use a WeakMap, not to mention it becomes irrelevant to have weakly referenced keys when the keys themselves are concrete values that can't be garbage collected elsewhere. Another factor to consider is because a WeakMap data type has weakly referenced keys, its keys aren't readily accessible like a regular Map data type (e.g. in a Map data type you can use keys() method to instantly get all its keys, but in a WeakMap data type this isn't possible because its keys are weakly referenced and designed to be removed in case they're garbage collected elsewhere). In summary, if you need to manage values related to object reference keys, then a WeakMap can be a better option because it reduces the potential for memory leaks since it removes all data in a WeakMap once object references become stale elsewhere.

The Set and WeakSet data types  

JavaScript introduced the Set[11] data type with the intent to fulfill another common requirement in software applications: to store unique values. Prior to the Set data type, the only way to store unique values was to use an Array data type with an ad-hoc mechanism (e.g. loop over elements and compare them among one another), with the Set data type this in no longer necessary as it ignores any duplicate values. Listing 4-15 illustrates several use cases of the Set data type.

Listing 4-15. JavaScript Set object data type
// Empty Set
 let vowels = new Set();

vowels.add("a").add("e").add("i").add("o").add("u");

// Verify Set value
console.log(vowels.has("a"));
console.log(vowels.has("n"));

// Loop over Set
vowels.forEach(function(value) { 
   console.log(value);
});

// Duplicate Set values are ignored 
console.log(vowels);
vowels.add("e");
console.log(vowels);

// Populate Set on creation 
let primeNumbers = new Set([2,3,5,7,11]);

// Delete Set value 
console.log(primeNumbers);
primeNumbers.delete(11);
console.log(primeNumbers);

Listing 4-15 begins by creating an empty Set instance assigned to the vowels reference and uses the Set's add() method to add various values to the set. Next, the Set's handy has() method is used to validate if the vowels set contains certain values and immediately after the Set's forEach() method is used to loop over the set values and output them to the console. Next, an attempt is made to add the e value to the vowels with the add() method, however, this has no effect because the e value pre-existed in the set. Finally, listing 4-15 illustrates how it's possible to create a Set instance with an initial set of values using an Array and how the Set's delete() method can remove a set element.

Similar to the Map/WeakMap relationship, the Set data type also has an equivalent WeakSet[12] data type designed for performance purposes. Just like it's key-value WeakMap companion explained earlier, a WeakSet data type can only store Object references and does not provide quick access to its values like a regular Set data type (e.g. WeakSet doesn't have forEach method like Set), because by design it has weakly referenced values to facilitate garbage collection. So just like the WeakMap data type, a WeakSet data type is a good option if you need to manage a large group of unique objects because it reduces the potential for memory leaks since it removes all data in a WeakSet once object references become stale elsewhere.

The Proxy and Reflect data types  

Are you familiar with JavaScript classes and object-orientated programming ?

To understand the JavaScript Proxy and Reflect object data types, you inevitably need to be comfortable with JavaScript classes and object-orientated programming concepts. If you've never worked with either of these concepts, I recommend you look over the following chapter on JavaScript object-orientated and prototype-based programming to gain a better understanding of the examples that follow.

Although software proxies and reflection are not widely used techniques compared to something like date and math operations, proxies and reflection can be essential for testing and certain object orientated designs. If you've never worked with software proxies, I recommend you research the proxy pattern[13] to get a better feel for the use of proxies, in addition to researching the concept of computer science reflection[14] in case you've never worked with the concept of reflection in software.

Like all software proxies, the purpose of a JavaScript Proxy is to serve as a front for a given object. And why on earth would you want to do that ? In most cases it's to override (or add) custom behavior for a given object without modifying the backing object itself. This process can be common in software testing, where it's helpful to temporarily alter or add new behavior to an object, without having to alter the original object's implementation.

A JavaScript Proxy[15] works with two key concepts: a target object and traps. A target object represents the object on which you want to override (or add) custom behaviors, this can be as simple as a vanilla object (e.g.{}) or as elaborate as an object instance from a custom data type or class (e.g. Language, Letter). Traps represent handlers to execute custom logic when certain actions are made on a target object. Traps -- which are defined as part of a Proxy -- can contain logic as simple as log messages or as complex as overriding elborate business logic in the underlying target object. In addition, there can be multiple types of traps to execute custom behaviors at different points in the lifecycle of an object (e.g. when getting an object property value, when executing a function call or when creating an object instance with new).

A JavaScript Proxy can include up to thirteen different traps[16]. Listing 4-16 illustrates the basic syntax of a JavaScript Proxy object data type using a get trap.

Listing 4-16. JavaScript Proxy object data type
class Language  {
   constructor(name,version) {
     this.name = name;
     this.version = version;
   }
   hello() {
     return "Hello from " + this.name;
   }
}

let instanceLanguage = new Language("Python","3.10");

let languageProxy = new Proxy(instanceLanguage, {
    get: function( target, key, context ) {
        console.log('Accessing ' + key + ' with value ' + target[key] + ' through languageProxy');
        if (key == "name" && target[key] == "Python") { 
           console.log("Python, really ? You know you can switch to JavaScript on the backend right ?")
        }
    }
});


languageProxy.name;
languageProxy.version;
languageProxy.hello;

Listing 4-16 starts with the creation of a JavaScript class named Language, followed by the creation of an instance of said class assigned to the instanceLanguage reference. Next, a Proxy instance is created with the intent to alter the behavior of the instanceLanguage object instance without modifying the underlying Language class. The syntax to create a Proxy follows the pattern new Proxy(target, handler), where target represents the object you want to apply the proxy to and handler the proxy logic (i.e.traps) applied to the target object.

In this case, the instanceLanguage object instance has a get trap applied to it which is used to execute logic every time an object's properties or methods are accessed. Next, you can see the logic executed by the get trap consists of outputting a few log messages and inspecting the object's properties and values.

Finally, notice how it's possible to call the languageProxy object instance directly and access the same properties and methods as the underlying object instance (i.e. name, version, hello). More importantly though, notice that when calling the properties and methods of the proxied object -- in this case instanceLanguage -- the output reflects the logic contained in the Proxy get trap. In this manner, it's possible to override (or add) behaviors to an object at different points in its lifecycle without modifying its underlying class.

The JavaScript Reflect[17] data type represents an effort to conceptually align JavaScript with mainstream OOP languages. In OOP languages like Java and C#, reflection is the umbrella term used to describe techniques that inspect, add or inclusively modify objects at run-time and for which each langauge has it own dedicated built-in package. However, reflection techniques in JavaScript prior to the appearance of the Reflect data type were fragmented.

For example, back in listing 4-1 you learned how the typeof operator outputs a reference's data type and in listing 4-9 you also learned how a call to Object.prototype.toString.call(reference) also outputs a reference's object data type. Both of these are reflection techniques that inspect an object to determine its type. Inclusively, the Object data type has several methods that produce reflection, such as Object.defineProperty() and Object.defineProperties() which can modify objects programmatically by adding a single or multiple properties to them, respectively.

Listing 4-16 illustrates a series of operations with the JavaScript reflect object data type demonstrating how it's possible to inspect, add or inclusively modify objects.

Listing 4-16. JavaScript Reflect object data type
class Language  {
   constructor(name,version) {
     this.name = name;
     this.version = version;
   }
   hello() {
     return "Hello from " + this.name;
   }
}

let instanceLanguage = new Language("Python","3.10");

let today = new Date();

let primeNumbers = [2,3,5,7,11];

// Inspect with Reflect
console.log(Reflect.apply(Object.prototype.toString,instanceLanguage,[]));
console.log(Reflect.apply(Date.prototype.toString,today,[]));
console.log(Reflect.apply(Math.max,undefined,primeNumbers));

console.log(Reflect.has(instanceLanguage, 'name'));
console.log(Reflect.has(today, 'day'));
console.log(Reflect.has(today, 'toString'));
console.log(Reflect.has(primeNumbers, 'length'));

// Add with Reflect 
console.log(instanceLanguage.author);
Reflect.defineProperty(instanceLanguage,'author',{'value':'Guido van Rossum'});
console.log(instanceLanguage.author);

console.log(primeNumbers);
Reflect.set(primeNumbers, 5, 13);
console.log(primeNumbers);

// Modify with Reflect
console.log(instanceLanguage.name);
Reflect.set(instanceLanguage,'name','JavaScript');
console.log(instanceLanguage.name);
Reflect.deleteProperty(instanceLanguage, 'name');
console.log(instanceLanguage.name);

console.log(today);
Reflect.apply(Date.prototype.setFullYear,today,[2030]);
console.log(today);

One important aspect of the Reflect data type is that all its operations are static. This means you don't create Reflect object instances with new, but rather make direct calls on data references as illustrated in listing 4-16. Note, the next chapter on JavaScript object-orientated and prototype-based programming contains more details about static JavaScript data type operations, specifically the to new or not to new section.

Listing 4-16 begins with a custom Language data type class and the instanceLanguage instance of said class, followed by a standard Date data type instance and an Array data type. The first section in listing 4-16 demonstrates how the Reflect.apply() and Reflect.has() methods are capable of inspecting object data types.

As you can see in listing 4-16, the Reflect.apply() method accepts three arguments: A function call, an object data type instance or this reference with which to call the function and an input argument array with which to call the function. The first two Reflect.apply() calls are made on the Object.prototype.toString and Date.prototype.toString functions, respectively. Each of these functions simply outputs the string representation of an object and data object and in each case a corresponding Object and Date instance are used as the second Reflect.apply() argument. Because both functions (i.e. Object.prototype.toString and Date.prototype.toString) don't require input arguments, the third argument of these first two Reflect.apply() calls is an empty array []. The third Reflect.apply() call is made on the Math.max function which determines the maximum number in an array. Because Math.max doesn't require an object instance and relies on an input array argument, the second argument to Reflect.apply() is undefined and the third argument is the primeNumbers array.The Reflect.has() method is used to determine if a data type instance has a given property and accepts two arguments: an object data type instance to inspect and a quoted value with a property name. You can see the Reflect.has() calls in listing 4-16 all output true with the exception of Reflect.has(today, 'day'), because the today instance -- a Date data type -- doesn't have a day property.

The second and third sections in listing 4-16 consist of adding properties and modifying object data types with Reflect operations. You can see in listing 4-16, the Reflect.defineProperty() call is used to add the 'author' property to the instanceLanguage object data type instance, where as the Reflect.set() call is used to add a prime number to the primeNumbers array, where the second argument represents an object property -- in this case index 5 -- and the third argument the actual value -- in this case prime number 13. Next, you can see another Reflect.set() call is used to modify the name property value of the instanceLanguage object data instance and a call to Reflect.delete() is used to the remove the name property altogether. Finally, you can see the Reflect.apply() method is used once again, but this time to invoke the Date.prototype.setFullYear function to modify the today date instance with the 2030 argument, which effectively change's the date instance's year.

So what should you make of the Reflect data type ? Particularly considering reflection-like operations are supported elsewhere in JavaScript ? From a practical standpoint, you should try to use the Reflect data type when you require reflection features, for the simple reason it provides a clear cut location to implement these tasks in JavaScript, just like it's done in languages like Java and C# that have built-in reflection packages (i.e. reflect tasks aren't spread out in operators like typeof or other data types like Object). You can of course keep using the different JavaScript reflection-like operators and methods supported outside of the Reflect data type, but it becomes much easier -- for yourself and others -- to identify and implement reflection in JavaScript if you do so with a consolidated and built-in data type like Reflect.

The Array data type  

The Math data type  

  1. https://en.wikipedia.org/wiki/UTF-16    

  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#browser_compatibility    

  3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean#browser_compatibility    

  4. https://en.wikipedia.org/wiki/Symbol_(programming)    

  5. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#browser_compatibility    

  6. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#browser_compatibility    

  7. https://developer.mozilla.org/en-US/docs/Web/API/Window    

  8. https://nodejs.org/api/globals.html#globals_global    

  9. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map    

  10. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap    

  11. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set    

  12. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet    

  13. https://en.wikipedia.org/wiki/Proxy_pattern    

  14. https://en.wikipedia.org/wiki/Reflection_(computer_programming)    

  15. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy    

  16. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Methods_of_the_handler_object    

  17. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect