JavaScript data types: ES5, ES6, ES2016 (ES7) and TypeScript

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 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, as well as how more sophisticated JavaScript data types have been incorporated into later ECMAScript versions to support more advanced constructs. In addition, you'll also learn about TypeScript -- a JavaScript superset -- and how its statically typed behavior influences JavaScript data types for the better, as well as how it offers a completely fresh outlook on JavaScript data types.

ES5: 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 2-1 illustrates JavaScript's dynamically typed behavior.

Listing 2-1. ES5: Dynamically typed behavior
var x = 1;
console.log(typeof(x));
x = ['one'];
console.log(typeof(x));
x = "1";
console.log(typeof(x));
var y = 2;
console.log(x + y);

As you can see in listing 2-1, the x variable is first interpreted as a number, then as an object 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. The previous chapter on JavaScript syntax described TypeScript - Static type syntax and the upcoming section on TypeScript data types expands on TypeScript's features.

ES5: Primitive data types

The first series of data types available in JavaScript ES5 are called primitives. JavaScript ES5 supports five primitives: string, number, boolean, null and undefined. Primitive data types cannot be de-constructed into simpler types, so they're pretty easy to identify, not to mention their behavior is also straightforward (e.g. var x = 'JavaScript'; is a primitive string, var x = 1; is a primitive number, var x = true; is a primitive boolean,etc).

Primitive data types by themselves don't support much functionality besides representing their values, but due to their potential for requiring additional functionalities, three of the five primitive data types -- string, number, boolean -- support constructor objects.

ES5: Constructor objects for string, number and boolean primitives

JavaScript constructor objects are a 'behind the scenes' mechanism by which JavaScript primitive data types emulate the behavior of an equivalent full-fledged JavaScript object data type. By emulating a full-fledged JavaScript object data type, these three primitive data types gain access to additional functionalities, as illustrated in listing 2-2.

Listing 2-2. ES5: Primitives and constructor objects
var x = 'JavaScript';
console.log(x.length);
console.log(x.indexOf('S'));
console.log(x.substring(4));

// Equivalent String object, which behind the scenes calls String.toString() 
var y = String('JavaScript');

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');
}

// Explicitly creates an object of a 'primitive' data type with 'new'
var z = new String('JavaScript');

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');
}

The var x = 'JavaScript'; statement in listing 2-2 is a primitive string data type -- sometimes also referred to as a literal string. However, notice that immediately after the declaration, calls are made to multiple properties (e.g. length) and methods (e.g.indexOf(), substring()) on the same x reference. Behind the scenes, the JavaScript engine provides all primitive string data types access to the same functionalities as a full-fledged String object data type. So in reality, the properties and methods used by the primitive string data type in this example, are actually part of the String object data type. This same automatic constructor object behavior for primitive string data types, is also used for primitive number and boolean data types (i.e. a primitive number data type gains access to a Number object data type and a primitive boolean data type gains access to a Boolean object data type).

The second statement var y = String('JavaScript'); in listing 2-2 is a constructor based string because it uses the String object data type constructor, which also results in a primitive string. Behind the scenes, String('JavaScript'); calls String.toString('JavaScript'), so you end up with an identical value to using the primitive string syntax, reason why this constructor based syntax is rarely used and considered redundant. You can confirm both String('JavaScript') and 'JavaScript' are equivalent in the subsequent condition evaluations in listing 2-2, where both x -- which doesn't use an explicit object data type -- and y -- which does use an explicit object data type -- are identical, using both the equality == operator and the strict equality === operator (a.k.a. identity operator).

The third statement var z = new String('JavaScript'); in the example makes use of the new operator to create a String object instance. While the new operator is perfectly valid JavaScript OOP (Object Orientated Programming) syntax, its use is discouraged in object data types that map directly to primitive data types (i.e. String, Number and Boolean).

