Chapter3 Language Basics

WHAT’S IN THIS CHAPTER?


➤➤ Reviewing syntax


➤➤ Working with data types


➤➤ Working with flow-control statements


➤➤ Understanding functions

Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/projavascript4e on the Download Code tab.


At the core of any language is a description of how it should work at the most basic level. This description typically defines syntax, operators, data types, and built-in functionality upon which complex solutions can be built. As previously mentioned, ECMA-262 defines all of this information for JavaScript in the form of a pseudolanguage called ECMAScript.


ECMAScript as defined in ECMA-262, fifth edition, is the most-implemented version among web browsers. The sixth edition is the next to be implemented in browsers, and as of the end of 2017, most major browsers have mostly or fully implemented the specification. For this reason, the following information is based primarily on ECMAScript as defined in the sixth edition.

SYNTAX

ECMAScript’s syntax borrows heavily from C and other C-like languages such as Java and Perl. Developers familiar with such languages should have an easy time picking up the somewhat looser syntax of ECMAScript.

Case-Sensitivity

The first concept to understand is that everything is case-sensitive; variables, function names, and operators are all case-sensitive, meaning that a variable named test is different from a variable named Test. Similarly, typeof can’t be the name of a function because it’s a keyword (described in the next section); however, typeof is a perfectly valid function name.

Identifiers

An identifier is the name of a variable, function, property, or function argument. Identifiers may be one or more characters in the following format:

 ➤➤ The first character must be a letter, an underscore ( _ ), or a dollar sign ($).


➤➤ All other characters may be letters, underscores, dollar signs, or numbers.

Letters in an identifier may include extended ASCII or Unicode letter characters such as À and Æ, though this is not recommended.

By convention, ECMAScript identifiers use camel case, meaning that the first letter is lowercase and each additional word is offset by a capital letter, like this:

firstSecond
myCar
doSomethingImportant

Although this is not strictly enforced, it is considered a best practice to adhere to the built-in ECMAScript functions and objects that follow this format.

NOTE Keywords, reserved words, true, false, and null cannot be used as identifiers. See the section “Keywords and Reserved Words” coming up shortly for more detail.

Comments

 ECMAScript uses C-style comments for both single–line and block comments. A single–line comment begins with two forward-slash characters, such as this:

// single line comment

A block comment begins with a forward slash and asterisk (/*) and ends with the opposite (*/), as in this example:

/* This is a multi-line
comment */

 Strict Mode

 ECMAScript 5 introduced the concept of strict mode. Strict mode is a different parsing and execution model for JavaScript, where some of the erratic behavior of ECMAScript 3 is addressed and errors are thrown for unsafe activities. To enable strict mode for an entire script, include the following at the top:

"use strict";

 Although this may look like a string that isn’t assigned to a variable, this is a pragma that tells supporting JavaScript engines to change into strict mode. The syntax was chosen specifically so as not to break ECMAScript 3 syntax.


You may also specify just a function to execute in strict mode by including the pragma at the top of the function body:

function doSomething() {
"use strict";
// function body
}

 Strict mode changes many parts of how JavaScript is executed, and as such, strict mode distinctions are pointed out throughout the book. All modern browsers support strict mode.

Statements

Statements in ECMAScript are terminated by a semicolon, though omitting the semicolon makes the parser determine where the end of a statement occurs, as in the following examples:

let sum = a + b // valid even without a semicolon - not recommended
let diff = a - b; // valid - preferred

Even though a semicolon is not required at the end of statements, you should always include one. Including semicolons helps prevent errors of omission, such as not finishing what you were typing, and allows developers to compress ECMAScript code by removing extra white space (such compression causes syntax errors when lines do not end in a semicolon). Including semicolons also improves performance in certain situations because parsers try to correct syntax errors by inserting semicolons where they appear to belong.

Multiple statements can be combined into a code block by using C-style syntax, beginning with a left curly brace ({) and ending with a right curly brace (}):

if (test) {
test = false;
console.log(test);
}

Control statements, such as if, require code blocks only when executing multiple statements. However, it is considered a best practice to always use code blocks with control statements, even if there’s only one statement to be executed, as in the following examples:

// valid, but error-prone and should be avoided
if (test)
console.log(test);
// preferred
if (test) { console.log(test);
}

 Using code blocks for control statements makes the intent clearer, and there’s less chance for errors when changes need to be made.

KEYWORDS AND RESERVED WORDS

 ECMA-262 describes a set of reserved keywords that have specific uses, such as indicating the beginning or end of control statements or performing specific operations. By rule, keywords are reserved and cannot be used as identifiers or property names. The complete list of keywords for ECMA-262, sixth edition is as follows:

break            do            in                    typeof
case             else          instanceof      var
catch            export       new               void
class            extends     return            while
const            finally        super             with
continue       for             switch            yield
debugger     function     this
default          if               throw
delete           import       try

The specification also describes a set of future reserved words that cannot be used as identifiers or property names. Though reserved words don’t have any specific usage in the language, they are reserved for future use as keywords.


The following is the complete list of future reserved words defined in ECMA-262, sixth edition:

Always reserved:
enum
Reserved in strict mode:
implements package public
interface protected static
let private
Reserved in module code:
await

These words may still not be used as identifiers but now can be used as property names in objects. Generally speaking, it’s best to avoid using both keywords and reserved words as both identifiers and property names to ensure compatibility with past and future ECMAScript editions.

VARIABLES

 ECMAScript variables are loosely typed, meaning that a variable can hold any type of data. Every variable is simply a named placeholder for a value. There are three keywords that can be used to declare a variable: var, which is available in all ECMAScript versions, and const and let, which were introduced in ECMAScript 6.

The ’var’ Keyword

 To define a variable, use the var operator (note that var is a keyword) followed by the variable name (an identifier, as described earlier), like this:

var message;

This code defines a variable named message that can be used to hold any value. (Without initialization, it holds the special value undefined, which is discussed in the next section.) ECMAScript implements variable initialization, so it’s possible to define the variable and set its value at the same time, as in this example:

var message = "hi";

 Here, message is defined to hold a string value of "hi". Doing this initialization doesn’t mark the variable as being a string type; it is simply the assignment of a value to the variable. It is still possible to not only change the value stored in the variable but also change the type of value, such as this:

var message = "hi";
message = 100; // legal, but not recommended

In this example, the variable message is first defined as having the string value "hi" and then overwritten with the numeric value 100. Although it’s not recommended to switch the data type that a variable contains, it is completely valid in ECMAScript.

var Declaration Scope 

 It’s important to note that using the var operator to define a variable makes it local to the function scope in which it was defined. For example, defining a variable inside of a function using var means that the variable is destroyed as soon as the function exits, as shown here:

function test() {
var message = "hi"; // local variable
}
test();
console.log(message); // error!

 Here, the message variable is defined within a function using var. The function is called test(), which creates the variable and assigns its value. Immediately after that, the variable is destroyed so the last line in this example causes an error. It is, however, possible to define a variable globally by simply omitting the var operator as follows:

function test() {
message = "hi"; // global variable
}
test();
console.log(message); // "hi"

 By removing the var operator from the example, the message variable becomes global. As soon as the function test() is called, the variable is defined and becomes accessible outside of the function once it has been executed.

NOTE Although it’s possible to define global variables by omitting the var operator, this approach is not recommended. Global variables defined locally are hard to maintain and cause confusion because it’s not immediately apparent if the omission of var was intentional. Strict mode throws a ReferenceError when an undeclared variable is assigned a value. 

If you need to define more than one variable, you can do it using a single statement, separating each variable (and optional initialization) with a comma like this:

var message = "hi",
found = false,
age = 29;

Here, three variables are defined and initialized. Because ECMAScript is loosely typed, variable initializations using different data types may be combined into a single statement. Though inserting line breaks and indenting the variables isn’t necessary, it helps to improve readability.

When you are running in strict mode, you cannot define variables named eval or arguments. Doing so results in a syntax error.

var Declaration Hoisting

 When using var, the following is possible because variables declared using that keyword are hoisted to the top of the function scope:

function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined

This does not throw an error because the ECMAScript runtime technically treats it like this:

function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined

This is “hoisting,” where the interpreter pulls all variable declarations to the top of its scope. It also allows you to use redundant var declarations without penalty:

function foo() {
    var age = 16;
    var age = 26;
    var age = 36;
    console.log(age);
}
foo(); // 36

’let’ Declarations

 let operates in nearly the same way as var, but with some important differences. Most notable is that let is block scoped, but var is function scoped.

if (true) {
var name = 'Matt';
    console.log(name); // Matt
}

console.log(name); // Matt

if (true) {
    let age = 26;
    console.log(age); // 26
}

console.log(age); // ReferenceError: age is not defined

Here, the age variable cannot be referenced outside the if block because its scope does not extend outside the block. Block scope is strictly a subset of function scope, so any scope limitations that apply to var declarations will also apply to let declarations.

A let declaration also does not allow for any redundant declarations within a block scope. Doing so will result in an error:

var name;
var name;
let age;
let age; // SyntaxError; identifier 'age' has already been declared

Of course, the JavaScript engine will keep track of identifiers used for variable declarations and the block scope they were declared inside, so nesting using identical identifiers behaves as you would expect with no errors because no redeclaration is occurring:

var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) {
var name = 'Matt';
console.log(name); // 'Matt'
}
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}

The declaration redundancy errors are not a function of order, and are not affected if let is mixed with var. The different keywords do not declare different types of variables—they just specify how the variables exist inside the relevant scope.

var name;
let name; // SyntaxError
let age;
var age; // SyntaxError

 Temporal Dead Zone

 Another important behavior of let distinguishing it from var is that let declarations cannot be used in a way that assumes hoisting:

// name is hoisted
console.log(name); // undefined
var name = 'Matt';
// age is not hoisted
console.log(age); // ReferenceError: age is not defined
let age = 26;

 When parsing the code, JavaScript engines will still be aware of the let declarations that appear later in a block, but these variables will be unable to be referenced in any way before the actual declaration occurs. The segment of execution that occurs before the declaration is referred to as the “temporal dead zone,” and any attempted references to these variables will throw a ReferenceError.

Global Declarations

Unlike the var keyword, when declaring variables using let in the global context, variables will not attach to the window object as they do with var.

 

var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined

However, let declarations will still occur inside the global block scope, which will persist for the lifetime of the page. Therefore, you must ensure your page does not attempt duplicate declarations in order to avoid throwing a SyntaxError.

Conditional Declaration

 When using var to declare variables, because the declaration is hoisted, the JavaScript engine will happily combine redundant declarations into a single declaration at the top of the scope. Because let declarations are scoped to blocks, it’s not possible to check if a let variable has previously been declared and conditionally declare it only if it has not.

<script>
var name = 'Nicholas';
let age = 26;
</script>
<script>
// Suppose this script is unsure about what has already been declared in the page.
// It will assume variables have not been declared.
var name = 'Matt';
// No problems here, since this will be handled as a single hoisted declaration.
// There is no need to check if it was previously declared.
let age = 36;
// This will throw an error when 'age' has already been declared.
</script>

Using a try/catch statement or the typeof operator are not solutions, as the let declaration inside the conditional block will be scoped to that block.

<script>
let name = 'Nicholas';
let age = 36;
</script>
<script>
// Suppose this script is unsure about what has already been declared in
the page.
// It will assume variables have not been declared.
if (typeof name !== 'undefined') {
let name;
}
// 'name' is restricted to the if {} block scope,
// so this assignment will act as a global assignment
name = 'Matt';
try (age) {
// If age is not declared, this will throw an error
}
catch(error) {
let age;
}
// 'age' is restricted to the catch {} block scope,
// so this assignment will act as a global assignment
age = 26;
</script>

Because of this, you cannot rely on a conditional declaration pattern with this new ES6 declaration keyword.


NOTE Not being able to use let for conditional declaration is a good thing, as conditional declaration is a bad pattern to have in your codebase. It makes it harder to understand program flow. If you find yourself reaching for this pattern, chances are very good that there is a better way to go about writing it.


let Declaration in for Loops

 Prior to the advent of let, for loop definition involved using an iterator variable whose definition would bleed outside the loop body:

for (var i = 0; i < 5; ++i) {
// do loop things
}
console.log(i); // 5

This is no longer a problem when switching to let declarations, as the iterator variable will be scoped only to the for loop block:

for (let i = 0; i < 5; ++i) {
    // do loop things
}
console.log(i); // ReferenceError: i is not defined

When using var, a frequent problem encountered was the singular declaration and modification of the iterator variable:

for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// You might expect this to console.log 0, 1, 2, 3, 4
// It will actually console.log 5, 5, 5, 5, 5

 This happens because the loop exits with its iterator variable still set to the value that caused the loop to exit: 5. When the timeouts later execute, they reference this same variable, and consequently console.log its final value.

When using let to declare the loop iterator, behind the scenes the JavaScript engine will actually declare a new iterator variable each loop iteration. Each setTimeout references that separate instance, and therefore it will console.log the expected value: the value of the iterator variable when that loop iteration was executed.

for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// console.logs 0, 1, 2, 3, 4

This per-iteration declarative behavior is applicable for all styles of for loops, including for-in and for-of loops.

’const’ Declarations

const behaves identically to that of let but with one important difference—it must be initialized with a value, and that value cannot be redefined after declaration. Attempting to modify a const variable will result in a runtime error. 

const age = 26;
age = 36; // TypeError: assignment to a constant
// const still disallows redundant declaration
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const is still scoped to blocks
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt

The const declaration is only enforced with respect to the reference to the variable that it points to. If a const variable references an object, it does not violate the const constraints to modify properties inside that object.

const person = {};
person.name = 'Matt'; // ok

Even though the JavaScript engine is creating new instances of let iterator variables in for loops, and even though const variables behave similarly to let variables, you cannot use const to declare for loop iterators:

for (const i = 0; i < 10; ++i) {} // TypeError: assignment to constant variable

 However, if you were to declare a for loop variable that is not modified, const is allowed—precisely because a new variable is declared for each iteration. This is especially relevant in the case of for-of and for-in loops:

