Expressions and Operators - Learning JavaScript (2016)

Learning JavaScript (2016)

Chapter 5. Expressions and Operators

An expression is a special kind of statement that evaluates to a value. The distinction between an expression statement (which results in a value) and a non-expression statement (which does not) is critical: understanding the difference gives you the tools you need to combine language elements in useful ways.

You can think of a (nonexpression) statement as an instruction, and an expression statement as a request for something. Imagine it’s your first day on the job, and the foreman comes over and says, “Your job is to screw widget A into flange B.” That’s a nonexpression statement: the foreman isn’t asking for the assembled part, merely instructing you to do the assembly. If the foreman instead said, “Screw widget A into flange B, and give it to me for inspection,” that would be equivalent to an expression statement: not only are you being given an instruction, you’re also being asked to return something. You may be thinking that either way something gets made: the assembled part exists whether you set it back on the assembly line or give it to the foreman for inspection. In a programming language, it’s similar: a nonexpression statement usually does produce something, but only an expression statement results in an explicit transfer of the thing that was produced.

Because expressions resolve to a value, we can combine them with other expressions, which in turn can be combined with other expressions, and so on. Nonexpression statements, on the other hand, might do something useful, but they cannot be combined in the same way.

Also because expressions resolve to a value, you can use them in assignments. That is, you can assign the result of the expression to a variable, constant, or property. Let’s consider a common operation expression: multiplication. It makes sense that multiplication is an expression: when you multiply two numbers, you get a result. Consider two very simple statements:

let x;

x = 3 * 5;

The first line is a declaration statement; we are declaring the variable x. Of course we could have combined these two lines, but that would confuse this discussion. What’s more interesting is the second line: there are actually two combined expressions in that line. The first expression is 3 * 5, a multiplication expression that resolves to the value 15. Then, there’s an assignment expression that assigns the value 15 to the variable x. Note that the assignment is itself an expression, and we know that an expression resolves to a value. So what does the assignment expression resolve to? As it turns out, assignment expressions resolve quite reasonably to the value that was assigned. So not only is x assigned the value 15, but the whole expression also resolves to the value 15. Because the assignment is an expression that resolves to a value, we could in turn assign it to another variable. Consider the following (very silly) example:

let x, y;

y = x = 3 * 5;

Now we have two variables, x and y, that both contain the value 15. We are able to do this because multiplication and assignment are both expressions. When JavaScript sees combined expressions like this, it has to break the combination down and evaluate it in parts, like so:

let x, y;

y = x = 3 * 5; // original statement

y = x = 15; // multiplication expression evaluated

y = 15; // first assignment evaluated; x now has value 15,

// y is still undefined

15; // second assignment evaluated; y now has value 15,

// the result is 15, which isn't used or assigned to

// anything, so this final value is simply discarded

The natural question is “How did JavaScript know to execute the expressions in that order?” That is, it could have reasonably done the assignment y = x first, giving y the value undefined, and then evaluated the multiplication and the final assignment, leaving y as undefined and x as15. The order in which JavaScript evaluates expressions is called operator precedence, and we’ll cover it in this chapter.

Most expressions, such as multiplication and assignment, are operator expressions. That is, a multiplication expression consists of a multiplication operator (the asterisk) and two operands (the numbers you are trying to multiply, which are themselves expressions).

The two expressions that are not operator expressions are identifier expressions (variable and constant names) and literal expressions. These are self-explanatory: a variable or constant is itself an expression, and a literal is itself an expression. Understanding this allows you to see how expressions provide homogeneity: if everything that results in a value is an expression, it makes sense that variables, constants, and literals are all expressions.

Operators

You can think of operators as the “verb” to an expression’s “noun.” That is, an expression is a thing that results in a value; an operator is something you do to produce a value. The outcome in both cases is a value. We’ll start our discussion of operators with arithmetic operators: however you may feel about math, most people have some experience with arithmetic operators, so they are an intuitive place to start.

NOTE

Operators take one or more operands to produce a result. For example, in the expression 1 + 2, 1 and 2 are the operands and + is the operator. While operand is technically correct, you often see operands called arguments.

Arithmetic Operators

JavaScript’s arithmetic operators are listed in Table 5-1.

Operator

Description

Example

+

Addition (also string concatenation)

3 + 2 // 5

-

Subtraction

3 - 2 // 1

/

Division

3/2 // 1.5

*

Multiplication

3*2 // 6

%

Remainder

3%2 // 1

-

Unary negation

-x // negative x; if x is 5, -x will be -5