The reason the new operator should be avoided in primitive scenarios is illustrated in the final snippets of listing 2-2. Notice that although the z reference -- that uses the new operator -- apparently creates an identical string to the x and y references, the values differ. On the subsequent condition evaluations, notice the z reference is only equal to the x and y references with an equality == comparison and not with a strict equality comparison ===. In this case, the strict equality comparison fails because the z reference is an object value and the other two are primitive values, which is why the new operator should be avoided on simple object data types that map directly to primitive data types to avoid this type of equality operator disparity. The next chapter contains the section To new or not to new and static properties/methods with additional details about the influence of using the new keyword with data types.

Forget about loose equality operators == & != syntax and instead use strict equality operators === & !==

A corollary to avoiding the new operator in primitive data type scenarios, is avoiding the use of loose equality operators == & != altogether, because they test against primitive values producing the unexpected results illustrated in this last example in listing 2-2.

Strict equality operators === & !== ensure comparisons are made against object values, producing intuitive results. Which is why strict equality syntax is often the recommended choice, particularly for JavaScript beginners.

If case you're curious about why loose equality operators == & != produce the results they do -- even when applied to object values -- they use the Abstract Equality Comparison Algorithm[1].

ES5: The global object, null and undefined, plus the JavaScript global scope, this, window and global keywords.

Although null and undefined are primitive data types, they don't have constructor objects like the string, number and boolean primitives data types. This means the null and undefined primitive data types don't have equivalent full-fledged JavaScript object data types and as such have no extra functionalities (e.g. indexOf(), substring()) and are truly limited to just representing a value.

Here it's important to understand the null and undefined primitive data types -- as well as other properties and functions -- originate in what's called the JavaScript global object. Every time you run a JavaScript engine, a global object is made available to access functionalities without requiring explicit instructions or objects. Table 2-1 illustrates the various JavaScript global object properties and functions.

Table 2-1. ES5: 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
null*A value that represents a missing object value. *NOTE: Technically null isn't a global object property, but rather an empty object. However, null is always accesible like other global object properties and functions.
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 2-1 is they're all available out of the box in any JavaScript environment. For example, you can place an eval() or parseInt() statement anywhere in a JavaScript application and it will happily work. However, although the properties and functions in table 2-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(), this.parseInt() is equivalent to parseInt()).

And in order to understand when it's 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.

By default, the JavaScript global object starts off with the properties and functions presented in table 2-1, however, as soon as you start adding statements to the global scope of a JavaScript application, these get automatically added to the global object. This means that if you add the statement var x = 'JavaScript'; to the top level -- like listing 2-2 -- it gets added to the JavaScript global object. And while in most cases it's enough to simply use the x syntax instead of this.x -- similar to the properties and functions in table 2-1 -- there are cases when explicitly referencing the JavaScript global object is necessary, one such case is presented in listing 2-3.

Listing 2-3. ES5: Global object 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 2-3 is initially accessed using either primeNumbers or this.primerNumbers, even though the latter syntax is redundant. However, notice the logic inside the testing() method 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 method in listing 2-3 uses the auxilary that reference which holds the this global object, so the that.primeNumbers syntax gives the method access to the JavaScript global object -- if you're curious as to why the var that = this placeholder was required, review the section lexical scope, the this execution context and closures in the previous chapter.

Now, if you've done any kind of object-orienated programming (OOP) or as you'll learn in the upcoming chapter on JavaScript object-orientated and prototype-based programming, the this keyword holds special meaning to reference object instances. Although the JavaScript global object is technically an object instance and therefore using the this keyword to reference it is accurate, this doesn't mean it isn't confusing.

The problem with using the this keyword to reference JavaScript's global object, is that if you have an OOP heavy design that uses the this keyword extensively, it can be hard to tell if this is referring to the JavaScript global object. Although most JavaScript engines continue to let you use the this keyword to reference JavaScript's global object, to alleviate this potential for confusion -- as well as add custom functionalities to the global object -- JavaScript engines have come to rely on either the window or global keywords to reference JavaScript's global object.

The window keyword has become the norm to reference the JavaScript global object in browser-based JavaScript engines (e.g. Chrome, Firefox, IE), where as the global keyword has been adopted as the JavaScript global object reference for non-visual JavaScript engines (e.g. Node.js).