let i = 0;
for (const j = 7; i < 5; ++i) {
console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}) {
console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]) {
console.log(value);
}
// 1, 2, 3, 4, 5

 Declaration Styles and Best Practices

 The introduction of let and const in ECMAScript 6 bring objectively better tooling to the language in the form of increased precision of declaration scope and semantics. It is no secret that the bizarre behavior of var declarations caused the JavaScript community to pull its collective hair out for years as a result of all the problems it caused. In the wake of the introduction of these new keywords, there are some increasingly common patterns emerging that can improve code quality.

Don’t Use var

With let and const, most developers will find that they no longer need to use var in their codebase anywhere. The patterns that emerge from restricting variable declaration to only let and const will serve to enforce higher codebase quality thanks to careful management of variable scope, declaration locality, and const correctness. 

Prefer const Over let 

Using const declarations allows the browser runtime to enforce constant variables, as well as for static code analysis tools to foresee illegal reassignment operations. Therefore, many developers feel it is to their advantage to, by default, declare variables as const unless they know they will need to reassign its value at some point. This allows for developers to more concretely reason about values that they know will never change, and also for quick detection of unexpected behavior in cases where the code execution attempts to perform an unanticipated value reassignment. 

DATA TYPES

 There are six simple data types (also called primitive types) in ECMAScript: Undefined, Null, Boolean, Number, String, and Symbol. Symbol was newly introduced in ECMAScript 6. There is also one complex data type called Object, which is an unordered list of name–value pairs. Because there is no way to define your own data types in ECMAScript, all values can be represented as one of these seven. Having only seven data types may seem like too few to fully represent data; however, ECMAScript’s data types have dynamic aspects that make each single data type behave like several.

The typeof Operator 

 Because ECMAScript is loosely typed, there needs to be a way to determine the data type of a given variable. The typeof operator provides that information. Using the typeof operator on a value returns one of the following strings:

➤➤ "undefined" if the value is undefined


➤➤ "boolean" if the value is a Boolean


➤➤ "string" if the value is a string


➤➤ "number" if the value is a number


➤➤ "object" if the value is an object (other than a function) or null


➤➤ "function" if the value is a function


➤➤ "symbol" if the value is a Symbol

The typeof operator is called like this:

let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"

In this example, both a variable (message) and a numeric literal are passed into the typeof operator. Note that because typeof is an operator and not a function, no parentheses are required (although they can be used).

Be aware there are a few cases where typeof seemingly returns a confusing but technically correct value. Calling typeof null returns a value of "object", as the special value null is considered to be an empty object reference.



NOTE Technically, functions are considered objects in ECMAScript and don’t represent another data type. However, they do have some special properties, which necessitates differentiating between functions and other objects via the typeof operator.


 The Undefined Type

 The Undefined type has only one value, which is the special value undefined. When a variable is declared using var or let but not initialized, it is assigned the value of undefined as follows:

let message;
console.log(message == undefined); // true

 In this example, the variable message is declared without initializing it. When compared with the literal value of undefined, the two are equal. This example is identical to the following:

let message = undefined;
console.log(message == undefined); // true

 Here the variable message is explicitly initialized to be undefined. This is unnecessary because, by default, any uninitialized variable gets the value of undefined.

 


NOTE Generally speaking, you should never explicitly set a variable to be undefined. The literal undefined value is provided mainly for comparison and wasn’t added until ECMA-262, third edition, to help formalize the difference between an empty object pointer (null) and an uninitialized variable.


Note that a variable containing the value of undefined is different from a variable that hasn’t been defined at all. Consider the following:

let message; // this variable is declared but has a value of undefined
// make sure this variable isn't declared
// let age
console.log(message); // "undefined"
console.log(age); // causes an error

In this example, the first console.log displays the variable message, which is "undefined". In the second console.log, an undeclared variable called age is passed into the console.log() function, which causes an error because the variable hasn’t been declared. Only one useful operation can be performed on an undeclared variable: you can call typeof on it (calling delete on an undeclared variable won’t cause an error, but this isn’t very useful and in fact throws an error in strict mode).

The typeof operator returns "undefined" when called on an uninitialized variable, but it also returns "undefined" when called on an undeclared variable, which can be a bit confusing. Consider this example:

let message; // this variable is declared but has a value of undefined
// make sure this variable isn't declared
// let age
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined"

In both cases, calling typeof on the variable returns the string "undefined". Logically, this makes sense because no real operations can be performed with either variable even though they are technically very different.


NOTE Even though uninitialized variables are automatically assigned a value of undefined, it is advisable to always initialize variables. That way, when typeof returns "undefined", you’ll know that it’s because a given variable hasn’t been declared rather than was simply not initialized.


The value undefined is falsy; therefore, you are able to more succinctly check for it wherever you might need to. Bear in mind, however, that many other possible values are also falsy, so be careful in scenarios where you need to test for an exact value of undefined rather than just a falsy value:

let message; // this variable is declared but has a value of undefined
// 'age' is not declared
if (message) {
// This block will not execute
}
if (!message) {
// This block will execute
}
if (age) {
// This will throw an error
}

The Null Type

The Null type is the second data type that has only one value: the special value null. Logically, a null value is an empty object pointer, which is why typeof returns "object" when it’s passed a null value in the following example: 

let car = null;
console.log(typeof car); // "object"

When defining a variable that is meant to later hold an object, it is advisable to initialize the variable to null as opposed to anything else. That way, you can explicitly check for the value null to determine if the variable has been filled with an object reference at a later time, such as in this example:

if (car != null) {
// do something with car
}

The value undefined is a derivative of null, so ECMA-262 defines them to be superficially equal as follows:

console.log(null == undefined); // true

Using the equality operator (==) between null and undefined always returns true, though keep in mind that this operator converts its operands for comparison purposes (covered in detail later in this chapter).

Even though null and undefined are related, they have very different uses. As mentioned previously, you should never explicitly set the value of a variable to undefined, but the same does not hold true for null. Any time an object is expected but is not available, null should be used in its place. This helps to keep the paradigm of null as an empty object pointer and further differentiates it from undefined.

The null type is falsy; therefore, you are able to more succinctly check for it wherever you might need to. Bear in mind, however, that many other possible values are also falsy, so be careful in scenarios where you need to test for an exact value of null rather than just a falsy value:

let message = null;
let age;
if (message) {
// This block will not execute
}
if (!message) {
// This block will execute
}
if (age) {
// This block will not execute
}
if (!age) {
// This block will execute
}

The Boolean Type

 The Boolean type is one of the most frequently used types in ECMAScript and has only two literal values: true and false. These values are distinct from numeric values, so true is not equal to 1, and false is not equal to 0. Assignment of Boolean values to variables is as follows:

let found = true;
let lost = false;

Note that the Boolean literals true and false are case–sensitive, so True and False (and other mixings of uppercase and lowercase) are valid as identifiers but not as Boolean values.

Though there are just two literal Boolean values, all types of values have Boolean equivalents in ECMAScript. To convert a value into its Boolean equivalent, the special Boolean() casting function is called, like this:

let message = "Hello world!";
let messageAsBoolean = Boolean(message);

In this example, the string message is converted into a Boolean value and stored in messageAsBoolean. The Boolean() casting function can be called on any type of data and will always return a Boolean value. The rules for when a value is converted to true or false depend on the data type as much as the actual value. The following table outlines the various data types and their specific conversions.

 

DATA TYPEVALUES CONVERTED TO TRUEVALUES CONVERTED TO FALSE
Booleantruefalse
StringAny nonempty string"" (empty string)
NumberAny nonzero number
(including infinity)
0, NaN (See the “NaN” section later in
this chapter.)
ObjectAny objectnull
Undefinedn/aundefined

These conversions are important to understand because flow-control statements, such as the if statement, automatically perform this Boolean conversion, as shown here:

let message = "Hello world!";
if (message) {
console.log("Value is true");
}

In this example, the console.log will be displayed because the string message is automatically converted into its Boolean equivalent (true). It’s important to understand what variable you’re using in a flow-control statement because of this automatic conversion. Mistakenly using an object instead of a Boolean can drastically alter the flow of your application.

The Number Type

 Perhaps the most interesting data type in ECMAScript is Number, which uses the IEEE–754 format to represent both integers and floating-point values (also called double–precision values in some languages). To support the various types of numbers, there are several different number literal formats.

The most basic number literal format is that of a decimal integer, which can be entered directly as shown here:

let intNum = 55; // integer

 Integers can also be represented as either octal (base 8) or hexadecimal (base 16) literals. For an octal literal, the first digit must be a zero (0) followed by a sequence of octal digits (numbers 0 through 7). If a number out of this range is detected in the literal, then the leading zero is ignored and the number is treated as a decimal, as in the following examples:

let octalNum1 = 070; // octal for 56
let octalNum2 = 079; // invalid octal - interpreted as 79
let octalNum3 = 08; // invalid octal - interpreted as 8

Octal literals are invalid when running in strict mode and will cause the JavaScript engine to throw a syntax error.

To create a hexadecimal literal, you must make the first two characters 0x (case insensitive), followed by any number of hexadecimal digits (0 through 9, and A through F). Letters may be in uppercase or lowercase. Here’s an example:

let hexNum1 = 0xA; // hexadecimal for 10
let hexNum2 = 0x1f; // hexadecimal for 31

Numbers created using octal or hexadecimal format are treated as decimal numbers in all arithmetic operations.



NOTE Because of the way that numbers are stored in JavaScript, it is actually possible to have a value of positive zero (+0) and negative zero (–0). Positive zero and negative zero are considered equivalent in all cases but are noted in this text for clarity.


Floating-Point Values

 To define a floating-point value, you must include a decimal point and at least one number after the decimal point. Although an integer is not necessary before a decimal point, it is recommended. Here are some examples:

let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // valid, but not recommended

Because storing floating-point values uses twice as much memory as storing integer values, ECMAScript always looks for ways to convert values into integers. When there is no digit after the decimal point, the number becomes an integer. Likewise, if the number being represented is a whole number (such as 1.0), it will be converted into an integer, as in this example:

let floatNum1 = 1.; // missing digit after decimal - interpreted as integer 1
let floatNum2 = 10.0; // whole number - interpreted as integer 10

 For very large or very small numbers, floating-point values can be represented using e-notation. E-notation is used to indicate a number that should be multiplied by 10 raised to a given power. The format of e-notation in ECMAScript is to have a number (integer or floating-point) followed by an uppercase or lowercase letter E, followed by the power of 10 to multiply by. Consider the following:

let floatNum = 3.125e7; // equal to 31250000

 In this example, floatNum is equal to 31,250,000 even though it is represented in a more compact form using e-notation. The notation essentially says, “Take 3.125 and multiply it by 10^7.”

E-notation can also be used to represent very small numbers, such as 0.00000000000000003, which can be written more succinctly as 3e–17. By default, ECMAScript converts any floating-point value with at least six zeros after the decimal point into e-notation (for example, 0.0000003 becomes 3e–7).

Floating-point values are accurate up to 17 decimal places but are far less accurate in arithmetic computations than whole numbers. For instance, adding 0.1 and 0.2 yields 0.30000000000000004 instead of 0.3. These small rounding errors make it difficult to test for specific floating-point values. Consider this example:

if (a + b == 0.3) { // avoid!
console.log("You got 0.3.");
}

 Here, the sum of two numbers is tested to see if it’s equal to 0.3. This will work for 0.05 and 0.25 and for 0.15 and 0.15. But if applied to 0.1 and 0.2, as discussed previously, this test would fail. Therefore you should never test for specific floating-point values.

NOTE It’s important to understand that rounding errors are a side effect of the way floating-point arithmetic is done in IEEE-754–based numbers and is not unique to ECMAScript. Other languages that use the same format have the same
issues.

 Range of Values

 Not all numbers in the world can be represented in ECMAScript because of memory constraints. The smallest number that can be represented in ECMAScript is stored in Number.MIN_VALUE and is 5e–324 on most browsers; the largest number is stored in Number.MAX_VALUE and is 1.7976931348623157e+308 on most browsers. If a calculation results in a number that cannot be represented by JavaScript’s numeric range, the number automatically gets the special value of Infinity. Any negative number that can’t be represented is –Infinity (negative infinity), and any positive number that can’t be represented is simply Infinity (positive infinity).

If a calculation returns either positive or negative Infinity, that value cannot be used in any further calculations, because Infinity has no numeric representation with which to calculate. To determine if a value is finite (that is, it occurs between the minimum and the maximum), there is the isFinite() function. This function returns true only if the argument is between the minimum and
the maximum values, as in this example:

let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false

 Though it is rare to do calculations that take values outside of the range of finite numbers, it is possible and should be monitored when doing very large or very small calculations.

NOTE You can also get the values of positive and negative Infinity by accessing Number.NEGATIVE _ INFINITY and Number.POSITIVE _ INFINITY. As you may expect, these properties contain the values –Infinity and Infinity, respectively.

 NaN

 There is a special numeric value called NaN, short for Not a Number, which is used to indicate when an operation intended to return a number has failed (as opposed to throwing an error). For example, dividing any number by 0 typically causes an error in other programming languages, halting code execution. In ECMAScript, dividing a number by 0 returns NaN, which allows other processing to continue.

The value NaN has a couple of unique properties. First, any operation involving NaN always returns NaN (for instance, NaN /10), which can be problematic in the case of multistep computations. Second, NaN is not equal to any value, including NaN. For example, the following returns false:

console.log(NaN == NaN); // false

 For this reason, ECMAScript provides the isNaN() function. This function accepts a single argument, which can be of any data type, to determine if the value is “not a number.” When a value is passed into isNaN(), an attempt is made to convert it into a number. Some nonnumerical values convert into numbers directly, such as the string "10" or a Boolean value. Any value that cannot be converted into a number causes the function to return true. Consider the following:

console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false - 10 is a number
console.log(isNaN("10")); // false - can be converted to number 10
console.log(isNaN("blue")); // true - cannot be converted to a number
console.log(isNaN(true)); // false - can be converted to number 1

 This example tests five different values. The first test is on the value NaN itself, which, obviously, returns true. The next two tests use numeric 10 and the string "10", which both return false because the numeric value for each is 10. The string "blue", however, cannot be converted into a number, so the function returns true. The Boolean value of true can be converted into the number 1, so the function returns false.

NOTE Although typically not done, isNaN() can be applied to objects. In that case, the object’s valueOf() method is first called to determine if the returned value can be converted into a number. If not, the toString() method is called and its returned value is tested as well. This is the general way that built-in functions and operators work in ECMAScript and is discussed more in the “Operators” section later in this chapter.