+

Unary plus

+x // if x is not a number, this will attempt conversion

++

Pre-increment

++x // increments x by one, and evaluates to the new value

++

Post-increment

x++ // increments x by one, and evaluates to value of x before the increment

--

Pre-decrement

--x // decrements x by one, and evaluates to the new value

--

Post-decrement

x-- // decrements x by one, and evaluates to value of x before the decrement

Table 5-1. Arithmetic operators

Remember that all numbers in JavaScript are doubles, meaning that if you perform an arithmetic operation on an integer (such as 3/2), the result will be a decimal number (1.5).

Subtraction and unary negation both use the same symbol (the minus sign), so how does JavaScript know how to tell them apart? The answer to this question is complex and beyond the scope of the book. What’s important to know is that unary negation is evaluated before subtraction:

const x = 5;

const y = 3 - -x; // y is 8

The same thing applies for unary plus. Unary plus is not an operator you see used very often. When it is, it is usually to force a string to be converted to a number, or to align values when some are negated:

const s = "5";

const y = 3 + +s; // y is 8; without the unary plus,

// it would be the result of string

// concatenation: "35"

// using unnecessary unary pluses so that expressions line up

const x1 = 0, x2 = 3, x3 = -1.5, x4 = -6.33;

const p1 = -x1*1;

const p2 = +x2*2;

const p3 = +x3*3;

const p3 = -x4*4;

Note that in these examples, I specifically used variables in conjunction with unary negation and unary plus. That’s because if you use them with numeric literals, the minus sign actually becomes part of the numeric literal, and is therefore technically not an operator.

The remainder operator returns the remainder after division. If you have the expression x % y, the result will be the remainder when dividing the dividend (x) by the divisor (y). For example, 10 % 3 will be 1 (3 goes into 10 three times, with 1 left over). Note that for negative numbers, the result takes on the sign of the dividend, not the divisor, preventing this operator from being a true modulo operator. While the remainder operator is usually only used on integer operands, in JavaScript, it works on fractional operands as well. For example, 10 % 3.6 will be 3 (3.6 goes into 10 twice, with 2.8 left over).

The increment operator (++) is effectively an assignment and addition operator all rolled into one. Likewise, the decrement operator (--) is an assignment and subtraction operator. They are useful shortcuts, but should be used with caution: if you bury one of these deep in an expression, it might be hard to see the “side effect” (the change in a variable). Understanding the difference between the prefix and postfix operators is important as well. The prefix version modifies the variable and then evaluates to the new value; the postfix operator modifies the variable and then evaluates to the value before modification. See if you can predict what the following expressions evaluate to (hint: increment and decrement operators are evaluated before addition, and we evaluate from left to right in this example):

let x = 2;

const r1 = x++ + x++;

const r2 = ++x + ++x;

const r3 = x++ + ++x;

const r4 = ++x + x++;

let y = 10;

const r5 = y-- + y--;

const r6 = --y + --y;

const r7 = y-- + --y;

const r8 = --y + y--;

Go ahead and run this example in a JavaScript console; see if you can anticipate what r1 through r8 will be, and what the value of x and y is at each step. If you have problems with this exercise, try writing the problem down on a piece of paper, and adding parentheses according to the order of operations, then doing each operation in order. For example:

let x = 2;

const r1 = x++ + x++;

// ((x++) + (x++))

// ( 2 + (x++)) eval left to right; x now has value 3

// ( 2 + 3 ) x now has value 4

// 5 result is 5; x has value 4

const r2 = ++x + ++x;

// ((++x) + (++x))

// ( 5 + (++x)) eval left to right; x now has value 5

// ( 5 + 6 ) x now has value 6

// 11 result is 11; x has value 6

const r3 = x++ + ++x;

// ((x++) + (++x))

// ( 6 + (++x)) eval left to right; x now has value 7

// ( 6 + 8 ) x now has value 8

// 14 result is 14; x has value 8

//

// ... and so on

Operator Precedence

Second in importance to understanding that every expression resolves to a value is understanding operator precedence: it is a vital step to following how a JavaScript program works.

Now that we’ve covered arithmetic operators, we’re going to pause our discussion of JavaScript’s many operators and review operator precedence—if you’ve had an elementary education, you’ve already been exposed to operator precedence, even if you aren’t aware of it.

See if you remember enough from elementary school to solve this problem (I apologize in advance to those who suffer from math anxiety):

8 division-sign 2 plus 3 times left-parenthesis 4 times 2 minus 1 right-parenthesis