In addition to the window and global keywords being the JavaScript global object reference and providing access to the properties and methods in table 2-1, these references also come equipped with their own custom functionalities. For example, the window reference has access to properties like console 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 -- in order to facilitate JavaScript development in browser bound applications.

In this sense, both the window and global JavaScript global object references are in fact full-fledged JavaScript object data types -- similar to the String object, Number object and Boolean object data types described in the first section of this chapter -- with the window reference being a Window object data type[2] and the global reference being a global namespace object[3].

ES5: 'First class' data types - object, string, number, boolean, undefined and function

Now that you understand JavaScript primitive data types and their close relationship to object data types, let's take a closer look at the 'first class' data types available in JavaScript ES5.

JavaScript ES5 has a small amount of data types, which can be particularly surprising for someone with experience in other programming languages with dozens of data types. Listing 2-4 makes use of the typeof method to illustrate JavaScript ES5 data types.

Listing 2-4. ES5: 'First class' data types

console.log(typeof("Hello")); // string
console.log(typeof(1)); // number
console.log(typeof(true)); // boolean

var a;
var b = function() { return "ok" }
var c = null;

console.log(typeof(a)); // undefined
console.log(typeof(b)); // function
console.log(typeof(c)); // object


console.log(typeof({})); // object
console.log(typeof({'language':'JavaScript'})); // object
console.log(typeof([1,2])); // object
console.log(typeof(new Date())); // object
console.log(typeof(null)); // object

The first three typeof statements in listing 2-4 correspond to the same three primitive/object data types you learned about in the first part of this chapter -- string, number and boolean -- which form part of the JavaScript ES5 primitive data types that support constructor objects.

Next, are three more typeof statements. The first statement illustrates the case of the a reference which outputs an undefined data type because a has no value assigned to it. The b reference is assigned a function() value and outputs the rather obvious function data type. But more importantly, notice the third and final typeof statement of this second set, the c reference is assigned a null value and outputs an object data type! So why does a null value produce an object data type instead of a null data type ?

Although the c reference is assigned a null primitive value, the typeof statement outputs an object data type, because as I already mentioned in the previous section, the null primitive value does not map directly to a JavaScript object data type and actually represents a missing object value as described in table 2-2, so it's assigned a generic JavaScript object data type.

All of which takes us back to the 'first class' wording in the title of this section. ES5 only has six 'first class' data types -- string, number, boolean, undefined, function and object. So why do I call them 'first class' data types ? Because as you just confirmed in listing 2-4, only these data types are unequivocally identified by a JavaScript engine, all other data values that even at first glance might look like data types themselves are assigned to the catch-all object data type.

You can confirm this catch-all behavior of the object data type in the last set of typeof statement in listing 2-4. The first two statements {} and {'language':'JavaScript'} in listing 2-4 in other programming languages are called hash tables/hash maps, dictionaries or associative arrays, but in both cases the JavaScript typeof statement identifies them as object data types.

Next, notice the third statement in the last set [1,2] which in other programming languages might be identified as an array or list, in JavaScript is also an object data type! Similarly, the fourth statement new Date() which is a date value also outputs an object data type, just like the fourth and final null value produces an object data type.

Now that you know about the prevalence of the object data type and its close relationship to various kinds of JavaScript values, let's take a closer look at it.

ES5: The all prevalent Object data type

Comprehending the JavaScript Object data type is one of the most important concepts you need to understand about JavaScript data types in general, primarily because all JavaScript data types are Object data types due to inheritance. This includes not only the JavaScript global object and the Window object data type you learned about in an earlier section, but also the String object data type, the Number object data type and the Boolean object data type, in addition to all the other JavaScript object data types you'll learn about in this chapter.

So with all JavaScript object data types being descendants of the Object data type, it means all data types gain access to a basic set of functionalities. The next chapter on JavaScript object-orientated and prototype-based programming takes a closer look at these functionalities and dives deeper into the topic of JavaScript prototypal inheritance, but for the moment let's continue to analyze the Object data type.