Number Conversions 

 There are three functions to convert nonnumeric values into numbers: the Number() casting function, the parseInt() function, and the parseFloat() function. The first function, Number(), can be used on any data type; the other two functions are used specifically for converting strings to numbers. Each of these functions reacts differently to the same input.

The Number() function performs conversions based on these rules: 

➤➤ When applied to Boolean values, true and false get converted into 1 and 0, respectively.


➤➤ When applied to numbers, the value is simply passed through and returned.


➤➤ When applied to null, Number() returns 0.


➤➤ When applied to undefined, Number() returns NaN.


➤➤ When applied to strings, the following rules are applied:


➤➤ If the string contains only numeric characters, optionally preceded by a plus or minus sign, it is always converted to a decimal number, so Number("1") becomes 1, Number("123") becomes 123, and Number("011") becomes 11 (note: leading zeros are ignored).


➤➤ If the string contains a valid floating-point format, such as "1.1", it is converted into the appropriate floating-point numeric value (once again, leading zeros are ignored).


➤➤ If the string contains a valid hexadecimal format, such as "0xf", it is converted into an integer that matches the hexadecimal value.


➤➤ If the string is empty (contains no characters), it is converted to 0.


➤➤ If the string contains anything other than these previous formats, it is converted
into NaN.


➤➤ When applied to objects, the valueOf() method is called and the returned value is converted based on the previously described rules. If that conversion results in NaN, the toString() method is called and the rules for converting strings are applied.

Converting to numbers from various data types can get complicated, as indicated by the number of rules there are for Number(). Here are some concrete examples:

let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1

In these examples, the string "Hello world" is converted into NaN because it has no corresponding numeric value, and the empty string is converted into 0. The string "000011" is converted to the number 11 because the initial zeros are ignored. Last, the value true is converted to 1.

NOTE The unary plus operator, discussed in the “Operators” section later in this chapter, works the same as the Number() function. 

 Because of the complexities and oddities of the Number() function when converting strings, the parseInt() function is usually a better option when you are dealing with integers. The parseInt() function examines the string much more closely to see if it matches a number pattern. Leading white space in the string is ignored until the first non–white space character is found. If this first character isn’t a number, the minus sign, or the plus sign, parseInt() always returns NaN, which means the empty string returns NaN (unlike with Number(), which returns 0). If the first character is a number, plus, or minus, then the conversion goes on to the second character and continues on until either the end of the string is reached or a nonnumeric character is found. For instance, "1234blue" is converted to 1234 because "blue" is completely ignored. Similarly, "22.5" will be converted to 22 because the decimal is not a valid integer character.

 Assuming that the first character in the string is a number, the parseInt() function also recognizes the various integer formats (decimal, octal, and hexadecimal, as discussed previously). This means when the string begins with "0x", it is interpreted as a hexadecimal integer; if it begins with "0" followed by a number, it is interpreted as an octal value.

Here are some conversion examples to better illustrate what happens:

let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10 - hexadecimal
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70 - decimal
let num6 = parseInt("0xf"); // 15 - hexadecimal

All of the different numeric formats can be confusing to keep track of, so parseInt() provides a second argument: the radix (number of digits). If you know that the value you’re parsing is in hexadecimal format, you can pass in the radix 16 as a second argument and ensure that the correct parsing will occur, as shown here:

let num = parseInt("0xAF", 16); // 175

In fact, by providing the hexadecimal radix, you can leave off the leading "0x" and the conversion will work as follows:

let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN

In this example, the first conversion occurs correctly, but the second conversion fails. The difference is that the radix is passed in on the first line, telling parseInt() that it will be passed a hexadecimal string; the second line sees that the first character is not a number and stops automatically.

Passing in a radix can greatly change the outcome of the conversion. Consider the following:

let num1 = parseInt("10", 2); // 2 - parsed as binary
let num2 = parseInt("10", 8); // 8 - parsed as octal
let num3 = parseInt("10", 10); // 10 - parsed as decimal
let num4 = parseInt("10", 16); // 16 - parsed as hexadecimal

Because leaving off the radix allows parseInt() to choose how to interpret the input, it’s advisable to always include a radix to avoid errors.

NOTE Most of the time you’ll be parsing decimal numbers, so it’s good to always include 10 as the second argument.

 The parseFloat() function works in a similar way to parseInt(), looking at each character starting in position 0. It also continues to parse the string until it reaches either the end of the string or a character that is invalid in a floating-point number. This means that a decimal point is valid the first time it appears, but a second decimal point is invalid and the rest of the string is ignored, resulting in "22.34.5" being converted to 22.34.

 Another difference in parseFloat() is that initial zeros are always ignored. This function will recognize any of the floating-point formats discussed earlier, as well as the decimal format (leading zeros are always ignored). Hexadecimal numbers always become 0. Because parseFloat() parses only decimal values, there is no radix mode. A final note: if the string represents a whole number (no decimal point or only a zero after the decimal point), parseFloat() returns an integer. Here are some examples:

let num1 = parseFloat("1234blue"); // 1234 - integer
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000

The String Type

 The String data type represents a sequence of zero or more 16-bit Unicode characters. Strings can be delineated by either double quotes ("), single quotes ('), or backticks (`), so all of the following are legal:

let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`

Unlike some languages in which using different quotes changes how the string is interpreted, there is no difference in the syntaxes in ECMAScript. Note, however, that a string beginning with a certain character must end with the same character. For example, the following will cause a syntax error:

let firstName = 'Nicholas"; // syntax error - quotes must match

Character Literals

 The String data type includes several character literals to represent nonprintable or otherwise useful characters, as listed in the following table:

 

LITERALMEANING
\nNew line
\tTab
\bBackspace
\rCarriage return
\fForm feed
\\Backslash (\)
\'Single quote (')—used when the string is delineated by single quotes. Example: 'He said, \'hey.\''.
\"Double quote (")—used when the string is delineated by double quotes. Example:
"He said, \"hey.\"".
\`Backtick (`)—used when the string is delineated by backticks. Example: `He said,
\`hey.\``.
\xnnA character represented by hexadecimal code nn (where n is a hexadecimal digit
0-F). Example: \x41 is equivalent to "A".
\unnnnA Unicode character represented by the hexadecimal code nnnn (where n is a
hexadecimal digit 0-F). Example: \u03a3 is equivalent to the Greek character Σ.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

These character literals can be included anywhere with a string and will be interpreted as if they were a single character, as shown here:

let text = "This is the letter sigma: \u03a3.";

In this example, the variable text is 28 characters long even though the escape sequence is 6 characters long. The entire escape sequence represents a single character, so it is counted as such.

The length of any string can be returned by using the length property, as follows:

console.log(text.length); // 28

This property returns the number of 16-bit characters in the string.

NOTE If a string contains double–byte characters, the length property may not accurately return the number of characters in the string. Mitigation strategies for this are detailed in the Basic Reference Types chapter.

 The Nature of Strings

 Strings are immutable in ECMAScript, meaning that once they are created, their values cannot change. To change the string held by a variable, the original string must be destroyed and the variable filled with another string containing a new value, like this:

let lang = "Java";
lang = lang + "Script";

Here, the variable lang is defined to contain the string "Java". On the next line, lang is redefined to combine "Java" with "Script", making its value "JavaScript". This happens by creating a new string with enough space for 10 characters and then filling that string with "Java" and "Script". The last step in the process is to destroy the original string "Java" and the string "Script", because neither is necessary anymore. All of this happens behind the scenes, which is why older browsers (such as pre–1.0 versions of Firefox and Internet Explorer 6.0) had very slow string concatenation. These inefficiencies were addressed in later versions of these browsers.

Converting to a String

There are two ways to convert a value into a string. The first is to use the toString() method that almost every value has. This method’s only job is to return the string equivalent of the value. Consider this example: 

let ageAsString = age.toString(); // the string "11"
let found = true;
let foundAsString = found.toString(); // the string "true"

The toString() method is available on values that are numbers, Booleans, objects, and strings. (Yes, each string has a toString() method that simply returns a copy of itself.) If a value is null or undefined, this method is not available.

In most cases, toString() doesn’t have any arguments. However, when used on a number value, toString() actually accepts a single argument: the radix in which to output the number. By default, toString() always returns a string that represents the number as a decimal, but by passing in a radix, toString() can output the value in binary, octal, hexadecimal, or any other valid base, as in this example:

let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

This example shows how the output of toString() can change for numbers when providing a radix. The value 10 can be output into any number of numeric formats. Note that the default (with no argument) is the same as providing a radix of 10.

If you’re not sure that a value isn’t null or undefined, you can use the String() casting function, which always returns a string regardless of the value type. The String() function follows these rules:

➤➤ If the value has a toString() method, it is called (with no arguments) and the result is returned.


➤➤ If the value is null, "null" is returned.


➤➤ If the value is undefined, "undefined" is returned.

Consider the following:

let value1 = 10;
let value2 = true;
let value3 = null;
let value4;

console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"

Here, four values are converted into strings: a number, a Boolean, null, and undefined. The result for the number and the Boolean are the same as if toString() were called. Because toString() isn’t available on "null" and "undefined", the String() method simply returns literal text for those values.

 NOTE You can also convert a value to a string by adding an empty string ("") to that value using the plus operator (discussed in the “Operators” section later in this chapter).

Template Literals 

 New in ECMAScript 6 is the capability to define strings using template literals. Unlike their single and double quoted counterparts, template literals respect new line characters, and can be defined spanning multiple lines:

let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;

console.log(myMultiLineString);
// first line
// second line"
console.log(myMultiLineTemplateLiteral);
// first line
// second line

console.log(myMultiLineString === myMultiLinetemplateLiteral); // true

As the name suggests, template literals are especially useful when defining templates, such as HTML:

let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;

Because template literals will exactly match the whitespace inside the backticks, special care will need to be applied when defining them. A correctly formatted template string may appear to have improper indentation:

// This template literal has 25 spaces following the line return character
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47

// This template literal begins with a line return character
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === '\n'); // true

// This template literal has no unexpected whitespace characters
let thirdTemplateLiteral = `first line
second line`;
console.log(thirdTemplateLiteral[0]);
// first line
// second line

 Interpolation

One of the most useful features of template literals is their support for interpolation, which allows you to insert values at one or more places inside a single unbroken definition. Technically, template literals aren’t strings, they are special JavaScript syntactical expressions that evaluate into strings. Template literals are evaluated immediately when they are defined and converted into a string instance, and any interpolated variables will be drawn from its immediate scope. 

This can be accomplished using a JavaScript expression inside ${}:

let value = 5;
let exponent = 'second';

// Formerly, interpolation was accomplished as follows:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// The same thing accomplished with template literals:
let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;

console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25

The value being interpolated will eventually be coerced into a string using toString(), but any JavaScript expression can safely be interpolated. Nesting template strings is safe with no escaping required:

console.log(`Hello, ${ `World` }!`); // Hello, World!

toString() is invoked to coerce expression result into string:

let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!

Invoking functions and methods inside interpolated expressions is allowed:

function capitalize(word) {
return `${ word[0].toUpperCase() }${ word.slice(1) }`;
}
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!

Additionally, templates can safely interpolate their previous value:

let value = '';
function append() {
value = `${value}abc`
console.log(value);
}
append(); // abc
append(); // abcabc
append(); // abcabcabc

Template Literal Tag Functions 

 Template literals also support the ability to define tag functions, which are able to define custom interpolation behavior. The tag function is passed the individual pieces after the template has been split by the interpolation token and after the expressions have been evaluated.

A tag function is defined as a regular function and is applied to a template literal by being prepended to it, as shown in the code that follows. The tag function will be passed the template literal split into its pieces: the first argument is an array of the raw strings, and the remaining arguments are the results of the evaluated expressions. The return value of this function will be the string evaluated from the template literal.

This is best demonstrated by example:

let a = 6;
let b = 9;

function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
    console.log(strings);
    console.log(aValExpression);
    console.log(bValExpression);
    console.log(sumExpression);

    return 'foobar';
}

let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15

console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"

 Because there are a variable number of expression arguments, using the spread operator to combine them into a single collection is usually prudent:

let a = 6;
let b = 9;

function simpleTag(strings, ...expressions) {
    console.log(strings);
    for(const expression of expressions) {
        console.log(expression);
}

    return 'foobar';
}

let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15

console.log(taggedResult); // "foobar"

For a template literal with n interpolated values, the number of expression arguments to the tag function will always be n, and the number of string pieces in the first argument will always be exactly n + 1. Therefore, if you wished to “zip” the strings and the evaluated expressions together into the default returned string, you could do so as follows:

let a = 6;
let b = 9;

function zipTag(strings, ...expressions) {
    return strings[0] +
        expressions.map((e, i) => `${e}${strings[i + 1]}`)
            .join('');
}

let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = zipTag`${ a } + ${ b } = ${ a + b }`;

console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "6 + 9 = 15"

Raw Strings

It is also possible to use template literals to give you access to the raw template literal contents without being converted into actual character representations, such as a new line or Unicode character. This can be done by using the String.raw tag function, which is available by default. 

// Unicode demo
// \u00A9 is the copyright symbol
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9

// Newline demo
console.log(`first line\nsecond line`);
// first line
// second line

console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"

// This does not work for actual newline characters: they do not
// undergo conversion from their plaintext escaped equivalents
console.log(`first line
second line`);
// first line
// second line

console.log(String.raw`first line
second line`);
// first line
// second line

The raw values are also available as a property on each element in the string piece collection inside the tag function:

function printRaw(strings) {
    console.log('Actual characters:');
    for (const string of strings) {
        console.log(string);
}

console.log('Escaped characters;');
for (const rawString of strings.raw) {
    console.log(rawString);
    }
}

printRaw`\u00A9${ 'and' }\n`;
// Actual characters:
// ©
// (newline)
// Escaped characters:
// \u00A9
// \n

 The Symbol Type

 New in ECMAScript 6 is the Symbol data type. Symbols are primitive values, and symbol instances are unique and immutable. The purpose of a symbol is to be a guaranteed unique identifier for object properties that does not risk property collision.

 Although they may seem to share some similarities with private properties, symbols are not intended to offer private property behavior (especially because the Object API offers methods to easily discover symbol properties). Instead, symbols are intended to be used as unique tokens that can be used to key special properties with something other than a string.