If you answered 25, you correctly applied operator precedence. You knew to start inside the parentheses, then move on to multiplication and division, then finish with addition and subtraction.

JavaScript uses a similar set of rules to determine how to evaluate any expression—not just arithmetic expressions. You’ll be pleased to know that arithmetic expressions in JavaScript use the same order of operations you learned in elementary school—perhaps with the aid of the mnemonic “PEMDAS” or “Please Excuse My Dear Aunt Sally.”

In JavaScript, there are many more operators than just arithmetic ones, so the bad news is that you have a much larger order to memorize. The good news is that, just like in mathematics, parentheses trump everything: if you’re ever unsure about the order of operations for a given expression, you can always put parentheses around the operations you want to happen first.

Currently, JavaScript has 56 operators grouped into 19 precedence levels. Operators with a higher precedence are performed before operators with a lower precedence. Although I have gradually memorized this table over the years (without making a conscious effort to do so), I still sometimes consult it to refresh my memory or see where new language features fit into the precedence levels. See Appendix B for the operator precedence table.

Operators at the same precedence level are either evaluated right to left or left to right. For example, multiplication and division have the same precedence level (14) and are evaluated left to right, and assignment operators (precedence level 3) are evaluated right to left. Armed with that knowledge, we can evaluate the order of operations in this example:

let x = 3, y;

x += y = 6*5/2;

// we'll take this in order of precedence, putting parentheses around

// the next operation:

//

// multiplication and division (precedence level 14, left to right):

// x += y = (6*5)/2

// x += y = (30/2)

// x += y = 15

// assignment (precedence level 3, right to left):

// x += (y = 15)

// x += 15 (y now has value 15)

// 18 (x now has value 18)

Understanding operator precedence may seem daunting at first, but it quickly becomes second nature.

Comparison Operators

Comparison operators, as the name implies, are used to compare two different values. Broadly speaking, there are three types of comparison operator: strict equality, abstract (or loose) equality, and relational. (We don’t consider inequality as a different type: inequality is simply “not equality,” even though it has its own operator for convenience.)

The most difficult distinction for beginners to understand is the difference between strict equality and abstract equality. We’ll start with strict equality because I recommend you generally prefer strict equality. Two values are considered strictly equal if they refer to the same object, or if they are the same type and have the same value (for primitive types). The advantage of strict equality is that the rules are very simple and straightforward, making it less prone to bugs and misunderstandings. To determine if values are strictly equal, use the === operator or its opposite, the not strictly equal operator (!==). Before we see some examples, let’s consider the abstract equality operator.

Two values are considered abstractly equal if they refer to the same object (so far, so good) or if they can be coerced into having the same value. It’s this second part that causes so much trouble and confusion. Sometimes this is a helpful property. For example, if you want to know if the number 33 and the string "33" are equal, the abstract equality operator will says yes, but the strict equality operator will say no (because they aren’t the same type). While this may make abstract equality seem convenient, you are getting a lot of undesirable behavior along with this convenience. For this reason, I recommend converting strings into numbers early, so you can compare them with the strict equality operator instead. The abstract equality operator is == and the abstract inequality operator is !=. If you want more information about the problems and pitfalls with the abstract equality operator, I recommend Douglas Crockford’s book, JavaScript: The Good Parts (O’Reilly).

TIP

Most of the problematic behavior of the abstract equality operators surrounds the values null, undefined, the empty string, and the number 0. For the most part, if you are comparing values that you know not to be one of these values, it’s generally safe to use the abstract equality operator. However, don’t underestimate the power of rote habit. If you decide, as I recommend, to use the strict equality operator by default, then you never have to think about it. You don’t have to interrupt your thought flow to wonder whether it’s safe or advantageous to use the abstract equality operator; you just use the strict equality operator and move on. If you later find that the strict equality operator isn’t producing the right result, you can do the appropriate type conversion instead of switching to the problematic abstract equality operator. Programming is hard enough: do yourself a favor, and avoid the problematic abstract equality operator.

Here are some examples of strict and abstract equality operators in use. Note that even though objects a and b contain the same information, they are distinct objects, and are neither strictly or abstractly equal:

const n = 5;

const s = "5";

n === s; // false -- different types

n !== s; // true

n === Number(s); // true -- "5" converted to numeric 5

n !== Number(s); // false

n == s; // true; not recommended

n != s; // false; not recommended

const a = { name: "an object" };

const b = { name: "an object" };

a === b; // false -- distinct objects

a !== b; // true