Since the Object data type is at the top of the hierarchy of all JavaScript data types, the Object data type has a very simple design to store properties and values of any kind, as illustrated in listing 2-5.

Listing 2-5. ES5: Everything is an Object
// Empty object literal
var stuff = {}; 

// Add property and value
stuff.language = 'JavaScript';

// Create populated object literal
var languages = {'JavaScript':
                    {
                      'versions':[
                            {'ES5': 'ECMAScript 5'},
                            {'ES6': 'ECMAScript 6'},
                            {'ES7': 'ECMAScript 7'},
                            {'TS' : 'TypeScript'}
                      ]
                    }
                }

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

console.log(stuff);
console.log(Object.prototype.toString.call(stuff));  // [object Object]
console.log(languages);
console.log(Object.prototype.toString.call(languages)); // [object Object]
console.log(wonderful);
console.log(Object.prototype.toString.call(wonderful)); // [object Object]

The first step in listing listing 2-5 creates an empty object with {} and assigns it to the stuff reference. Next, using a dot on the stuff reference, the language property with a 'JavaScript' string value is added to the stuff object. It's worth mentioning, this technique of adding properties and values to a JavaScript object, is how the JavaScript global object operates behind the scenes (e.g. global variables and functions are appended as properties to the global object).

The other two object statements in listing 2-5 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.log statements in listing 2-5 output the contents of each object and its data types, which you can confirm for all three cases is [object Object].

But let's pause for a second, why do you think the data type (i.e. the output of Object.prototype.toString.call) for all three objects in listing 2-5 is [object Object] ? In this case, the JavaScript engine is saying the data types are Object object data types, which is the most general purpose data type JavaScript has to offer.

As it happens and I already mentioned a few paragraphs ago, the fallback data type for JavaScript values is always an object data type. However, JavaScript objects in themselves always have an object data type, which is the topic of the next section.

ES5: JavaScript object data types

Now that you have a firm understanding of JavaScript's 'first class' object data type, it's time to take a closer look at the various kind of object data types available in JavaScript ES5 that are descendant of this data type.

Even though JavaScript is technically limited in its number of data types -- with many being object data types -- there are many JavaScript object types to facilitate operations with JavaScript object values. Listing 2-6 makes use of the Object.prototype.toString.call() method to illustrates various object types.

Listing 2-6. ES5: JavaScript Object data types
var numbers = [1,2];
var today = new Date();

console.log(Object.prototype.toString.call(numbers)); // [object Array]
console.log(Object.prototype.toString.call(today)); // [object Date]


var languages = {'JavaScript':
                    {
                      'versions':[
                            {'ES5': 'ECMAScript 5'},
                            {'ES6': 'ECMAScript 6'},
                            {'ES7': 'ECMAScript 7'},
                            {'TS' : 'TypeScript'}
                      ]
                    }
                }

console.log(Object.prototype.toString.call(languages)); // [object Object]

Notice how the first set of Object.prototype.toString.call() statements in listing 2-6 outputs [object Array] and [object Date], indicating a specific type of object. The importance of a specific object data type can't be understated, because this is what provides easy access to custom functionality to work with different kinds of data, a mechanism you learned first hand in listing 2-2 with the String data type having various methods designed to work with string values.

The last Object.prototype.toString.call() statements in listing 2-6 -- executed on the languagesreferences -- outputs [object Object]. The interesting aspect of this output is that it doesn't produce a specific object data type like the previous statements, but rather a generic object data type.

Although object data types represent a powerful option due to their generic nature and flexibility, this last behavior is one of JavaScript's greatest deficiencies in terms of ES5 data types. The problem with a generic data type like object is it has limited functionality. For example, while Date object values gain access to methods like getFullYear() and getHours(), and Array object values gain access to methods like pop() and push() -- all of which facilitate operations on these types of values -- Object object values have no additional functionality. This lack of functionality is most evident in JavaScript for loops where ad-hoc techniques are required to execute simple tasks on generic object data types. To solve these issues, ES6 introduced additional object data types to deal with hash table/hash map like values, which are described in the upcoming section.