Basic Symbol Use

 Symbols are instantiated using the Symbol function. Because it is its own primitive type, the typeof operator will identify a symbol as symbol.

let sym = Symbol();
console.log(typeof sym); // symbol

When invoking the function, you can provide an optional string that can be used for identifying the symbol instance when debugging. The string you provide is totally separate from the symbol’s definition or identity: 

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');

console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false

 Symbols do not have a literal string syntax, and this is central to their purpose. The specification governing how symbols operate allows you to create a new Symbol instance and use it to key a new property on an object with the guarantee that you will not be overwriting an existing object property—irrespective of whether it is using a string or symbol as a key.

let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()

let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);

Importantly, the Symbol function cannot be used with the new keyword. The purpose of this is to avoid symbol object wrappers, as is possible with Boolean, String, and Number, which support constructor behavior and instantiate a primitive wrapper object:

 

let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"

let myString = new String();
console.log(typeof myString); // "object"

let myNumber = new Number();
console.log(typeof myNumber); // "object"

let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

Should you want to utilize an object wrapper, you can make use of the Object() function:

let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"

Using the Global Symbol Registry

 In scenarios where different parts of the runtime would like to share and reuse a symbol instance, it is possible to create and reuse symbols in a string-keyed global symbol registry.

This behavior can be achieved using Symbol.for():

let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // "object"

Symbol.for() is an idempotent operation for each string key. The first time it is called with a given string, it will check the global runtime registry, find that no symbol exists, generate a new symbol instance, and add it to the registry. Additional invocations with the same string key will check the global runtime registry, find that a symbol does exist for that string, and return that symbol instance instead.

let fooGlobalSymbol = Symbol.for('foo'); // creates new symbol
let otherFooGlobalSymbol = Symbol.for('foo'); // reuses existing symbol

console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

Symbols defined in the global registry are totally distinct from symbols created using Symbol(), even if they share a description:

let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');

console.log(localSymbol === globalSymbol); // false

 The global registry requires string keys, so anything you provide as an argument to Symbol.for() will be converted to a string. Additionally, the key used for the registry will also be used as the symbol description.

let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)

It is possible to check against the global registry using Symbol.keyFor(), which accepts a symbol and will return the global string key for that global symbol, or undefined if the symbol is not a global symbol.

// Create global symbol
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo

// Create regular symbol
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined

Using Symbol.keyFor() with a non-symbol will throw a TypeError:

Symbol.keyFor(123); // TypeError: 123 is not a symbol

Using Symbols as Properties

 Anywhere you can normally use a string or number property, you can also use a symbol. This includes object literal properties and Object.defineProperty()/Object.defineProperties(). An object literal can only use a symbol as a property inside the computed property syntax.

let s1 = Symbol('foo'),
    s2 = Symbol('bar'),
    s3 = Symbol('baz'),
    s4 = Symbol('qux');

let o = {
    [s1]: 'foo val'
};
// Also valid: o[s1] = 'foo val';

console.log(o);
// {Symbol{foo}: foo val}

Object.defineProperty(o, s2, {value: 'bar val'});

console.log(o);
// {Symbol{foo}: foo val, Symbol(bar): bar val}

Object.defineProperties(o, {
    [s3]: {value: 'baz val'},
    [s4]: {value: 'qux val'}
});

console.log(o);
// {Symbol{foo}: foo val, Symbol(bar): bar val,
// Symbol{baz}: baz val, Symbol(qux): qux val}

 Just as Object.getOwnPropertyNames() returns an array of regular properties on an object instance, Object.getOwnPropertySymbols() returns an array of symbol properties on an object instance. The return values of these two methods are mutually exclusive. Object .getOwnPropertyDescriptors() will return an object containing both regular and symbol property descriptors. Reflect.ownKeys() will return both types of keys:

let s1 = Symbol('foo'),
    s2 = Symbol('bar');

let o = {
    [s1]: 'foo val',
    [s2]: 'bar val',
    baz: 'baz val',
    qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]

console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]

console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}

console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]

Because a property counts as a reference to that symbol in memory, Symbols are not lost if directly created and used as properties. However, declining to keep an explicit reference to a property means that traversing all the object’s symbol properties will be required to recover the property key:

let o = {
    [Symbol('foo')]: 'foo val',
    [Symbol('bar')]: 'bar val'
};

console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}

let barSymbol = Object.getOwnPropertySymbols(o)
               .find((symbol) => symbol.toString().match(/bar/));

console.log(barSymbol);
// Symbol(bar)

Well-Known Symbols

Along with the addition of symbols, ECMAScript 6 also introduced a collection of well-known symbols that would be used throughout the language to expose internal language behaviors for direct access, overriding, or emulating. These well-known symbols exist as string properties on the Symbol factory function. 

One of the primary utilities of these well-known symbols is redefining them as to alter the behavior of the native language constructs. For example, because it is known how the for-of loop will use the Symbol.iterator property on whatever object is provided to it, it is possible to provide a custom definition of Symbol.iterator’s value in a custom object in order to control how for-of behaves when provided that object.

There is nothing special about these well-known symbols, they are regular string properties on the Symbol global that key an instance of a symbol. Each well-defined symbol property is non-writeable, non-enumerable, and non-configurable.

NOTE In discussions about the ECMAScript specification, you will frequently see these symbols referred to by their specification names, which are prefixed with @@. For example, @@iterator refers to Symbol.iterator.

Symbol.asyncIterator 

 Per the ECMAScript specification, this symbol is used as a property for “A method that returns the default AsyncIterator for an object. Called by the semantics of the for-await-of statement”. It is used to identify the function that implements the asynchronous iterator API.

Language constructs such as the for-await-of loop make use of this function to perform asynchronous iteration. They will invoke the function keyed by Symbol.asyncIterator and expect it to return an object which implements the Iterator API. In many cases, this will take the form of an AsyncGenerator, an object which implements this API:

class Foo {
    async *[Symbol.asyncIterator]() {}
}

let f = new Foo();

console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}

Specifically, the object produced by the Symbol.asyncIterator function should sequentially produce Promise instances via its next() method. This can be through explicit next() method definition or implicitly through an async generator function:

class Emitter {
    constructor(max) {
        this.max = max;
        this.asyncIdx = 0;
}

async *[Symbol.asyncIterator]() {
    while(this.asyncIdx < this.max) {
        yield new Promise((resolve) => resolve(this.asyncIdx++));
        }
    }
}

async function asyncCount() {
    let emitter = new Emitter(5);

    for await(const x of emitter) {
        console.log(x);
    }
}

asyncCount();
// 0
// 1
// 2
// 3
// 4

NOTE Symbol.asyncIterator is part of the ES2018 specification, and so only very modern browser versions will support it. More details on asynchronous iteration and the for-await-of loop can be found in Appendix A.

Symbol.hasInstance 

Per the ECMAScript specification, this symbol is used as a property for “A method that determines if a constructor object recognizes an object as one of the constructor’s instances. Called by the semantics of the instanceof operator”. The instanceof operator provides a way of determining if an object instance has a prototype in its prototype chain. Typical use of the instanceof is as follows: 

function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true

class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true

 In ES6, the instanceof operator is using a Symbol.hasInstance function to evaluate this relationship. The Symbol.hasInstance keys a function which performs the same behavior but with the operands reversed:

function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true

class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true

This property is defined on the Function prototype, and therefore it is automatically available by default to all function and class definitions. Because the instanceof operator will seek the property definition on the prototype chain like any other property, it is possible to redefine the function on an inherited class as a static method:

class Bar {}
class Baz extends Bar {
    static [Symbol.hasInstance]() {
        return false;
    }
}

let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false

Symbol.isConcatSpreadable

Per the ECMAScript specification, this symbol is used as a property for “A Boolean valued property that if true indicates that an object should be flattened to its array elements by Array.prototype.concat()”. The Array.prototype.concat method in ES6 will select how to join an array-like object to the array instance based on the type of object it is passed. The value of Symbol.isConcatSpreadable allows you to override this behavior.

Array objects by default will be flattened into the existing array; a value of false or falsy value will append the entire object to the array. Array-like objects by default will be appended to the array; a value of true or truthy value will flatten the array-like object into the array instance. Other objects which are not array-like will be ignored when Symbol.isConcatSpreadable is set to true.

let initial = ['foo'];

let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)]

let arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']

let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo']

 Symbol.iterator

 

Per the ECMAScript specification, this symbol is used as a property for “A method that returns the default Iterator for an object. Called by the semantics of the for-of statement”. It is used to identify the function that implements the iterator API. 

Language constructs such as the for-of loop make use of this function to perform iteration. They will invoke the function keyed by Symbol.iterator and expect it to return an object which implements the Iterator API. In many cases, this will take the form of a Generator, an object which implements this API:

class Foo {
    *[Symbol.iterator]() {}
}

let f = new Foo();

console.log(f[Symbol.iterator]());
// Generator {<suspended>}

Specifically, the object produced by the Symbol.iterator function should sequentially produce values via its next() method. This can be through explicit next() method definition or implicitly through a generator function:

class Emitter {
    constructor(max) 
        this.max = max;
        this.idx = 0;
}

*[Symbol.iterator]() {
    while(this.idx < this.max) {
        yield this.idx++;
        }
    }
}

function count() {
    let emitter = new Emitter(5);

    for (const x of emitter) {
        console.log(x);
    }
}

count();
// 0
// 1
// 2
// 3
// 4

 NOTE Iterator definition is covered in depth in the Iterators and Generators chapter.

Symbol.match 

Per the ECMAScript specification, this symbol is used as a property for “A regular expression method that matches the regular expression against a string. Called by the String.prototype.match() method”. The String.prototype.match() method will use the function keyed by Symbol.match to evaluate the expression. The regular expression prototype has this function defined by default, and therefore all regular expression instances are valid parameters to the String method by default: 

console.log(RegExp.prototype[Symbol.match]);
// ƒ [Symbol.match]() { [native code] }

console.log('foobar'.match(/bar/));
// ["bar", index: 3, input: "foobar", groups: undefined]

Providing something other than a regular expression to this method will cause it to be converted to a RegExp object. If you wish to circumvent this behavior and have the method use the parameter directly, it is possible to pass something other than a regular expression instance to the match() method by defining a Symbol.match function to supplant the behavior that would otherwise be exhibited by the regular expression. This function has a single parameter which is the string instance upon which match() is invoked. The return value is unrestricted:

class FooMatcher {
    static [Symbol.match](target) {
        return target.includes('foo');
    }
}

console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false

class StringMatcher {
    constructor(str) {
        this.str = str;
}

    [Symbol.match](target) {
        return target.includes(this.str);
    }
}

console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false

Symbol.replace

Per the ECMAScript specification, this symbol is used as a property for “A regular expression method that replaces matched substrings of a string. Called by the String.prototype.replace() method”. The String.prototype.replace() method will use the function keyed by Symbol.replace to evaluate the expression. The regular expression prototype has this function defined by default, and therefore all regular expression instances are valid parameters to the String method by default: 

// ƒ [Symbol.replace]() { [native code] }

console.log('foobarbaz'.replace(/bar/, 'qux'));
// 'fooquxbaz'

Providing something other than a regular expression to this method will cause it to be converted to a RegExp object. If you wish to circumvent this behavior and have the method use the parameter directly, it is possible to pass something other than a regular expression instance to the replace() method by defining a Symbol.replace function to supplant the behavior that would otherwise be exhibited by the regular expression. This function has two parameters, the string instance upon which replace() is invoked and the replacement string. The return value is unrestricted:

class FooReplacer {
    static [Symbol.replace](target, replacement) {
        return target.split('foo').join(replacement);
    }
}
console.log('barfoobaz'.replace(FooReplacer, 'qux'));
// "barquxbaz"

class StringReplacer {
    constructor(str) {
        this.str = str;
}

    [Symbol.replace](target, replacement) {
        return target.split(this.str).join(replacement);
    }
}

console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux'));
// "barquxbaz"

Symbol.search

 Per the ECMAScript specification, this symbol is used as a property for “A regular expression method that returns the index within a string that matches the regular expression. Called by the String.prototype.search() method”. The String.prototype.search() method will use the function keyed by Symbol.search to evaluate the expression. The regular expression prototype has this function defined by default, and therefore all regular expression instances are valid parameters to the String method by default:

console.log(RegExp.prototype[Symbol.search]);
// ƒ [Symbol.search]() { [native code] }

console.log('foobar'.search(/bar/));
// 3

Providing something other than a regular expression to this method will cause it to be converted to a RegExp object. If you wish to circumvent this behavior and have the method use the parameter directly, it is possible to pass something other than a regular expression instance to the search() method by defining a Symbol.search function to supplant the behavior that would otherwise be exhibited by the regular expression. This function has a single parameter which is the string instance upon which search() is invoked. The return value is unrestricted:

class FooSearcher {
    static [Symbol.search](target) {
        return target.indexOf('foo');
    }
}

console.log('foobar'.search(FooSearcher)); // 0
console.log('barfoo'.search(FooSearcher)); // 3
console.log('barbaz'.search(FooSearcher)); // -1

class StringSearcher {
    constructor(str) {
        this.str = str;
}

    [Symbol.search](target) {
        return target.indexOf(this.str);
    }
}

console.log('foobar'.search(new StringSearcher('foo'))); // 0
console.log('barfoo'.search(new StringSearcher('foo'))); // 3
console.log('barbaz'.search(new StringSearcher('qux'))); // -1

Symbol.species

Per the ECMAScript specification, this symbol is used as a property for “A function valued property that is the constructor function that is used to create derived objects”. This is most commonly used for build-in types which expose methods that instantiate derived objects for the return value of an instance method. Defining a static getter method with Symbol.species allows you to override the prototype definition for the newly created instance: 

class Bar extends Array {}
class Baz extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}

let bar = new Bar();
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
bar = bar.concat('bar');
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true

let baz = new Baz();
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // true
baz = baz.concat('baz');
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // false

Symbol.split

Per the ECMAScript specification, this symbol is used as a property for “A regular expression method that splits a string at the indices that match the regular expression. Called by the String.prototype.split() method”. The String.prototype.split() method will use the function keyed by Symbol.split to evaluate the expression. The regular expression prototype has this function
defined by default, and therefore all regular expression instances are valid parameters to the String method by default: 