a == b; // false; not recommended

a != b; // true; not recommended

Relational operators compare values in relation to one another, and they only make sense for data types that have a natural ordering, such as strings (“a” comes before “b”) and numbers (0 comes before 1). The relational operators are less than (<), less than or equal to (<=), greater than (>), and greater than or equal to (>=):

3 > 5; // false

3 >= 5; // false

3 < 5; // true

3 <= 5; // true

5 > 5; // false

5 >= 5; // true

5 < 5; // false

5 <= 5; // true

Comparing Numbers

When doing identity or equality comparisons on numbers, you must take special care.

First, note that the special numeric value NaN is not equal to anything, including itself (that is, NaN === NaN and NaN == NaN are both false). If you want to test to see if a number is NaN, use the built-in isNaN function: isNaN(x) will return true if x is NaN and false otherwise.

Recall that all numbers in JavaScript are doubles: because doubles are (by necessity) approximations, you can run into nasty surprises when comparing them.

If you’re comparing integers (between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER, inclusive), you can safely use identity to test for equality. If you’re using fractional numbers, you are better off using a relational operator to see if your test number is “close enough” to the target number. What’s close enough? That depends on your application. JavaScript does make a special numeric constant available, Number.EPSILON. It’s a very small value (about 2.22e-16), and generally represents the difference required for two numbers to be considered distinct. Consider this example:

let n = 0;

while(true) {

n += 0.1;

if(n === 0.3) break;

}

console.log(`Stopped at ${n}`);

If you try to run this program, you will be in for a rude surprise: instead of stopping at 0.3, this loop will skip right past it and execute forever. This is because 0.1 is a well-known value that can’t be expressed exactly as a double, as it falls between two binary fraction representations. So the third pass through this loop, n will have the value 0.30000000000000004, the test will be false, and the only chance to break the loop will have passed.

You can rewrite this loop to use Number.EPSILON and a relational operator to make this comparison “softer” and successfully halt the loop:

let n = 0;

while(true) {

n += 0.1;

if(Math.abs(n - 0.3) < Number.EPSILON) break;

}

console.log(`Stopped at ${n}`);

Notice we subtract our target (0.3) from our test number (n) and take the absolute value (using Math.abs, which we’ll cover in Chapter 16). We could have done an easier calculation here (for example, we could have just tested to see if n was greater than 0.3), but this is the general way to determine if two doubles are close enough to be considered equal.

String Concatenation

In JavaScript, the + operator doubles as numeric addition and string concatenation (this is quite common; Perl and PHP are notable counterexamples of languages that don’t use + for string concatenation).

JavaScript determines whether to attempt addition or string concatenation by the types of operands. Both addition and concatenation are evaluated from left to right. JavaScript examines each pair of operands from left to right, and if either operand is a string, it assumes string concatenation. If both values are numeric, it assumes addition. Consider the following two lines:

3 + 5 + "8" // evaluates to string "88"

"3" + 5 + 8 // evaluates to string "358"

In the first case, JavaScript evaluated (3 + 5) as addition first. Then it evaluated (8 + "8") as string concatenation. In the second case, it evaluated ("3" + 5) as concatenation, then ("35" + 8) as concatenation.

Logical Operators

Whereas the arithmetic operators we’re all familiar with operate on numbers that can take on an infinite number of values (or at least a very large number of values, as computers have finite memory), logical operators concern themselves only with boolean values, which can take on only one of two values: true or false.

In mathematics—and many other programming languages—logical operators operate only on boolean values and return only boolean values. JavaScript allows you to operate on values that are not boolean, and even more surprising, can return values that aren’t boolean. That is not to say JavaScript’s implementation of logical operators is somehow wrong or not rigorous: if you use only boolean values, you will get results that are only boolean values.

Before we discuss the operators themselves, we need to acquaint ourselves with JavaScript’s mechanism for mapping nonboolean values to boolean values.

Truthy and Falsy Values

Many languages have a concept of “truthy” and “falsy” values; C, for example, doesn’t even have a boolean type: numeric 0 is false, and all other numeric values are true. JavaScript does something similar, except it includes all data types, effectively allowing you to partition any value into truthy or falsy buckets. JavaScript considers the following values to be falsy:

§ undefined

§ null

§ false

§ 0

§ NaN

§ '' (an empty string)

Everything else is truthy. Because there are a great number of things that are truthy, I won’t enumerate them all here, but I will point out some you should be conscious of:

§ Any object (including an object whose valueOf() method returns false)