But before we get to the topic of ES6 object data types, I want you to get a better feel for some of JavaScript's ES5 most common object data types. Not only because ES5 object data types continue to be a staple of JavaScript development, but also because these objects data types have evolved in newer ECMAScript specifications. Table 2-2 illustrates the most common JavaScript ES5 object data types.

Table 2-2. ES5: Most common object data types
Object data typeDescriptionSample properties and methods
Object[4]All JavaScript object data types are descendants of this class and have access to its properties and methods. Values like associative arrays or dictionaries that can't fit into a more specific sub data type (e.g. String, Date) are simply Object's. Object.prototype.- Allows the addition of properties to an object. See the next chapter on JavaScript object-orientated and prototype-based programming for additional details on its use.
Object.keys().- Returns an array with an object's properties.
String[5]A JavaScript object to store text or character values.String.charAt(x).- Returns the character at the x index.
String.endsWith(x).- A boolean evaluation to determine if string ends with the x string/characters.
Number[6]A JavaScript object to store numerical valuesNumber.isInteger(x).- A boolean evaluation to determine if x is an integer
Number.toExponential(x).- Returns a string representing the number x in exponential notation.
Boolean[7]A JavaScript object to store boolean valuesContains no particular functionalities of its own, except those behavior inherited from Object
Function[8]A JavaScript object to represent functionsFuction.apply().- Calls a function and sets its this to the provided value; see listing 1-7 for example
Function.call().- Executes a function and sets its this to the provided value; see listing 1-7 for example
Array[9]A JavaScript object to store arrays or lists as they're called in other programming languages.Array.pop().-Removes the last element from an array and returns that element.
Array.reverse().-Reverses the order of the elements of an array in place -- the first becomes the last, and the last becomes the first
Date[10]A JavaScript object to represents a single moment in time.Date.now().- Represents the numeric value corresponding to the current time -- the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC (a.k.a. 'Unix epoch time') -- with leap seconds ignored.
Date.getMonth().- Returns the month (0-11) ood the date instance according to local time
Math[11]A JavaScript object with mathematical constants and functionsMath.PI.- Represents the ratio of the circumference of a circle to its diameter, approximately 3.14159.
Math.round(x).- Returns the value of x rounded to the nearest integer.
JSON[12]A JavaScript object to parse and convert JavaScript Object Notation (JSON) data structures.JSON.parse().- Parses a string as JSON
JSON.stringify().- Return a JSON string corresponding to the specified value.
Error[13]A JavaScript object to handle errors or custom errors.Error.message.- Displays error message
Error.name.- Displays error name.
RegExp[14]A JavaScript object for regular experessions.Regexp.exec(x).- Executes a search for a match on the x string .
RegExp.ignoreCase.- Ignores case while attempting to match a string.

ES6: symbol primitive, Proxy and Reflect, the Map and Set data types, plus additions to ES5 data types

ES6 introduced major changes and additions to JavaScript data types. First, ES6 introduced a new primitive data type dubbed symbol to support unique values. Another important addition to ES6 was the inclusion of the Proxy and Reflect object data types, to give JavaScript the ability to proxy or modify application structures and behaviors at runtime.

To facilitate working with data structures that in ES5 were pegged to the the generic and all prevalent Object data type, ES6 introduced the Map, Set, WeakMap and WeakSet object data types designed to work with key-value pairs and unique values. In addition, ES6 also introduced the Promise object data type which plays a key role in JavaScript asynchronous behavior, as well as the Generator and GeneratorFunction object data types which are equipped to deal with JavaScript generator expressions which are closely tied to JavaScript for loops.

Finally, many of the ES5 object data types presented in table 2-2 were also updated with new properties and methods to support new requirements. For example, the Object object data type was updated with the is() to compare if two values are the same value; the String object data type was updated with the startsWith() method to determine if a string begins with the characters of a give string; and the Array object data type was updated with the find() method to return the value of the first element in an array that satisfies a given testing function.

ES6: symbol primitive