console.log(RegExp.prototype[Symbol.split]);
// ƒ [Symbol.split]() { [native code] }

console.log('foobarbaz'.split(/bar/));
// ['foo', 'baz']

 Providing something other than a regular expression to this method will cause it to be converted to a RegExp object. If you wish to circumvent this behavior and have the method use the parameter directly, it is possible to pass something other than a regular expression instance to the split() method by defining a Symbol.split function to supplant the behavior that would otherwise be exhibited by the regular expression. This function has a single parameter which is the string instance upon which split() is invoked. The return value is unrestricted:

class FooSplitter {
    static [Symbol.split](target) {
    return target.split('foo');
    }
}

console.log('barfoobaz'.split(FooSplitter));
// ["bar", "baz"]

class StringSplitter {
    constructor(str) {
        this.str = str;
    }

    [Symbol.split](target) {
        return target.split(this.str);
    }
}

console.log('barfoobaz'.split(new StringSplitter('foo')));
// ["bar", "baz"]

Symbol.toPrimitive

Per the ECMAScript specification, this symbol is used as a property for “A method that converts an object to a corresponding primitive value. Called by the ToPrimitive abstract operation”. There are a number of built-in operations which will attempt to coerce an object into a primitive value: a string, a number, or an unspecified primitive type. For a custom object instance, it is possible to divert this behavior by defining a function on the instance’s Symbol.toPrimitive property. Based on a string parameter provided to the function (either string, number, or default), you are able to control the returned primitive: 

class Foo {}
let foo = new Foo();

console.log(3 + foo); // "3[object Object]"
console.log(3 - foo); // NaN
console.log(String(foo)); // "[object Object]"

class Bar {
    constructor() {
        this[Symbol.toPrimitive] = function(hint) {
            switch (hint) {
                case 'number':
                    return 3;
                case 'string':
                    return 'string bar';
                case 'default':
                default:
                    return 'default bar';
            }
        }
    }
}

let bar = new Bar();

console.log(3 + bar); // "3default bar"
console.log(3 - bar); // 0
console.log(String(bar)); // "string bar"

Symbol.toStringTag

Per the ECMAScript specification, this symbol is used as a property for “A String valued property that is used in the creation of the default string description of an object. Accessed by the built-in method Object.prototype.toString()”. 

Object identification via the toString() method will retrieve the instance identifier specified by Symbol.toStringTag, defaulting to Object. Built-in types have this value already specified, but custom class instances require explicit definition:

let s = new Set();

console.log(s); // Set(0) {}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set

class Foo {}
let foo = new Foo();

console.log(foo); // Foo {}
console.log(foo.toString()); // [object Object]
console.log(foo[Symbol.toStringTag]); // undefined

class Bar {
    constructor() {
        this[Symbol.toStringTag] = 'Bar';
    }
}
let bar = new Bar();

console.log(bar); // Bar {}
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar

Symbol.unscopables

Per the ECMAScript specification, this symbol is used as a property for “An object valued property whose own and inherited property names are property names that are excluded from the with environment bindings of the associated object”. Setting this symbol so it keys an object mapping a corresponding property to true will prevent a with environment binding, as shown here: 

let o = { foo: 'bar' };

with (o) {
    console.log(foo); // bar
}

o[Symbol.unscopables] = {
    foo: true
};

with (o) {
    console.log(foo); // ReferenceError
}

NOTE It’s not recommended to use with, so using Symbol.unscopables is also not recommended.

The Object Type 

Objects in ECMAScript start out as nonspecific groups of data and functionality. Objects are created by using the new operator followed by the name of the object type to create. Developers create their own objects by creating instances of the Object type and adding properties and/or methods to it, as shown here: 

let o = new Object();

This syntax is similar to Java, although ECMAScript requires parentheses to be used only when providing arguments to the constructor. If there are no arguments, as in the following example, then the parentheses can be omitted safely (though that’s not recommended):

let o = new Object; // legal, but not recommended

Instances of Object aren’t very useful on their own, but the concepts are important to understand because, similar to java.lang.Object in Java, the Object type in ECMAScript is the base from which all other objects are derived. All of the properties and methods of the Object type are also present on other, more specific objects.

Each Object instance has the following properties and methods:

➤➤ constructor—The function that was used to create the object. In the previous example, the constructor is the Object() function.


➤➤ hasOwnProperty(propertyName)—Indicates if the given property exists on the object instance (not on the prototype). The property name must be specified as a string (for example, o.hasOwnProperty("name")).


➤➤ isPrototypeof(object)—Determines if the object is a prototype of another object. (Prototypes are discussed in Chapter 5.)


➤➤ propertyIsEnumerable(propertyName)—Indicates if the given property can be enumerated using the for-in statement (discussed later in this chapter). As with hasOwnProperty(), the property name must be a string.

➤➤ toLocaleString()—Returns a string representation of the object that is appropriate for the locale of execution environment.


➤➤ toString()—Returns a string representation of the object.


➤➤ valueOf()—Returns a string, number, or Boolean equivalent of the object. It often returns the same value as toString().

Because Object is the base for all objects in ECMAScript, every object has these base properties and methods. Chapters 5 and 6 cover the specifics of how this occurs. 

NOTE Technically speaking, the behavior of objects in ECMA-262 need not necessarily apply to other objects in JavaScript. Objects that exist in the browser environment, such as those in the Browser Object Model (BOM) and Document Object Model (DOM), are considered host objects because they are provided and defined by the host implementation. Host objects aren’t governed by ECMA-262 and, as such, may or may not directly inherit from Object.

OPERATORS 

ECMA-262 describes a set of operators that can be used to manipulate data values. The operators range from mathematical operations (such as addition and subtraction) and bitwise operators to relational operators and equality operators. Operators are unique in ECMAScript in that they can be used on a wide range of values, including strings, numbers, Booleans, and even objects. When used on objects, operators typically call the valueOf() and/or toString() method to retrieve a value they can work with. 

Unary Operators

Operators that work on only one value are called unary operators. They are the simplest operators in ECMAScript. 

Increment/Decrement 

 The increment and decrement operators are taken directly from C and come in two versions: prefix and postfix. The prefix versions of the operators are placed before the variable they work on; the postfix ones are placed after the variable. To use a prefix increment, which adds 1 to a numeric value, you place two plus signs (++) in front of a variable like this:

let age = 29;
++age;

In this example, the prefix increment changes the value of age to 30 (adding 1 to its previous value of 29). This is effectively equal to the following:

let age = 29;
age = age + 1;

The prefix decrement acts in a similar manner, subtracting 1 from a numeric value. To use a prefix decrement, place two minus signs (--) before a variable, as shown here:

let age = 29;
--age;

Here the age variable is decremented to 28 (subtracting 1 from 29).

When using either a prefix increment or a prefix decrement, the variable’s value is changed before the statement is evaluated. (In computer science, this is usually referred to as having a side effect.) Consider the following:

let age = 29;
let anotherAge = --age + 2;

console.log(age); // 28
console.log(anotherAge); // 30

In this example, the variable anotherAge is initialized with the decremented value of age plus 2. Because the decrement happens first, age is set to 28, and then 2 is added, resulting in 30.

The prefix increment and decrement are equal in terms of order of precedence in a statement and are therefore evaluated left to right. Consider this example:

let num1 = 2;
let num2 = 20;
let num3 = --num1 + num2;
let num4 = num1 + num2;
console.log(num3); // 21
console.log(num4); // 21

Here, num3 is equal to 21 because num1 is decremented to 1 before the addition occurs. The variable num4 also contains 21, because the addition is also done using the changed values.

The postfix versions of increment and decrement use the same syntax (++ and --, respectively) but are placed after the variable instead of before it. Postfix increment and decrement differ from the prefix versions in one important way: the increment or decrement doesn’t occur until after the containing statement has been evaluated. In certain circumstances, this difference doesn’t matter, as in this example:

let age = 29;
age++;

Moving the increment operator after the variable doesn’t change what these statements do, because the increment is the only operation occurring. However, when mixed together with other operations, the difference becomes apparent, as in the following example:

let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 21

With just one simple change in this example, using postfix decrement instead of prefix, you can see the difference. In the prefix example, num3 and num4 both ended up equal to 21, whereas this example ends with num3 equal to 22 and num4 equal to 21. The difference is that the calculation for num3 uses the original value of num1 (2) to complete the addition, whereas num4 is using the decremented value (1).

All four of these operators work on any values, meaning not just integers but strings, Booleans, floating-point values, and objects. The increment and decrement operators follow these rules regarding values:

➤➤ When used on a string that is a valid representation of a number, convert to a number and apply the change. The variable is changed from a string to a number.


➤➤ When used on a string that is not a valid number, the variable’s value is set to NaN. The variable is changed from a string to a number.


➤➤ When used on a Boolean value that is false, convert to 0 and apply the change. The variable is changed from a Boolean to a number.


➤➤ When used on a Boolean value that is true, convert to 1 and apply the change. The variable is changed from a Boolean to a number.


➤➤ When used on a floating-point value, apply the change by adding or subtracting 1.


➤➤ When used on an object, call its valueOf() method (discussed more in Chapter 5) to get a value to work with. Apply the other rules. If the result is NaN, then call toString() apply the other rules again. The variable is changed from an object to a number.

The following example demonstrates some of these rules:

let s1 = "2";
let s2 = "z";
let b = false;
let f = 1.1;
let o = {
    valueOf() {
        return -1;
    }
};

s1++; // value becomes numeric 3
s2++; // value becomes NaN
b++; // value becomes numeric 1
f--; // value becomes 0.10000000000000009 (due to floating-point inaccuracies)
o--; // value becomes numeric -2

Unary Plus and Minus

The unary plus and minus operators are familiar symbols to most developers and operate the same way in ECMAScript as they do in high-school math. The unary plus is represented by a single plus sign (+) placed before a variable and does nothing to a numeric value, as shown in this example: 

let num = 25;
num = +num;
console.log(num); // 25

When the unary plus is applied to a nonnumeric value, it performs the same conversion as the Number() casting function: the Boolean values of false and true are converted to 0 and 1, string values are parsed according to a set of specific rules, and objects have their valueOf() and/or toString() method called to get a value to convert.

The following example demonstrates the behavior of the unary plus when acting on different data types:

let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
    valueOf() {
        return -1;
    }
};

s1 = +s1; // value becomes numeric 1
s2 = +s2; // value becomes numeric 1.1
s3 = +s3; // value becomes NaN
b = +b; // value becomes numeric 0
f = +f; // no change, still 1.1
o = +o; // value becomes numeric -1

The unary minus operator’s primary use is to negate a numeric value, such as converting 1 into –1. The simple case is illustrated here:

let num = 25;
num = -num;
console.log(num); // -25

When used on a numeric value, the unary minus simply negates the value (as in this example). When used on nonnumeric values, unary minus applies all of the same rules as unary plus and then negates the result, as shown here:

let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
    valueOf() {
        return -1;
    }
};
s1 = -s1; // value becomes numeric -1
s2 = -s2; // value becomes numeric -1.1
s3 = -s3; // value becomes NaN
b = -b; // value becomes numeric 0
f = -f; // change to -1.1
o = -o; // value becomes numeric 1

The unary plus and minus operators are used primarily for basic arithmetic but can also be useful for conversion purposes, as illustrated in the previous example.

Bitwise Operators

 

The next set of operators works with numbers at their very base level, with the bits that represent them in memory. All numbers in ECMAScript are stored in IEEE–754 64-bit format, but the bitwise operations do not work directly on the 64-bit representation. Instead, the value is converted into a 32-bit integer, the operation takes place, and the result is converted back into 64 bits. To the developer, it appears that only the 32-bit integer exists because the 64-bit storage format is transparent. With that in mind, consider how 32-bit integers work. 

Signed integers use the first 31 of the 32 bits to represent the numeric value of the integer. The 32nd bit represents the sign of the number: 0 for positive or 1 for negative. Depending on the value of that bit, called the sign bit, the format of the rest of the number is determined. Positive numbers are stored in true binary format, with each of the 31 bits representing a power of 2, starting with the first bit (called bit 0), representing 20, the second bit represents 21, and so on. If any bits are unused, they are filled with 0 and essentially ignored. For example, the number 18 is represented as 00000000000000000000000000010010, or more succinctly as 10010. These are the five most significant bits and can be used, by themselves, to determine the actual value (see Figure 3-1).

Negative numbers are also stored in binary code but in a format called two’s complement. The two’s complement of a number is calculated in three steps:

1. Determine the binary representation of the absolute value (for example, to find –18, first determine the binary representation of 18).


2. Find the one’s complement of the number, which essentially means that every 0 must be replaced with a 1, and vice versa.


3. Add 1 to the result.

Using this process to determine the binary representation –18, start with the binary representation of 18, which is the following:

0000 0000 0000 0000 0000 0000 0001 0010

Next, take the one’s complement, which is the inverse of this number:

1111 1111 1111 1111 1111 1111 1110 1101

Finally, add 1 to the one’s complement as follows:

So the binary equivalent of –18 is 11111111111111111111111111101110. Keep in mind that you have no access to bit 31 when dealing with signed integers.

ECMAScript does its best to keep all of this information from you. When outputting a negative number as a binary string, you get the binary code of the absolute value preceded by a minus sign, as in this example:

let num = -18;
console.log(num.toString(2)); // "-10010"

When you convert the number –18 to a binary string, the result is –10010. The conversion process interprets the two’s complement and represents it in an arguably more logical form.

NOTE By default, all integers are represented as signed in ECMAScript. There is, however, such a thing as an unsigned integer. In an unsigned integer, the 32nd bit doesn’t represent the sign because there are only positive numbers. Unsigned integers also can be larger, because the extra bit becomes part of the number instead of an indicator of the sign.

When you apply bitwise operators to numbers in ECMAScript, a conversion takes place behind the scenes: the 64-bit number is converted into a 32-bit number, the operation is performed, and then the 32-bit result is stored back into a 64-bit number. This gives the illusion that you’re dealing with true 32-bit numbers, which makes the binary operations work in a way similar to the operations of other languages. A curious side effect of this conversion is that the special values NaN and Infinity both are treated as equivalent to 0 when used in bitwise operations. 