§ Any array (even an empty array)

§ Strings containing only whitespace (such as " ")

§ The string "false"

Some people stumble over the fact that the string "false" is true, but for the most part, these partitions make sense, and are generally easy to remember and work with. One notable exception might be the fact that an empty array is truthy. If you want an array arr to be falsy if it’s empty, use arr.length (which will be 0 if the array is empty, which is falsy).

AND, OR, and NOT

The three logical operators supported by JavaScript are AND (&&), OR (||), and NOT (!). If you have a math background, you might know AND as conjunction, OR as disjunction, and NOT as negation.

Unlike numbers, which have infinite possible values, booleans can take on only two possible values, so these operations are often described with truth tables, which completely describe their behavior (see Tables 5-2 through 5-4).

x

y

x && y

false

false

false

false

true

false

true

false

false

true

true

true

Table 5-2. Truth table for AND (&&)

x

y

x || y

false

false

false

false

true

true

true

false

true

true

true

true

Table 5-3. Truth table for OR (||)

x

!x

false

true

true

false

Table 5-4.Truth table for NOT (!)

Looking over these tables, you’ll see that AND is true only if both of its operands are true, and OR is false only if both of its operands are false. NOT is straightforward: it takes its only operand and inverts it.

The OR operator is sometimes called “inclusive OR” because if both operands are true, the result is true. There is also an “exclusive OR” (or XOR), which is false if both operands are true. JavaScript doesn’t have a logical operator for XOR, but it does have a bitwise XOR, which will be discussed later.

TIP

If you need the exclusive OR (XOR) of two variables x and y, you can use the equivalent expression (x || y) && x !== y.

Short-Circuit Evaluation

If you look at the truth table for AND (Table 5-2), you’ll notice you can take a shortcut: if x is falsy, you don’t even need to consider the value of y. Similarly, if you’re evaluating x || y, and x is truthy, you don’t need to evaluate y. JavaScript does exactly this, and it’s known as short-circuit evaluation.

Why is short-circuit evaluation important? Because if the second operand has side effects, they will not happen if the evaluation is short-circuited. Very often, the term “side effect” is taken to be a bad thing in programming, but it isn’t always: if the side effect is intentional and clear, then it’s not a bad thing.

In an expression, side effects can arise from incrementing, decrementing, assignment, and function calls. We’ve already covered increment and decrement operators, so let’s see an example:

const skipIt = true;

let x = 0;

const result = skipIt || x++;

The second line in that example has a direct result, stored in the variable result. That value will be true because the first operand (skipIt) is true. What’s interesting, though, is because of the short-circuit evaluation, the increment expression isn’t evaluated, leaving the value of x at 0. If you change skipIt to false, then both parts of the expression have to be evaluated, so the increment will execute: the increment is the side effect. The same thing happens in reverse with AND:

const doIt = false;

let x = 0;

const result = doIt && x++;

Again, JavaScript will not evaluate the second operand, which contains the increment, because the first operand to AND is false. So result will be false, and x will not be incremented. What happens if you change doIt to true? JavaScript has to evaluate both operands, so the increment will happen, and result will be 0. Wait, what? Why is result 0 and not false? The answer to that question brings us neatly to our next subject.

Logical Operators with Nonboolean Operands

If you are using boolean operands, the logical operators always return booleans. If you’re not, however, the value that determined the outcome gets returned, as Tables 5-5 and 5-6 show.

x

y

x && y

falsy

falsy

x (falsy)

falsy

truthy

x (falsy)

truthy

falsy

y (falsy)

truthy

truthy

y (truthy)

Table 5-5. Truth table for AND (&&) with nonboolean operands

x

y

x || y

falsy

falsy

y (falsy)

falsy

truthy

y (truthy)

truthy

falsy

x (truthy)

truthy

truthy

x (truthy)

Table 5-6. Truth table for OR (||) with nonboolean operands

Note that if you convert the result to a boolean, it will be correct according to the boolean definitions of AND and OR. This behavior of the logical operators allows you to take certain convenient shortcuts. Here’s one that you will see a lot:

const options = suppliedOptions || { name: "Default" }

Remember that objects (even if they are empty) always evaluate to truthy. So if suppliedOptions is an object, options will refer to suppliedOptions. If no options were supplied—in which case, suppliedOptions will be null or undefined—options will get some default values.

In the case of NOT, there’s no reasonable way to return anything other than a boolean, so the NOT operator (!) always returns a boolean for an operand of any type. If the operand is truthy, it returns false, and if the operand is falsy, it returns true.