Complementing the five primitive data types from ES5 -- string, number, boolean, null and undefined -- ES6 introduced the symbol primitive, whose purpose is to generate unique JavaScript values, just like symbols in other programming languages[15].

In order to create a symbol primitive value, ES6 introduced the JavaScript global object function Symbol()[16] -- which complements the ES5 Global object properties and functions presented in table 2-1. Listing 2-7 illustrates the basic usage of a symbol primitive.

Listing 2-7. ES6: JavaScript symbol primitive data type
let vowel = Symbol();
let letter = Symbol("Letter");

console.log(typeof(vowel));
console.log(typeof(letter));
console.log(vowel.toString());
console.log(letter.toString());

// Create another symbol with the same description
let character = Symbol("Letter");

// Symbols are never the same, even if they use the same description 
if (letter == character) { 
   console.log('letter == character is True');
} else {
  console.log('letter == character is False');
}

if (letter === character) { 
   console.log('letter === character is True');
} else {
  console.log('letter === character is False');
}

// Use symbols as Object keys 
let config = {}
config[vowel] = "A";
config[letter] = "B";
config[character] = "C";

console.log(config);

The first two statements in listing 2-7 generate two primitive symbols with the global Symbol() function, in which the second variation uses an optional argument -- in this case "Letter" -- to give the symbol a description. Next, you can see the type for both references is a symbol data type and the output generated by the toString() method for the symbol corresponds to the description assigned to the symbol (if any).

More importantly though, notice the third symbol definition in listing 2-7 -- let character = Symbol("Letter"); -- uses the same symbol generation syntax as let letter = Symbol("Letter"); before it. Under most computer science scenarios you'd likely expect character to be equal to letter since they're built with the same syntax, but they're not, as you can see in the subsequent conditionals.

JavaScript registers symbols in a symbol registry to ensure uniqueness everytime a call is made to Symbol(). And what is a symbol's actual value ? It doesn't matter -- you can assign a descriptive name as you've already seen -- but behind the scenes all you need to know is the value is unique, a technique that comes in handy to avoid assignment clashes (e.g. loading multiple libraries that use the same references or assigning object keys).

The last snippet in listing 2-7 creates an empty object literal and then uses the prior symbols as keys. This is in contrast to the more typical approach of using strings as object keys which have the probability of clashing with one another. For example, if an object has the potential to have dozens of keys or be modified in multiple places, using an assignment like config['name'] (i.e. a string key) has the probability of being overwritten/reassigned vs. an assignment like config[name] where name is a symbol (i.e. a symbol key) and is never overwritten/reassigned since it's unique.

Proxy and Reflect object data types

Similar to the object data types in table 2-2 designed to simplify certain JavaScript tasks (e.g. date operations with Date, math operations with Math), JavaScript ES6 introduced the Proxy and Reflect data types to simplify working with proxies and reflection.

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: ES5, ES6, ES2016 (ES7) and TypeScript 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[17] to get a better feel for the use of proxies, in addition to researching the concept of computer science reflection[18] 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[19] 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[20]. Listing 2-8 illustrates the basic syntax of a JavaScript Proxy object data type using a get trap.

Listing 2-8. ES6: 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","2.7");

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 2-8 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[21] 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 2-1 you learned how the typeof operator outputs a reference's data type and in listing 2-5 you also learned how a call to Object.prototype.toString.call(reference) also outputs a reference's data type. Both of these are reflection techniques that inspect an object to determine its type. Inclusively, the all prevalent 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.

So what are you to make of the Reflect data type if reflection is supported elsewhere in JavaScript ? It provides a clear cut location for reflection 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 to identify and implement reflection in JavaScript if you do so with a consolidated and built-in data type like Reflect.

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 to the data type as illustrated in listing 2-9. 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 and static properties/methods section.

Listing 2-9. ES6: JavaScript Reflect object data type

ES7

TypeScript

  1. http://es5.github.io/#x11.9.3    

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

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

  4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object    

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

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

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

  8. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function    

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

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

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

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

  13. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error    

  14. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp    

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

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

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

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

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

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

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