If a bitwise operator is applied to a nonnumeric value, the value is first converted into a number using the Number() function (this is done automatically) and then the bitwise operation is applied. The resulting value is a number.

Bitwise NOT

The bitwise NOT is represented by a tilde (~) and simply returns the one’s complement of the number. Bitwise NOT is one of just a few ECMAScript operators related to binary mathematics. Consider this example: 

let num1 = 25; // binary 00000000000000000000000000011001
let num2 = ~num1; // binary 11111111111111111111111111100110
console.log(num2); // -26

Here, the bitwise NOT operator is used on 25, producing –26 as the result. This is the end effect of the bitwise NOT: it negates the number and subtracts 1. The same outcome is produced with the following code:

let num1 = 25;
let num2 = -num1 - 1;
console.log(num2); // "-26"

Realistically, though this returns the same result, the bitwise operation is much faster because it works at the very lowest level of numeric representation.

Bitwise AND

The bitwise AND operator is indicated by the ampersand character (&) and works on two values. Essentially, bitwise AND lines up the bits in each number and then, using the rules in the following truth table, performs an AND operation between the two bits in the same position.

BIT FROM FIRST NUMBERBIT FROM SECOND NUMBERRESULT
11
0 0
010
000

A bitwise AND operation returns 1 if both bits are 1. It returns 0 if any bits are 0.
As an example, to AND the numbers 25 and 3 together, use the following code:

let result = 25 & 3;
console.log(result); // 1

The result of a bitwise AND between 25 and 3 is 1. Why is that? Take a look:

25 =    0000 0000 0000 0000 0000 0000 0001 1001
3 =      0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
AND = 0000 0000 0000 0000 0000 0000 0000 0001

As you can see, only one bit (bit 0) contains a 1 in both 25 and 3. Because of this, every other bit of the resulting number is set to 0, making the result equal to 1.

Bitwise OR

 The bitwise OR operator is represented by a single pipe character (|) and also works on two numbers. Bitwise OR follows the rules in this truth table:

BIT FROM FIRST NUMBERBIT FROM SECOND NUMBERRESULT
111
101
011
000

 

 

 

 

 

 

A bitwise OR operation returns 1 if at least one bit is 1. It returns 0 only if both bits are 0.


Using the same example as for bitwise AND, if you want to OR the numbers 25 and 3 together, the code looks like this:

let result = 25 | 3;
console.log(result); // 27

The result of a bitwise OR between 25 and 3 is 27:

25 =   0000 0000 0000 0000 0000 0000 0001 1001
3 =     0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
OR = 0000 0000 0000 0000 0000 0000 0001 1011

In each number, four bits are set to 1, so these are passed through to the result. The binary code 11011 is equal to 27. 

Bitwise XOR 

 The bitwise XOR operator is represented by a caret (^) and also works on two values. Here is the truth table for bitwise XOR:

BIT FROM FIRST NUMBERBIT FROM SECOND NUMBERRESULT
110
101
011
000

 

 

 

 

 

 

Bitwise XOR is different from bitwise OR in that it returns 1 only when exactly one bit has a value of 1 (if both bits contain 1, it returns 0).

To XOR the numbers 25 and 3 together, use the following code:

let result = 25 ^ 3;
console.log(result); // 26

The result of a bitwise XOR between 25 and 3 is 26, as shown here:

25 =     0000 0000 0000 0000 0000 0000 0001 1001
3 =       0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
XOR = 0000 0000 0000 0000 0000 0000 0001 1010

Four bits in each number are set to 1; however, the first bit in both numbers is 1, so that becomes 0 in the result. All of the other 1s have no corresponding 1 in the other number, so they are passed directly through to the result. The binary code 11010 is equal to 26. (Note that this is one less than when performing bitwise OR on these numbers.)

Left Shift

 The left shift is represented by two less-than signs (<<) and shifts all bits in a number to the left by the number of positions given. For example, if the number 2 (which is equal to 10 in binary) is shifted 5 bits to the left, the result is 64 (which is equal to 1000000 in binary), as shown here:

let oldValue = 2; // equal to binary 10
let newValue = oldValue << 5; // equal to binary 1000000 which is decimal 64

Note that when the bits are shifted, five empty bits remain to the right of the number. The left shift fills these bits with 0s to make the result a complete 32-bit number (see Figure 3-2).

Note that left shift preserves the sign of the number it’s operating on. For instance, if –2 is shifted to the left by five spaces, it becomes –64, not positive 64.

Signed Right Shift

 The signed right shift is represented by two greater-than signs (>>) and shifts all bits in a 32-bit number to the right while preserving the sign (positive or negative). A signed right shift is the exact opposite of a left shift. For example, if 64 is shifted to the right five bits, it becomes 2:

let oldValue = 64; // equal to binary 1000000
let newValue = oldValue >> 5; // equal to binary 10 which is decimal 2

Once again, when bits are shifted, the shift creates empty bits. This time, the empty bits occur at the left of the number but after the sign bit (see Figure 3-3). Once again, ECMAScript fills these empty bits with the value in the sign bit to create a complete number.

Unsigned Right Shift

 The unsigned right shift is represented by three greater-than signs (>>>) and shifts all bits in a 32-bit number to the right. For numbers that are positive, the effect is the same as a signed right shift. Using the same example as for the signed-right-shift example, if 64 is shifted to the right five bits, it becomes 2:

let oldValue = 64; // equal to binary 1000000
let newValue = oldValue >>> 5; // equal to binary 10 which is decimal 2

For numbers that are negative, however, something quite different happens. Unlike signed right shift, the empty bits get filled with zeros regardless of the sign of the number. For positive numbers, it has the same effect as a signed right shift; for negative numbers, the result is quite different. The unsigned-right-shift operator considers the binary representation of the negative number to be representative of a positive number instead. Because the negative number is the two’s complement of its absolute value, the number becomes very large, as you can see in the following example:

let oldValue = -64; // equal to binary 11111111111111111111111111000000
let newValue = oldValue >>> 5; // equal to decimal 134217726

When an unsigned right shift is used to shift –64 to the right by five bits, the result is 134217726. This happens because the binary representation of –64 is 11111111111111111111111111000000, but because the unsigned right shift treats this as a positive number, it considers the value to be 4294967232. When this value is shifted to the right by five bits, it becomes 00000111111111111111111111111110, which is 134217726.

Boolean Operators

Almost as important as equality operators, Boolean operators are what make a programming language function. Without the capability to test relationships between two values, statements such as if...else and loops wouldn’t be useful. There are three Boolean operators: NOT, AND, and OR. 

Logical NOT

The logical NOT operator is represented by an exclamation point (!) and may be applied to any value in ECMAScript. This operator always returns a Boolean value, regardless of the data type it’s used on. The logical NOT operator first converts the operand to a Boolean value and then negates it, meaning that the logical NOT behaves in the following ways: 

➤➤ If the operand is an object, false is returned.


➤➤ If the operand is an empty string, true is returned.


➤➤ If the operand is a nonempty string, false is returned.


➤➤ If the operand is the number 0, true is returned.

➤➤ If the operand is any number other than 0 (including Infinity), false is returned.


➤➤ If the operand is null, true is returned.


➤➤ If the operand is NaN, true is returned.


➤➤ If the operand is undefined, true is returned.

The following example illustrates this behavior:

console.log(!false); // true
console.log(!"blue"); // false
console.log(!0); // true
console.log(!NaN); // true
console.log(!""); // true
console.log(!12345); // false

The logical NOT operator can also be used to convert a value into its Boolean equivalent. By using two NOT operators in a row, you can effectively simulate the behavior of the Boolean() casting function. The first NOT returns a Boolean value no matter what operand it is given. The second NOT negates that Boolean value and so gives the true Boolean value of a variable. The end result is the same as using the Boolean() function on a value, as shown here:

console.log(!!"blue"); // true
console.log(!!0); // false
console.log(!!NaN); // false
console.log(!!""); // false
console.log(!!12345); // true

Logical AND

 The logical AND operator is represented by the double ampersand (&&) and is applied to two values, as in this example:

let result = true && false;

Logical AND behaves as described in the following truth table:

OPERAND 1OPERAND 2RESULT
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

Logical AND can be used with any type of operand, not just Boolean values. When either operand is not a primitive Boolean, logical AND does not always return a Boolean value; instead, it does one of the following:

➤➤ If the first operand is an object, then the second operand is always returned.


➤➤ If the second operand is an object, then the object is returned only if the first operand evaluates to true.

➤➤ If both operands are objects, then the second operand is returned.


➤➤ If either operand is null, then null is returned.


➤➤ If either operand is NaN, then NaN is returned.


➤➤ If either operand is undefined, then undefined is returned.

The logical AND operator is a short-circuited operation, meaning that if the first operand determines the result, the second operand is never evaluated. In the case of logical AND, if the first operand is false, no matter what the value of the second operand, the result can’t be equal to true. Consider the following example:

let found = true;
let result = (found && someUndeclaredVariable); // error occurs here
console.log(result); // this line never executes

This code causes an error when the logical AND is evaluated, because the variable someUndeclaredVariable isn’t declared. The value of the variable found is true, so the logical AND operator continued to evaluate the variable someUndeclaredVariable. When it did, an error occurred because someUndeclaredVariable is not declared and therefore cannot be used in a logical AND operation. If found is instead set to false, as in the following example, the error won’t occur:

let found = false;
let result = (found && someUndeclaredVariable); // no error
console.log(result); // works

In this code, the console.log is displayed successfully. Even though the variable someUndeclaredVariable is undefined, it is never evaluated because the first operand is false. This means that the result of the operation must be false, so there is no reason to evaluate what’s to the right of the &&. Always keep in mind short-circuiting when using logical AND.

Logical OR

The logical OR operator is represented by the double pipe (||) in ECMAScript, like this: 

let result = true || false;

Logical OR behaves as described in the following truth table:

OPERAND 1OPERAND 2OPERAND 3
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

 

Just like logical AND, if either operand is not a Boolean, logical OR will not always return a Boolean value; instead, it does one of the following:

➤➤ If the first operand is an object, then the first operand is returned.


➤➤ If the first operand evaluates to false, then the second operand is returned.

➤➤ If both operands are objects, then the first operand is returned.


➤➤ If both operands are null, then null is returned.


➤➤ If both operands are NaN, then NaN is returned.


➤➤ If both operands are undefined, then undefined is returned.


Also like the logical AND operator, the logical OR operator is short-circuited. In this case, if the first operand evaluates to true, the second operand is not evaluated. Consider this example:

let found = true;
let result = (found || someUndeclaredVariable); // no error
console.log(result); // works

As with the previous example, the variable someUndefinedVariable is undefined. However, because the variable found is set to true, the variable someUndefinedVariable is never evaluated and thus the output is "true". If the value of found is changed to false, an error occurs, as in the following example:

let found = false;
let result = (found || someUndeclaredVariable); // error occurs here
console.log(result); // this line never executes

You can also use this behavior to avoid assigning a null or undefined value to a variable. Consider the following:

let myObject = preferredObject || backupObject;

In this example, the variable myObject will be assigned one of two values. The preferredObject variable contains the value that is preferred if it’s available, whereas the backupObject variable contains the backup value if the preferred one isn’t available. If preferredObject isn’t null, then it’s assigned to myObject; if it is null, then backupObject is assigned to myObject. This pattern is used very frequently in ECMAScript for variable assignment and is used throughout this book.

Multiplicative Operators

There are three multiplicative operators in ECMAScript: multiply, divide, and modulus. These operators work in a manner similar to their counterparts in languages such as Java, C, and Perl, but they also include some automatic type conversions when dealing with nonnumeric values. If either of the operands for a multiplication operation isn’t a number, it is converted to a number behind the scenes using the Number() casting function. This means that an empty string is treated as 0, and the Boolean value of true is treated as 1. 

Multiply

The multiply operator is represented by an asterisk (*) and is used, as one might suspect, to multiply two numbers. The syntax is the same as in C, as shown here: 

let result = 34 * 56;

However, the multiply operator also has the following unique behaviors when dealing with special values:

➤➤ If the operands are numbers, regular arithmetic multiplication is performed, meaning that two positives or two negatives equal a positive, whereas operands with different signs yield a negative. If the result cannot be represented by ECMAScript, either Infinity or –Infinity is returned.


➤➤ If either operand is NaN, the result is NaN.


➤➤ If Infinity is multiplied by 0, the result is NaN.


➤➤ If Infinity is multiplied by any finite number other than 0, the result is either Infinity or –Infinity, depending on the sign of the second operand.


➤➤ If Infinity is multiplied by Infinity, the result is Infinity.


➤➤ If either operand isn’t a number, it is converted to a number behind the scenes using Number() and then the other rules are applied.

Divide

The divide operator is represented by a slash (/) and divides the first operand by the second operand, as shown here:

 

let result = 66 / 11;

The divide operator, like the multiply operator, has special behaviors for special values. They are as follows:

➤➤ If the operands are numbers, regular arithmetic division is performed, meaning that two positives or two negatives equal a positive, whereas operands with different signs yield a negative. If the result can’t be represented in ECMAScript, it returns either Infinity or –Infinity.


➤➤ If either operand is NaN, the result is NaN.


➤➤ If Infinity is divided by Infinity, the result is NaN.


➤➤ If zero is divided by zero, the result is NaN.


➤➤ If a nonzero finite number is divided by zero, the result is either Infinity or –Infinity, depending on the sign of the first operand.


➤➤ If Infinity is divided by any number, the result is either Infinity or –Infinity, depending on the sign of the second operand.


➤➤ If either operand isn’t a number, it is converted to a number behind the scenes using Number() and then the other rules are applied.

Modulus

The modulus (remainder) operator is represented by a percent sign (%) and is used in the following way: 

let result = 26 % 5; // equal to 1

Just like the other multiplicative operators, the modulus operator behaves differently for special values, as follows:

➤➤ If the operands are numbers, regular arithmetic division is performed, and the remainder of that division is returned.


➤➤ If the dividend is an infinite number and the divisor is a finite number, the result is NaN.


➤➤ If the dividend is a finite number and the divisor is 0, the result is NaN.


➤➤ If Infinity is divided by Infinity, the result is NaN.


➤➤ If the dividend is a finite number and the divisor is an infinite number, then the result is the dividend.


➤➤ If the dividend is zero and the divisor is nonzero, the result is zero.