Conditional Operator

The conditional operator is JavaScript’s sole ternary operator, meaning it takes three operands (all other operands take one or two). The conditional operator is the expression equivalent of an if...else statement. Here’s an example of the conditional operator:

const doIt = false;

const result = doIt ? "Did it!" : "Didn't do it.";

If the first operand (which comes before the question mark—doIt, in this example) is truthy, the expression evaluates to the second operand (between the question mark and colon), and if it’s falsy, it evaluates to the third operand (after the colon). Many beginning programmers see this as a more confusing version of an if...else statement, but the fact that it’s an expression and not a statement gives it a very useful property: it can be combined with other expressions (such as the assignment to result in the last example).

Comma Operator

The comma operator provides a simple way to combine expressions: it simply evaluates two expressions and returns the result of the second one. It is convenient when you want to execute more than one expression, but the only value you care about is the result of the final expression. Here is a simple example:

let x = 0, y = 10, z;

z = (x++, y++);

In this example, x and y are both incremented, and z gets the value 10 (which is what y++ returns). Note that the comma operator has the lowest precedence of any operator, which is why we enclosed it in parentheses here: if we had not, z would have received 0 (the value of x++), and theny would have been incremented. You most commonly see this used to combine expressions in a for loop (see Chapter 4) or to combine multiple operations before returning from a function (see Chapter 6).

Grouping Operator

As mentioned already, the grouping operator (parentheses) has no effect other than to modify or clarify operator precedence. Thus, the grouping operator is the “safest” operator in that it has no effect other than on order of operations.

Bitwise Operators

Bitwise operators allow you to perform operations on all the individual bits in a number. If you’ve never had any experience with a low-level language such as C, or have never had any exposure to how numbers are stored internally in a computer, you may want to read up on these topics first (you are also welcome to skip or skim this section: very few applications require bitwise operators anymore). Bitwise operators treat their operands as 32-bit signed integers in two’s complement format. Because all numbers in JavaScript are doubles, JavaScript converts the numbers to 32-bit integers before performing bitwise operators, and then converts them back to doubles before returning the result.

Bitwise operators are related to the logical operators in that they perform logical operations (AND, OR, NOT, XOR), but they perform them on every individual bit in an integer. As Table 5-7 shows, they also include additional shift operators to shift bits into different positions.

Operator

Description

Example

&

Bitwise AND

0b1010 & 0b1100 // result: 0b1000

|

Bitwise OR

0b1010 | 0b1100 // result: 0b1110

^

Bitwise XOR

0b1010 ^ 0b1100 // result: 0b0110

~

Bitwise NOT

~0b1010 // result: 0b0101

<<

Left shift

0b1010 << 1 // result: 0b10100

0b1010 << 2 // result: 0b101000

>>

Sign-propagating right shift

(See below)

>>>

Zero-fill right shift

(See below)

Table 5-7. Bitwise operators

Note that left shifting is effectively multiplying by two, and right shifting is effectively dividing by two and rounding down.

In two’s complement, the leftmost bit is 1 for negative numbers, and 0 for positive numbers, hence the two ways to perform a right shift. Let’s take the number –22, for example. If we want to get its binary representation, we start with positive 22, take the complement (one’s complement), and then add one (two’s complement):

let n = 22 // 32-bit binary: 00000000000000000000000000010110

n >> 1 // 00000000000000000000000000001011

n >>> 1 // 00000000000000000000000000001011

n = ~n // one's complement: 11111111111111111111111111101001

n++ // two's complement: 11111111111111111111111111101010

n >> 1 // 11111111111111111111111111110101

n >>> 1 // 01111111111111111111111111110101

Unless you are interfacing with hardware, or getting a better grasp on how numbers are represented by a computer, you’ll probably have little call to use bitwise operators (often playfully called “bit twiddling”). One nonhardware use you may see is using bits to efficiently store “flags” (boolean values).

For example, consider Unix-style file permissions: read, write, and execute. A given user can have any combination of these three settings, making them ideal for flags. Because we have three flags, we need three bits to store the information:

const FLAG_READ 1 // 0b001

const FLAG_WRITE 2 // 0b010

const FLAG_EXECUTE 4 // 0b100

With bitwise operators, we can combine, toggle, and detect individual flags in a single numeric value:

let p = FLAG_READ | FLAG_WRITE; // 0b011

let hasWrite = p & FLAG_WRITE; // 0b010 - truthy

let hasExecute = p & FLAG_EXECUTE; // 0b000 - falsy