➤➤ If either operand isn’t a number, it is converted to a number behind the scenes using Number() and then the other rules are applied.

Exponentiation Operator

 New in ECMAScript 7, Math.pow() now gets its own ** operator, which behaves identically.

console.log(Math.pow(3, 2); // 9
console.log(3 ** 2); // 9

console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5); // 4

What’s more, the operator also gets its own exponentiate assignment operator, **=, which performs the exponentiation and subsequent assignment of the result:

let squared = 3;
squared **= 2;
console.log(squared); // 9

let sqrt = 16;
sqrt **= 0.5;
console.log(sqrt); // 4

Additive Operators

The additive operators, add and subtract, are typically the simplest mathematical operators in programming languages. In ECMAScript, however, a number of special behaviors are associated with each operator. As with the multiplicative operators, conversions occur behind the scenes for different data types. For these operators, however, the rules aren’t as straightforward. 

Add

The add operator (+) is used just as one would expect, as shown in the following example: 

let result = 1 + 2;

 If the two operands are numbers, they perform an arithmetic add and return the result according to the following rules:

➤➤ If either operand is NaN, the result is NaN.


➤➤ If Infinity is added to Infinity, the result is Infinity.


➤➤ If –Infinity is added to –Infinity, the result is –Infinity.


➤➤ If Infinity is added to –Infinity, the result is NaN.


➤➤ If +0 is added to +0, the result is +0.


➤➤ If –0 is added to +0, the result is +0.


➤➤ If –0 is added to –0, the result is –0.

If, however, one of the operands is a string, then the following rules apply:

➤➤ If both operands are strings, the second string is concatenated to the first.


➤➤ If only one operand is a string, the other operand is converted to a string and the result is the concatenation of the two strings.

If either operand is an object, number, or Boolean, its toString() method is called to get a string value and then the previous rules regarding strings are applied. For undefined and null, the String() function is called to retrieve the values "undefined" and "null", respectively.

Consider the following:

let result1 = 5 + 5; // two numbers
console.log(result1); // 10
let result2 = 5 + "5"; // a number and a string
console.log(result2); // "55"

This code illustrates the difference between the two modes for the add operator. Normally, 5 + 5 equals 10 (a number value), as illustrated by the first two lines of code. However, if one of the operands is changed to a string, "5", the result becomes "55" (which is a primitive string value) because the first operand gets converted to "5" as well.

One of the most common mistakes in ECMAScript is being unaware of the data types involved with an addition operation. Consider the following:

let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + num1 + num2;
console.log(message); // "The sum of 5 and 10 is 510"

In this example, the message variable is filled with a string that is the result of two addition operations. One might expect the final string to be "The sum of 5 and 10 is 15"; however, it actually ends up as "The sum of 5 and 10 is 510". This happens because each addition is done separately. The first combines a string with a number (5), which results in a string. The second takes that result (a string) and adds a number (10), which also results in a string. To perform the arithmetic calculation and then append that to the string, just add some parentheses like this:

let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + (num1 + num2);
console.log(message); // "The sum of 5 and 10 is 15"

Here, the two number variables are surrounded by parentheses, which instruct the interpreter to calculate its result before adding it to the string. The resulting string is "The sum of 5 and 10 is 15".

Subtract

The subtract operator (-) is another that is used quite frequently. Here’s an example: 

let result = 2 - 1;

Just like the add operator, the subtract operator has special rules to deal with the variety of type conversions present in ECMAScript. They are as follows:

➤➤ If the two operands are numbers, perform arithmetic subtract and return the result.


➤➤ If either operand is NaN, the result is NaN.


➤➤ If Infinity is subtracted from Infinity, the result is NaN.


➤➤ If –Infinity is subtracted from –Infinity, the result is NaN.


➤➤ If –Infinity is subtracted from Infinity, the result is Infinity.


➤➤ If Infinity is subtracted from –Infinity, the result is –Infinity.


➤➤ If +0 is subtracted from +0, the result is +0.


➤➤ If –0 is subtracted from +0, the result is –0.


➤➤ If –0 is subtracted from –0, the result is +0.


➤➤ If either operand is a string, a Boolean, null, or undefined, it is converted to a number (using Number() behind the scenes) and the arithmetic is calculated using the previous rules. If that conversion results in NaN, then the result of the subtraction is NaN.


➤➤ If either operand is an object, its valueOf() method is called to retrieve a numeric value to represent it. If that value is NaN, then the result of the subtraction is NaN. If the object doesn’t have valueOf() defined, then toString() is called and the resulting string is converted into a number.

The following are some examples of these behaviors:

let result1 = 5 - true; // 4 because true is converted to 1
let result2 = NaN - 1; // NaN
let result3 = 5 - 3; // 2
let result4 = 5 - ""; // 5 because "" is converted to 0
let result5 = 5 - "2"; // 3 because "2" is converted to 2
let result6 = 5 - null; // 5 because null is converted to 0

Relational Operators

The less-than (<), greater-than (>), less-than-or-equal-to (<=), and greater-than-or-equal-to (>=) relational operators perform comparisons between values in the same way that you learned in math class. Each of these operators returns a Boolean value, as in this example: 

let result1 = 5 > 3; // true
let result2 = 5 < 3; // false

As with other operators in ECMAScript, there are some conversions and other oddities that happen when using different data types. They are as follows:

➤➤ If the operands are numbers, perform a numeric comparison.


➤➤ If the operands are strings, compare the character codes of each corresponding character in the string.


➤➤ If one operand is a number, convert the other operand to a number and perform a numeric comparison.


➤➤ If an operand is an object, call valueOf() and use its result to perform the comparison according to the previous rules. If valueOf() is not available, call toString() and use that value according to the previous rules.


➤➤ If an operand is a Boolean, convert it to a number and perform the comparison.

When a relational operator is used on two strings, an interesting behavior occurs. Many expect that less-than means “alphabetically before” and greater-than means “alphabetically after,” but this is not the case. For strings, each of the first string’s character codes is numerically compared against the character codes in a corresponding location in the second string. After this comparison is complete, a Boolean value is returned. The problem here is that the character codes of uppercase letters are all lower than the character codes of lowercase letters, meaning that you can run into situations like this:

let result = "Brick" < "alphabet"; // true

In this example, the string "Brick" is considered to be less than the string "alphabet" because the letter B has a character code of 66 and the letter a has a character code of 97. To force a true alphabetic result, you must convert both operands into a common case (upper or lower) and then compare like this:

let result = "Brick".toLowerCase() < "alphabet".toLowerCase(); // false

Converting both operands to lowercase ensures that "alphabet" is correctly identified as alphabetically before "Brick".

Another sticky situation occurs when comparing numbers that are strings, such as in this example:

let result = "23" < "3"; // true

This code returns true when comparing the string "23" to "3". Because both operands are strings, they are compared by their character codes (the character code for "2" is 50; the character code for "3" is 51). If, however, one of the operands is changed to a number as in the following example, the result makes more sense:

let result = "23" < 3; // false

Here, the string "23" is converted into the number 23 and then compared to 3, giving the expected result. Whenever a number is compared to a string, the string is converted into a number and then numerically compared to the other number. This works well for cases like the previous example, but what if the string can’t be converted into a number? Consider this example:

let result = "a" < 3; // false because "a" becomes NaN

The letter "a" can’t be meaningfully converted into a number, so it becomes NaN. As a rule, the result of any relational operation with NaN is false, which is interesting when considering the following:

let result1 = NaN < 3; // false
let result2 = NaN >= 3; // false

In most comparisons, if a value is not less than another, it is always greater than or equal to it. When using NaN, however, both comparisons return false.

Equality Operators

 Determining whether two variables are equivalent is one of the most important operations in programming. This is fairly straightforward when dealing with strings, numbers, and Boolean values, but the task gets a little complicated when you take objects into account. Originally ECMAScript’s equal and not-equal operators performed conversions into like types before doing a comparison. The question of whether these conversions should, in fact, take place was then raised. The end result was for ECMAScript to provide two sets of operators: equal and not equal to perform conversion before comparison, and identically equal and not identically equal to perform comparison without conversion.

Equal and Not Equal

The equal operator in ECMAScript is the double equal sign (==), and it returns true if the operands are equal. The not-equal operator is the exclamation point followed by an equal sign (!=), and it returns true if two operands are not equal. Both operators do conversions to determine if two operands are equal (often called type coercion). 

When performing conversions, the equal and not-equal operators follow these basic rules:

➤➤ If an operand is a Boolean value, convert it into a numeric value before checking for equality. A value of false converts to 0, whereas a value of true converts to 1.


➤➤ If one operand is a string and the other is a number, attempt to convert the string into a number before checking for equality.


➤➤ If one of the operands is an object and the other is not, the valueOf() method is called on the object to retrieve a primitive value to compare according to the previous rules.

The operators also follow these rules when making comparisons:

➤➤ Values of null and undefined are equal.


➤➤ Values of null and undefined cannot be converted into any other values for equality checking.


➤➤ If either operand is NaN, the equal operator returns false and the not-equal operator returns true. Important note: even if both operands are NaN, the equal operator returns false because, by rule, NaN is not equal to NaN.


➤➤ If both operands are objects, then they are compared to see if they are the same object. If both operands point to the same object, then the equal operator returns true. Otherwise, the two are not equal.

The following table lists some special cases and their results:

EXPRESSIONVALUE
null == undefinedtrue
"NaN" == NaNfalse
5 == NaNfalse
NaN == NaNfalse
NaN != NaNtrue
false == 0true
true == 1true
true == 2false
undefined == 0false
null == 0false
"5" == 5true

Identically Equal and Not Identically Equal

The identically equal and not identically equal operators do the same thing as equal and not equal, except that they do not convert operands before testing for equality. The identically equal operator is represented by three equal signs (===) and returns true only if the operands are equal without conversion, as in this example: 

let result1 = ("55" == 55); // true - equal because of conversion
let result2 = ("55" === 55); // false - not equal because different data types

In this code, the first comparison uses the equal operator to compare the string "55" and the number 55, which returns true. As mentioned previously, this happens because the string "55" is converted to the number 55 and then compared with the other number 55. The second comparison uses the identically equal operator to compare the string and the number without conversion, and of course, a string isn’t equal to a number, so this outputs false.

The not identically equal operator is represented by an exclamation point followed by two equal signs (!==) and returns true only if the operands are not equal without conversion. For example:

let result1 = ("55" != 55); // false - equal because of conversion
let result2 = ("55" !== 55); // true - not equal because different data types

Here, the first comparison uses the not-equal operator, which converts the string "55" to the number 55, making it equal to the second operand, also the number 55. Therefore, this evaluates to false because the two are considered equal. The second comparison uses the not identically equal operator. It helps to think of this operation as saying, “Is the string 55 different from the number 55?” The answer to this is yes (true).

Keep in mind that while null == undefined is true because they are similar values, null === undefined is false because they are not the same type.

NOTE Because of the type conversion issues with the equal and not-equal operators, it is recommended to use identically equal and not identically equal instead. This helps to maintain data type integrity throughout your code.

 Conditional Operator

The conditional operator is one of the most versatile in ECMAScript, and it takes on the same form as in Java, which is as follows: 

variable = boolean_expression ? true_value : false_value;

This basically allows a conditional assignment to a variable depending on the evaluation of the boolean_expression. If it’s true, then true_value is assigned to the variable; if it’s false, then false_value is assigned to the variable, as in this instance:

let max = (num1 > num2) ? num1 : num2;

In this example, max is to be assigned the number with the highest value. The expression states that if num1 is greater than num2, then num1 is assigned to max. If, however, the expression is false (meaning that num1 is less than or equal to num2), then num2 is assigned to max.

Assignment Operators

Simple assignment is done with the equal sign (=) and simply assigns the value on the right to the variable on the left, as shown in the following example: 

let num = 10;

Compound assignment is done with one of the multiplicative, additive, or bitwise–shift operators followed by an equal sign (=). These assignments are designed as shorthand for such common situations as this:

let num = 10;
num = num + 10;

The second line of code can be replaced with a compound assignment:

let num = 10;
num += 10;

Compound-assignment operators exist for each of the major mathematical operations and a few others as well. They are as follows:

➤➤ Multiply/assign (*=)
➤➤ Divide/assign (/=)
➤➤ Modulus/assign (%=)
➤➤ Add/assign (+=)
➤➤ Subtract/assign (-=)
➤➤ Left shift/assign (<<=)
➤➤ Signed right shift/assign (>>=)
➤➤ Unsigned right shift/assign (>>>=)

These operators are designed specifically as shorthand ways of achieving operations. They do not represent any performance improvement.

Comma Operator

The comma operator allows execution of more than one operation in a single statement, as illustrated here: 

let num1 = 1, num2 = 2, num3 = 3;

 Most often, the comma operator is used in the declaration of variables; however, it can also be used to assign values. When used in this way, the comma operator always returns the last item in the expression, as in the following example:

let num = (5, 1, 4, 8, 0); // num becomes 0

In this example, num is assigned the value of 0 because it is the last item in the expression. There aren’t many times when commas are used in this way; however, it is helpful to understand that this behavior exists.

STATEMENTS

ECMA-262 describes several statements (also called flow-control statements). Essentially, statements define most of the syntax of ECMAScript and typically use one or more keywords to accomplish a given task. Statements can be simple, such as telling a function to exit, or complicated, such as specifying a number of commands to be executed repeatedly. 

The if Statement 

One of the most frequently used statements in most programming languages is the if statement. The if statement has the following syntax: 

if (condition) statement1 else statement2

The condition can be any expression; it doesn’t even have to evaluate to an actual Boolean value. ECMAScript automatically converts the result of the expression into a Boolean by calling the Boolean() casting function on it. If the condition evaluates to true, statement1 is executed; if the condition evaluates to false, statement2 is executed. Each of the statements can be either a single line or a code block (a group of code lines enclosed within braces). Consider this example:

if (i > 25)
    console.log("Greater than 25."); // one–line statement
else {
    console.log("Less than or equal to 25."); // block statement
}

It’s considered best coding practice to always use block statements, even if only one line of code is to be executed. Doing so can avoid confusion about what should be executed for each condition.

You can also chain if statements together like so:

if (condition1) statement1 else if (condition2) statement2 else statement3

Here’s an example:

if (i > 25) {
    console.log("Greater than 25.");
} else if (i < 0) {
    console.log("Less than 0.");
} else {
    console.log("Between 0 and 25, inclusive.");
}

The do-while Statement

The do-while statement is a post-test loop, meaning that the escape condition is evaluated only after the code inside the loop has been executed. The body of the loop is always executed at least once before the expression is evaluated. Here’s the syntax: 

do {
    statement
} while (expression);

And here’s an example of its usage:

let i = 0;
do {
    i += 2;
} while (i < 10);

In this example, the loop continues as long as i is less than 10. The variable starts at 0 and is incremented by two each time through the loop.

NOTE Post-test loops such as this are most often used when the body of the loop should be executed at least once before exiting.

The while Statement 

The while statement is a pretest loop. This means the escape condition is evaluated before the code inside the loop has been executed. Because of this, it is possible that the body of the loop is never executed. Here’s the syntax: 

while(expression) statement

And here’s an example of its usage:

let i = 0;
while (i < 10) {
    i += 2;
}

In this example, the variable i starts out equal to 0 and is incremented by two each time through the loop. As long as the variable is less than 10, the loop will continue.

The for Statement

 The for statement is also a pretest loop with the added capabilities of variable initialization before entering the loop and defining postloop code to be executed. Here’s the syntax:

for (initialization; expression; post-loop-expression) statement

And here’s an example of its usage:

let count = 10;
for (let i = 0; i < count; i++) {
    console.log(i);
}

This code defines a variable i that begins with the value 0. The for loop is entered only if the conditional expression (i < count) evaluates to true, making it possible that the body of the code might not be executed. If the body is executed, the postloop expression is also executed, iterating the variable i. This for loop is the same as the following:

let count = 10;
let i = 0;
while (i < count) {
    console.log(i);
    i++;
}

Nothing can be done with a for loop that can’t be done using a while loop. The for loop simply encapsulates the loop-related code into a single location.

It’s important to note that there’s no need to use the variable declaration keyword inside the for loop initialization. However, the overwhelming majority of the time you will find that the iterator variable is not useful after the loop completes. In these cases, the cleanest implementation is to use a let declaration inside the loop initialization to declare the iterator variable because its scope will be limited to only the loop itself.

The initialization, control expression, and postloop expression are all optional. You can create an infinite loop by omitting all three, like this:

for (;;) { // infinite loop
    doSomething();
}

Including only the control expression effectively turns a for loop into a while loop, as shown here:

let i = 0;
for (; i < count; ) {
    console.log(i);
    i++;
}

This versatility makes the for statement one of the most used in the language.

The for-in Statement

 The for-in statement is a strict iterative statement. It is used to enumerate the non-symbol keyed properties of an object. Here’s the syntax:

for (property in expression) statement

And here’s an example of its usage:

for (const propName in window) {
    document.write(propName);
}

Here, the for-in statement is used to display all the properties of the BOM window object. Each time through the loop, the propName variable is filled with the name of a property that exists on the window object. This continues until all of the available properties have been enumerated. As with the for statement, the const operator in the control statement is not necessary but is recommended for ensuring the use of a local variable that will not be altered.

Object properties in ECMAScript are unordered, so the order in which property names are returned in a for-in statement cannot necessarily be predicted. All enumerable properties will be returned once, but the order may differ across browsers.

Note that the for-in statement simply doesn’t execute the body of the loop if the variable representing the object to iterate over is null or undefined.

The for-of Statement

The for-of statement is a strict iterative statement. It is used to loop through elements in an iterable object. Here’s the syntax: 

for (property of expression) statement

And here’s an example of its usage:

for (const el in [2,4,6,8) {
    document.write(el);
}

Here, the for-of statement is used to display all the elements inside the four-element array. This continues until each element in the array has been looped over. As with the for statement, the const operator in the control statement is not necessary but is recommended for ensuring the use of a local variable that will not be altered.

The for-of loop will iterate in the order that the iterable produces values via its next() method. This is covered in-depth in the Iterators and Generators chapter.

Note that the for-of statement will throw an error if the entity that it is attempting to iterate over does not support iteration.

NOTE In ES2018, the for-of statement is extended as a for-await-of loop to support async iterables which produce promises. This is covered in Appendix A.

Labeled Statements 

It is possible to label statements for later use with the following syntax: 

label: statement

 Here’s an example:

start: for (let i = 0; i < count; i++) {
    console.log(i);
}

 In this example, the label start can be referenced later by using the break or continue statement. Labeled statements are typically used with nested loops.

The break and continue Statements

The break and continue statements provide stricter control over the execution of code in a loop. The break statement exits the loop immediately, forcing execution to continue with the next statement after the loop. The continue statement, on the other hand, exits the loop immediately, but execution continues from the top of the loop. Here’s an example: 

let num = 0;

for (let i = 1; i < 10; i++) {
    if (i % 5 == 0) {
        break;
    }
    num++;
}

console.log(num); // 4

 In this code, the for loop increments the variable i from 1 to 10. In the body of a loop, an if statement checks to see if the value of i is evenly divisible by 5 (using the modulus operator). If so, the break statement is executed and the loop is exited. The num variable starts out at 0 and indicates the number of times the loop has been executed. After the break statement has been hit, the next line of code to be executed is the console.log, which displays 4. The number of times the loop has been executed is four because when i equals 5, the break statement causes the loop to be exited before num can be incremented. A different effect can be seen if break is replaced with continue like this:

let num = 0;

for (let i = 1; i < 10; i++) {
    if (i % 5 == 0) {
        continue;
    }
    num++;
}

console.log(num); // 8

Here, the console.log displays 8, the number of times the loop has been executed. When i reaches a value of 5, the loop is exited before num is incremented, but execution continues with the next iteration, when the value is 6. The loop then continues until its natural completion, when i is 10. The final value of num is 8 instead of 9 because one increment didn’t occur due to the continue statement.

Both the break and continue statements can be used in conjunction with labeled statements to return to a particular location in the code. This is typically used when there are loops inside of loops, as in the following example:

let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            break outermost;
        }
        num++;
    }
}

console.log(num); // 55

In this example, the outermost label indicates the first for statement. Each loop normally executes 10 times, meaning that the num++ statement is normally executed 100 times and, consequently, num should be equal to 100 when the execution is complete. The break statement here is given one argument: the label to break to. Adding the label allows the break statement to break not just out of the inner for statement (using the variable j) but also out of the outer for statement (using the variable i). Because of this, num ends up with a value of 55, because execution is halted when both i and j are equal to 5. The continue statement can be used in the same way, as shown in the following example:

let num = 0;

outermost:
for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            continue outermost;
        }
        num++;
    }
}

console.log(num); // 95

In this case, the continue statement forces execution to continue—not in the inner loop but in the outer loop. When j is equal to 5, continue is executed, which means that the inner loop misses five iterations, leaving num equal to 95.

Using labeled statements in conjunction with break and continue can be very powerful but can cause debugging problems if overused. Always use descriptive labels and try not to nest more than a few loops.

The with Statement

The with statement sets the scope of the code within a particular object. The syntax is as follows: 

with (expression) statement;

The with statement was created as a convenience for times when a single object was being coded to over and over again, as in this example:

let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;

Here, the location object is used on every line. This code can be rewritten using the with statement as follows:

with(location) {
    let qs = search.substring(1);
    let hostName = hostname;
    let url = href;
}

In this rewritten version of the code, the with statement is used in conjunction with the location object. This means that each variable inside the statement is first considered to be a local variable. If it’s not found to be a local variable, the location object is searched to see if it has a property of the same name. If so, then the variable is evaluated as a property of location.

In strict mode, the with statement is not allowed and is considered a syntax error.

WARNING It is widely considered a poor practice to use the with statement in production code because of its negative performance impact and the difficulty in debugging code contained in the with statement.

 The switch Statement

Closely related to the if statement is the switch statement, another flow-control statement adopted from other languages. The syntax for the switch statement in ECMAScript closely resembles the syntax in other C-based languages, as you can see here: 

switch (expression) {
    case value1:
        statement
        break;
    case value2:
        statement
        break;
    case value3:
        statement
        break;
    case value4:
        statement
        break;
    default:
        statement
}

Each case in a switch statement says, “If the expression is equal to the value, execute the statement.” The break keyword causes code execution to jump out of the switch statement. Without the break keyword, code execution falls through the original case into the following one. The default keyword indicates what is to be done if the expression does not evaluate to one of the cases. (In effect, it is an else statement.)

Essentially, the switch statement prevents a developer from having to write something like this:

if (i == 25) {
    console.log("25");
} else if (i == 35) {
    console.log("35");
} else if (i == 45) {
    console.log("45");
} else {
    console.log("Other");
}

The equivalent switch statement is as follows:

switch (i) {
    case 25:
        console.log("25");
        break;
    case 35:
        console.log("35");
        break;
    case 45:
        console.log("45");
        break;
    default:
        console.log("Other");
}

It’s best to always put a break statement after each case to avoid having cases fall through into the next one. If you need a case statement to fall through, include a comment indicating that the omission of the break statement is intentional, such as this:

switch (i) {
    case 25:
        /* falls through */
    case 35:
        console.log("25 or 35");
        break;
    case 45:
        console.log("45");
        break;
    default:
        console.log("Other");
}

Although the switch statement was borrowed from other languages, it has some unique characteristics in ECMAScript. First, the switch statement works with all data types (in many languages it works only with numbers), so it can be used with strings and even with objects. Second, the case values need not be constants; they can be variables and even expressions. Consider the following example:

switch ("hello world") {
    case "hello" + " world":
        console.log("Greeting was found.");
        break;
    case "goodbye":
        console.log("Closing was found.");
        break;
    default:
        console.log("Unexpected message was found.");
}

In this example, a string value is used in a switch statement. The first case is actually an expression that evaluates a string concatenation. Because the result of this concatenation is equal to the switchargument, the console.log displays "Greeting was found." The ability to have case expressions also allows you to do things like this:

let num = 25;
switch (true) {
    case num < 0:
        console.log("Less than 0.");
        break;
    case num >= 0 && num <= 10:
        console.log("Between 0 and 10.");
        break;
    case num > 10 && num <= 20:
        console.log("Between 10 and 20.");
        break;
    default:
        console.log("More than 20.");
}

Here, a variable num is defined outside the switch statement. The expression passed into the switch statement is true because each case is a conditional that will return a Boolean value. Each case is evaluated, in order, until a match is found or until the default statement is encountered (which is the case here).

NOTE The switch statement compares values using the identically equal operator, so no type coercion occurs (for example, the string "10" is not equal to the number 10).

FUNCTIONS 

Functions are the core of any language because they allow the encapsulation of statements that can be run anywhere and at any time. Functions in ECMAScript are declared using the function keyword, followed by a set of arguments and then the body of the function. 

NOTE In-depth function coverage can be found in the Functions chapter.

The basic function syntax is as follows: 

function functionName(arg0, arg1,...,argN) {
    statements
}

Here’s an example:

function sayHi(name, message) {
    console.log("Hello " + name + ", " + message);
}

This function can then be called by using the function name, followed by the function arguments enclosed in parentheses (and separated by commas, if there are multiple arguments). The code to call the sayHi() function looks like this:

sayHi("Nicholas", "how are you today?");

The output of this function call is, "Hello Nicholas, how are you today?" The named arguments name and message are used as part of a string concatenation that is ultimately displayed in an console.log.

Functions in ECMAScript need not specify whether they return a value. Any function can return a value at any time by using the return statement followed by the value to return. Consider this example:

function sum(num1, num2) {
    return num1 + num2;
}

The sum() function adds two values together and returns the result. Note that aside from the return statement, there is no special declaration indicating that the function returns a value. This function can be called using the following:

const result = sum(5, 10);

 Keep in mind that a function stops executing and exits immediately when it encounters the return statement. Therefore, any code that comes after a return statement will never be executed. For example:

function sum(num1, num2) {
    return num1 + num2;
    console.log("Hello world"); // never executed
}

In this example, the console.log will never be displayed because it appears after the return statement.


It’s also possible to have more than one return statement in a function, like this:

function diff(num1, num2) {
if (num1 < num2) {
    return num2 - num1;
} else {
    return num1 - num2;
}
}

Here, the diff() function determines the difference between two numbers. If the first number is less than the second, it subtracts the first from the second; otherwise it subtracts the second from the first. Each branch of the code has its own return statement that does the correct calculation.

The return statement can also be used without specifying a return value. When used in this way, the function stops executing immediately and returns undefined as its value. This is typically used in functions that don’t return a value to stop function execution early, as in the following example, where the console.log won’t be displayed:

function sayHi(name, message) {
    return;
    console.log("Hello " + name + ", " + message); // never called
}

NOTE Best practices dictate that a function either always return a value or never return a value. Writing a function that sometimes returns a value causes confusion, especially during debugging.

Strict mode places several restrictions on functions: 

➤➤ No function can be named eval or arguments.
➤➤ No named parameter can be named eval or arguments.
➤➤ No two named parameters can have the same name.

If any of these occur, it’s considered a syntax error and the code will not execute.

SUMMARY

The core language features of JavaScript are defined in ECMA-262 as a pseudolanguage named ECMAScript. ECMAScript contains all of the basic syntax, operators, data types, and objects necessary to complete basic computing tasks, though it provides no way to get input or to produce output. Understanding ECMAScript and its intricacies is vital to a complete understanding of JavaScript as implemented in web browsers. The following are some of the basic elements of ECMAScript: 

 ➤➤ The basic data types in ECMAScript are Undefined, Null, Boolean, Number, String, and Symbol.


➤➤ Unlike other languages, there’s no separate data type for integers versus floating-point values; the Number type represents all numbers.


➤➤ There is also a complex data type, Object, that is the base type for all objects in the language.


➤➤ A strict mode places restrictions on certain error-prone parts of the language.


➤➤ ECMAScript provides a lot of the basic operators available in C and other C-like languages, including arithmetic operators, Boolean operators, relational operators, equality operators, and assignment operators.


➤➤ The language features flow-control statements borrowed heavily from other languages, such as the if statement, the for statement, and the switch statement.

Functions in ECMAScript behave differently than functions in other languages:

➤➤ There is no need to specify the return value of the function because any function can return any value at any time.


➤➤ Functions that don’t specify a return value actually return the special value undefined.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值