p = p ^ FLAG_WRITE; // 0b001 -- toggle write flag (now off)

p = p ^ FLAG_WRITE; // 0b011 -- toggle write flag (now on)

// we can even determine multiple flags in one expression:

const hasReadAndExecute = p & (FLAG_READ | FLAG_EXECUTE);

Note that for hasReadAndExecute we had to use the grouping operator; AND has a higher precedence than OR, so we had to force the OR to evaluate before the AND.

typeof Operator

The typeof operator returns a string representing the type of its operand. Unfortunately, this operator doesn’t exactly map to the seven data types in JavaScript (undefined, null, boolean, number, string, symbol, and object), which has caused no end of criticism and confusion.

The typeof operator has one quirk that’s usually referred to as a bug: typeof null returns "object". null is not, of course, an object (it is a primitive). The reasons are historical and not particularly interesting, and it has been suggested many times that it be fixed, but too much existing code is already built on this behavior, so it’s now immortalized in the language specification.

typeof is also often criticized for not being able to distinguish arrays and nonarray objects. It correctly identifies functions (which are also special types of objects), but typeof [] results in "object".

Table 5-8 lists the possible return values of typeof.

Expression

Return value

Notes

typeof undefined

"undefined"

typeof null

"object"

Unfortunate, but true

typeof {}

"object"

typeof true

"boolean"

typeof 1

"number"

typeof ""

"string"

typeof Symbol()

"symbol"

New in ES6

typeof function() {}

"function"

Table 5-8. typeof return values

NOTE

Because typeof is an operator, parentheses are not required. That is, if you have a variable x, you can use typeof x instead of typeof(x). The latter is valid syntax—the parentheses just form an unnecessary expression group.

void Operator

The void operator has only one job: to evaluate its operand and then return undefined. Sound useless? It is. It can be used to force expression evaluation where you want a return value of undefined, but I have never run across such a situation in the wild. The only reason I include it in this book is that you will occasionally see it used as the URI for an HTML <a> tag, which will prevent the browser from navigating to a new page:

<a href="javascript:void 0">Do nothing.</a>

This is not a recommended approach, but you do see it from time to time.

Assignment Operators

The assignment operator is straightforward: it assigns a value to a variable. What’s on the lefthand side of the equals sign (sometimes called the lvalue) must be a variable, property, or array element. That is, it must be something that can hold a value (assigning a value to a constant is technically part of the declaration, not an assignment operator).

Recall from earlier in this chapter that assignment is itself an expression, so the value that’s returned is the value that’s being assigned. This allows you to chain assignments, as well as perform assignments within other expressions:

let v, v0;

v = v0 = 9.8; // chained assignment; first v0 gets

// the value 9.8, and then v gets the

// value 9.8

const nums = [ 3, 5, 15, 7, 5 ];

let n, i=0;

// note the assignment in the while condition; n

// takes on the value of nums[i], and the whole

// expression also evalutes to that value, allowing

// a numeric comparison:

while((n = nums[i]) < 10, i++ < nums.length) {

console.log(`Number less than 10: ${n}.`);

}

console.log(`Number greater than 10 found: ${n}.`);

console.log(`${nums.length} numbers remain.`);

Notice in the second example, we have to use a grouping operator because assignment has a lower precedence than relational operators.

In addition to the ordinary assignment operators, there are convenience assignment operators that perform an operation and assignment in one step. Just as with the ordinary assignment operator, these operators evaluate to the final assignment value. Table 5-9 summarizes these convenience assignment operators.

Operator

Equivalent

x += y

x = x + y

x -= y

x = x - y

x *= y

x = x * y

x /= y

x = x / y

x %= y

x = x % y

x <<= y

x = x << y

x >>= y

x = x >> y

x >>>= y

x = x >>> y

x &= y

x = x & y

x |= y

x = x | y

x ^= y

x = x ^ y

Table 5-9.Assignment with operation

Destructuring Assignment

New in ES6 is a welcome feature called destructuring assignment that allows you to take an object or an array, and “destructure” it into individual variables. We’ll start with object destructuring:

// a normal object

const obj = { b: 2, c: 3, d: 4 };

// object destructuring assignment

const {a, b, c} = obj;

a; // undefined: there was no property "a" in obj

b; // 2

c; // 3

d; // reference error: "d" is not defined

When you destructure an object, the variable names must match property names in the object (array destructuring can only assign property names that are identifiers). In this example, a didn’t match any property in the object, so it received the value undefined. Also, because we didn’t specify d in the declaration, it was not assigned to anything.

In this example, we’re doing declaration and assignment in the same statement. Object destructuring can be done on assignment alone, but it must be surrounded by parentheses; otherwise, JavaScript interprets the lefthand side as a block:

const obj = { b: 2, c: 3, d: 4 };

let a, b, c;

// this produces an error:

{a, b, c} = obj;

// this works:

({a, b, c} = obj);

With array destructuring, you can assign any names you want (in order) to the elements of the array:

// a normal array

const arr = [1, 2, 3];

// array destructuring assignment

let [x, y] = arr;

x; // 1

y; // 2

z; // error: z hasn't been defined

In this example, x is assigned the value of the first element in the array, and y is assigned the value of the second element; all elements past that are discarded. It’s possible to take all the remaining elements and put those in a new array with the spread operator (...), which we’ll cover inChapter 6:

const arr = [1, 2, 3, 4, 5];

let [x, y, ...rest] = arr;

x; // 1

y; // 2

rest; // [3, 4, 5]

In this example, x and y receive the first two elements, and the variable rest receives the rest (you don’t have to name the variable rest; you can use whatever name you want). Array destructuring makes it easy to swap the values of variables (something that previously required a temporary variable):

let a = 5, b = 10;

[a, b] = [b, a];

a; // 10

b; // 5

NOTE

Array destructuring doesn’t only work on arrays; it works on any iterable object (which we’ll cover in Chapter 9).

In these simple examples, it would have been easier to simply assign the variables directly instead of using destructuring. Where destructuring comes in handy is when you get an object or array from elsewhere, and can easily pick out certain elements. We’ll see this used to great effect inChapter 6.

Object and Array Operators

Objects, arrays, and functions have a collection of special operators. Some we have already seen (such as the member access and computed member access operator), and the rest will be covered in Chapters 6, 8, and 9. For completeness, they are summarized in Table 5-10.

Operator

Description

Chapter

.

Member access

Chapter 3

[]

Computed member access

Chapter 3

in

Property existence operator

Chapter 9

new

Object instantiation operator

Chapter 9

instanceof

Prototype chain test operator

Chapter 9

...

Spread operator

Chapter 6, Chapter 8

delete

Delete operator

Chapter 3

Table 5-10. Object and array operators

Expressions in Template Strings

Template strings, which we introduced in Chapter 3, can be used to inject the value of any expression into a string. The example in Chapter 3 used a template string to display the current temperature. What if we wanted to display the temperature difference or display the temperature in degrees Fahrenheit instead of Celsius? We can use expressions in template strings:

const roomTempC = 21.5;

let currentTempC = 19.5;

const message = `The current temperature is ` +

`${currentTempC-roomTempC}\u00b0C different than room temperature.`;

const fahrenheit =

`The current temperature is ${currentTempC * 9/5 + 32}\u00b0F`;

Again we see the pleasing symmetry that expressions bring. We can use variables by themselves in a template string because a variable is simply a type of expression.

Expressions and Control Flow Patterns

In Chapter 4, we covered some common control flow patterns. Now that we’ve seen some expressions that can affect control flow (ternary expressions and short-circuit evaluation), we can cover some additional control flow patterns.

Converting if...else Statements to Conditional Expressions

Whenever an if...else statement is used to resolve a value—either as part of assignment, a smaller part of an expression, or the return value of a function—it is generally preferable to use the conditional operator instead of if...else. It produces more compact code that is easier to read. For example:

if(isPrime(n)) {

label = 'prime';

} else {

label = 'non-prime';

}

would be better written as:

label = isPrime(n) ? 'prime' : 'non-prime';

Converting if Statements to Short-Circuited Logical OR Expressions

Just as if...else statements that resolve to a value can easily be translated to conditional expressions, if statements that resolve to a value can easily be translated to short-circuited logical OR expressions. This technique is not as obviously superior as conditional operators overif...else statements, but you will see it quite often, so it’s good to be aware of. For example,

if(!options) options = {};

can be easily translated to:

options = options || {};

Conclusion

JavaScript, like most modern languages, has an extensive and useful collection of operators, which will form the basic building blacks for manipulating data. Some, like the bitwise operators, you will probably use very seldom. Others, such as the member access operators, you won’t even normally think about as operators (where it can come in handy is when you’re trying to untangle a difficult operator precedence problem).

Assignment, arithmetic, comparison, and boolean operators are the most common operators, and you’ll be using them frequently, so make sure you have a good understanding of them before proceeding.