Another RayJune

Eloquent JavaScript 小记

Don’t assume you understand them until you’ve actually written a working solution.

Preface

This book intends to make you familiar enough with JavaScript to be able to make a computer do what you want.

Why language matters

A good programming language helps the programmer by allowing them to talk about the actions that the computer has to perform on a higher level.

It helps omit uninteresting details, provides convenient building blocks (such as while and console.log), allows you to define your own building blocks (such as sum and range), and makes those blocks easy to compose.

What is JavaScript?

JavaScript is ridiculously liberal in what it allows. The idea behind this design was that it would make programming in JavaScript easier for beginners.

In actuality, it mostly makes finding problems in your programs harder because the system will not point them out to you.

liberal 宽容的,慷慨的

This flexibility also has its advantages, though. It leaves space for a lot of techniques that are impossible in more rigid languages, and as you will see (for example in Chapter 10) it can be used to overcome some of JavaScript’s shortcomings.

Code, and what to do with it

In my experience, reading code and writing code are indispensable parts of learning to program, so try to not just glance over the examples. Don’t assume you understand them until you’ve actually written a working solution.

indispensable 不可缺少的,绝对必要的

Values, Types, and Operators

Numbers

fractional digital numbers

Calculations with whole numbers (also called integers) smaller than the aforementioned 9 quadrillion are guaranteed to always be precise.
Unfortunately, calculations with fractional numbers are generally not.

The important thing is to be aware of it and treat fractional digital numbers as approximations, not as precise values.

approximations 近似物

such as:

1
2
1.222 - 1.111
// 0.11099999999999999

Special numbers

There are three special values in JavaScript that are considered numbers but don’t behave like normal numbers.

The result two are Infinity and -Infinity.

Infinity - 1 is still Infinity, and so on. Don’t put too much trust in infinity-based computation. It isn’t mathematically solid, and it will quickly lead to our next special number: NaN.

NaN stands for “not a number”, even though it is a value of the number type.

1
console.log(typeof NaN);    // number

You’ll get this result when you, for example, try to calculate 0 / 0 (zero divided by zero), Infinity - Infinity, or any number of other numeric operations that don’t yield a precise, meaningful result.

1
2
console.log(typeof NaN);    // number
console.log(typeof Infinity); // number

Strings

Strings cannot be divided, multiplied, or subtracted, but the + operator can be used on them. It does not add, but it concatenates—it glues two strings together. The following line will produce the string “concatenate”:

1
'con' + 'cat' + 'e' + 'nate';

Unary operators

Not all operators are symbols. Some are written as words. One example:

1
2
console.log(typeof 4.5) //number 
console.log(typeof "x") // string

The other operators we saw all operated on two values, but typeof takes only one.

Operators that use two values are called binary operators, while those that take one are called unary operators.

The minus operator can be used both as a binary operator and as a unary operator.

1
console.log(- (10 - 2)) // → -8

and ! to negate logically

1
console.log(!!true) // → true

Comparisons

There is only one value in JavaScript that is not equal to itself, and that is NaN, which stands for “not a number”.

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

NaN is supposed to denote the result of a nonsensical computation, and as such, it isn’t equal to the result of any other nonsensical computations.

Automatic type conversion

JavaScript goes out of its way to accept almost any program you give it, even programs that do odd things. This is nicely demonstrated by the following expressions:

1
2
3
4
5
console.log(8 * null) // 0 because null -> 0
console.log('5' - 1) //4
console.log('5' + 1) // 51
console.log("five" * 2) //NaN
console.log(false == 0) //false

When an operator is applied to the “wrong” type of value, JavaScript will quietly convert that value to the type it wants, using a set of rules that often aren’t what you want or expect. This is called type coercion.

When something that doesn’t map to a number in an obvious way (such as “five” or undefined) is converted to a number, the value NaN is produced.

Boolean: value false

  • 0
  • undefined
  • null
  • NaN
  • empty string (“”)
  • false

Program Structure

The environment

The collection of variables and their values that exist at a given time is called the environment.

When a program starts up, this environment is not empty. It always contains variables that are part of the language standard, and most of the time, it has variables that provide ways to interact with the surrounding system.

For example, in a browser, there are variables and functions to inspect and in influence the currently loaded website and to read mouse and keyboard input.

Summary

You now know that a program is built out of statements, which themselves sometimes contain more statements. Statements tend to contain expressions, which themselves can be built out of smaller expressions.

Putting statements after one another gives you a program that is executed from top to bottom. You can introduce disturbances in the flow of control by using conditional (if, else, and switch) and looping (while, do, and for) statements.

Functions are special values that encapsulate a piece of program. You can invoke them by writing functionName(argument1, argument2). Such a function call is an expression, and may produce a value.

Exercises

Looping a triangle

my solution (maybe better)

1
2
3
4
5
6
7
var i;
var shap = '#';

for (i = 0; i < 7; i++) {
console.log(shap);
shap += '#';
}

book’s standard solution (readability worse)

1
2
for (var line = "#"; line.length < 8; line += "#")
console.log(line);

FizzBuzz

my solution (a little redundancy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (var i = 0; i < 100; i++) {
if (i % 3 === 0) {
if (i % 5 === 0) {
console.log('FizzBuzz');
} else {
console.log('Fizz');
}
} else if (i % 5 === 0) {
if (i % 3 === 0) {
console.log('FizzBuzz');
} else {
console.log('Buzz');
}
} else {
console.log(i);
}
}

book’s standard solution (better choice)

1
2
3
4
5
6
7
8
9
10
11
12
13
var n;
var output;

for (n = 1; n <= 100; n++) {
output = '';
if (n % 3 === 0) {
output += 'Fizz';
}
if (n % 5 === 0) {
output += 'Buzz';
}
console.log(output || n); // 如果 output Boolean 为 true,则打印 output
}

Chess board

my solution (maybe better)

1
2
3
4
5
6
7
8
9
var i;

for (i = 1; i < 9; i++) {
if (i % 2) {
console.log(' # # # #');
} else {
console.log('# # # #');
}
}

book’s standard solution (too complex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var size = 8;
var board = "";

for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
if ((x + y) % 2 == 0)
board += " ";
else
board += "#";
}
board += "\n";
}

console.log(board);

Functions

(*)Function hoisting

1
2
3
4
console.log("The future says:", future());
function future() {
return "We STILL have no flying cars.";
}

This code works. They are conceptually moved to the top of their scope and can be used by all the code in that scope.

This is sometimes useful because it gives us the freedom to order code in a way that seems meaningful.

What happens when you put such a function definition inside a conditional (if) block or a loop? Well, don’t do that.

Different JavaScript platforms in different browsers have traditionally done different things in that situation, and the latest standard actually forbids it.

1
2
3
4
5
6
function example() { 
function a() {} // Okay
if (something) {
function b() {} // Danger!
}
}

(*)The call stack

Take a closer look at the way control flows through functions:

1
2
3
4
5
function greet(who) { 
console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

A run through this program goes roughly like this:

1
2
3
4
5
6
7
top 
greet
console.log
greet
top
console.log
top

Because a function has to jump back to the place of the call when it returns, the computer must remember the context from which the function was called.

The place where the computer stores this context is the call stack.

Every time a function is called, the current context is put on top of this “stack”. When the function returns, it removes the top context from the stack and uses it to continue execution.

Storing this stack requires space in the computer’s memory. When the stack grows too big, the computer will fail with a message like “out of stack space” or “too much recursion”.

The following code illustrates this by asking the computer a really hard question, which causes an infinite back-and-forth between two functions:

back-and-forth 反复的,来回的

1
2
3
4
5
6
7
function chicken() { 
return egg();
}
function egg() {
return chicken();
}
console.log(chicken() + " came first."); // → ??

addition question: Browser JavaScript Stack size limit?

This is browser specific, not only the stack size, but also optimizations, things like tail recursion optimization and stuff.
I guess the only reliable thing here is to code in a way that doesn’t put tons of stuff into the stack, or manually testing(reading deep into the documentation of) each browser.

After all, when you see the “too much recursion” error or similar you already know there’s something really wrong with your code.

https://stackoverflow.com/questions/7826992/browser-JavaScript-stack-size-limit

Optional Arguments

JavaScript is extremely broad-minded about the number of arguments you pass to a function If you pass too many, the extra ones are ignored. If you pass too few, the missing parameters simply get assigned the value undefined.

(*)Closure

The ability to treat functions as values, combined with the fact that local variables are “re-created” every time a function is called, brings up an interesting question.

What happens to local variables when the function call that created them is no longer active?

1
2
3
4
5
6
7
8
9
10
11
12
function wrapValue(n) {
var localVariable = n;

return function() {
return localVariable;
};
}

var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);
console.log(wrap1()); // → 1
console.log(wrap2()); // → 2

This is allowed and works as you’d hope—the variable can still be accessed.

This feature being able to reference a specific instance of local variables in an enclosing function—is called closure. A function that “closes over” some local variables is called a closure.

(*)Recursion

A function that calls itself is called recursive.

1
2
3
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

This is rather close to the way mathematicians define exponentiation and arguably describes the concept in a more elegant way than the looping variant does.

But this implementation has one important problem: in typical JavaScript implementations, it’s about 10 times slower than the looping version. Running through a simple loop is a lot cheaper than calling a function multiple times.

The dilemma of speed versus elegance is an interesting one. You can see it as a kind of continuum between human-friendliness and machine-friendliness.

Almost any program can be made faster by making it bigger and more convoluted. The programmer must decide on an appropriate balance.

(*)Efficiency about loop & recursion

Often, though, a program deals with such complex concepts that giving up some efficiency in order to make the program more straightforward becomes an attractive choice.

The basic rule, is to not worry about efficiency until you know for sure that the program is too slow.

If it is, find out which parts are taking up the most time, and start exchanging elegance for efficiency in those parts.

Of course, this rule doesn’t mean one should start ignoring performance altogether. Sometimes an experienced programmer can see right away that a simple approach is never going to be fast enough.

The reason I’m stressing this is that surprisingly many beginning programmers focus fanatically on efficiency, even in the smallest details. The result is bigger, more complicated, and often less correct programs, that take longer to write than their more straightforward equivalents and that usually run only marginally faster.

But recursion is not always just a less-efficient alternative to looping. Some problems are much easier to solve with recursion than with loops. Most often these are problems that require exploring or processing several “branches”, each of which might branch out again into more branches.

Consider this puzzle: by starting from the number 1 and repeatedly either adding 5 or multiplying by 3, an infinite amount of new numbers can be produced. How would you write a function that, given a number, tries to find a sequence of such additions and multiplications that produce that number?

Here is a recursive solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function findSolution(target) {
function find(current, history) {
if (current == target) {
return history;
} else if (current > target) {
return null;
} else {
return find(current + 5, '(' + history + ' + 5)') || find(current * 3, '(' + history + ' * 3)');
}
}
return find(1, '1');
}

console.log(findSolution(24)); //→ (((1 * 3) + 5) * 3)

Growing functions

There are two more or less natural ways for functions to be introduced into programs. When you:

  1. find yourself writing very similar code multiple times.
  2. find you need some functionality that you haven’t written yet and that sounds like it deserves its own function. .

How difficult it is to find a good name for a function is a good indication of how clear a concept it is that you’re trying to wrap. Let’s go through an example.

1
2
3
4
5
6
7
8
9
10
11
12
13
function printFarmInventory(cows, chickens) {
var cowString = String(cows);
while (cowString.length < 3) {
cowString = '0' + cowString;
}
console.log(cowString + ' Cows');
var chickenString = String(chickens);
while (chickenString.length < 3) {
chickenString = '0' + chickenString;
}
console.log(chickenString + ' Chickens');
}
printFarmInventory(7, 11);

But if famers calls and tells us he’s also started keeping pigs, and couldn’t we please extend the software to also print pigs?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function printZeroPaddedWithLabel(number, label) {
var numberString = String(number);
while (numberString.length < 3) {
numberString = '0' + numberString;
}
console.log(numberString + ' ' + label);
}

function printFarmInventory(cows, chickens, pigs) {
printZeroPaddedWithLabel(cows, 'Cows');
printZeroPaddedWithLabel(chickens, 'Chickens');
printZeroPaddedWithLabel(pigs, 'Pigs');
}
printFarmInventory(7, 11, 3);

It works! But that name, printZeroPaddedWithLabel, is a little awkward.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function zeroPad(number, width) {
var string = String(number);
while (string.length < width) {
string = '0' + string;
}
return string;
}

function printFarmInventory(cows, chickens, pigs) {
console.log(zeroPad(cows, 3) + ' Cows');
console.log(zeroPad(chickens, 3) + ' Chickens');
console.log(zeroPad(pigs, 3) + ' Pigs');
}
printFarmInventory(7, 16, 3);

A function with a nice, obvious name like zeroPad makes it easier for someone who reads the code to figure out what it does. And it is useful in more situations than just this specific program. For example, you could use it to help print nicely aligned tables of numbers.

How smart and versatile should our function be?

A useful principle is not to add cleverness unless you are absolutely sure you’re going to need it.

Exercises

Minimum

my solution (maybe better)

1
2
3
4
5
function min(num1, num2) {
return num1 < num2 ? num1 : num2;
}

min(42, 24); // 24

book’s standard solution

1
2
3
4
5
6
7
8
9
10
11
function min(a, b) {
if (a < b)
return a;
else
return b;
}

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

Recursion

my solution (almost as same as the standard solution, all good)

1
2
3
4
5
6
7
8
9
10
11
12
13
function isEven(num) {
if (num === 0) {
return false;
} else if (num === 1) {
return true;
} else if (num < 0) {
return void 0;
}
return isEven(num - 2);
}

isEven(3); // true
isEven(8); // false

book’s standard solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function isEven(n) {
if (n == 0)
return true;
else if (n == 1)
return false;
else if (n < 0)
return isEven(-n);
else
return isEven(n - 2);
}

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → false

Bean counting

my solution (forget the essence of the subject, not correct)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function countBs(str) {
var i;
var times = 0;
for (i = 0; i < str.length; i++) {
if (str[i] === 'B') {
times++;
}
}
return times;
}

countBs('B11111B'); // 2

function countChar(str, letter) {
var i;
var times = 0;
if (!letter) {
letter = 'B';
}
for (i = 0; i < str.length; i++) {
if (str[i] === letter) {
times++;
}
}
return times;
}

countChar('BCBCBC','C'); // 3
countChar('BCBCC',); // 2

book’s standard solution (better)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function countChar(str, ch) {
var i;
var counted = 0;
var len = str.length;

for (i = 0; i < len; i++) {
if (str.charAt(i) === ch) {
counted += 1;
}
}

return counted;
}

function countBs(str) {
return countChar(str, "B");
}

console.log(countBs("BBC"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4

Data Structures: Objects and Arrays

in operator

The in operator returns true if the specified property is in the specified object or its prototype chain.

The same keyword can also be used in a for loop (for (var name in object)) to loop over an object’s properties.

object == object

With objects, there is a difference between having two reference to the same object and having two different objects that contain the same properties. Consider the following code:

JavaScript’s == operator, when comparing objects, will return true only if both objects are precisely the same value. Comparing different objects will return false, even if they have identical contents.

There is no “deep” comparison operation built into JavaScript, which looks at object’s contents, but it is possible to write it yourself (which will be one of the exercises at the end of this chapter).

Exercises

The sum of a range

my solution (all good)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function range(start, end, step) {
var arr = [];
var i = start;

if (!step) {
step = 1;
}
if (step > 0) {
for (; i <= end; i += step) {
arr.push(i);
}
} else {
for (; i >= end; i += step) {
arr.push(i);
}
}

return arr;
}

function sum(arr) {
var i;
var result = 0;
var len = arr.length;

for (i = 0; i < len; i++) {
result += arr[i];
}

return result;
}

console.log(range(1, 10))
// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(range(5, 2, -1));
// → [5, 4, 3, 2]
console.log(sum(range(1, 10)));

book’s standard solution (almost same as mine)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function range(start, end, step) {
if (step == null) step = 1;
var array = [];

if (step > 0) {
for (var i = start; i <= end; i += step)
array.push(i);
} else {
for (var i = start; i >= end; i += step)
array.push(i);
}
return array;
}

function sum(array) {
var total = 0;
for (var i = 0; i < array.length; i++)
total += array[i];
return total;
}

console.log(range(1, 10))
// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(range(5, 2, -1));
// → [5, 4, 3, 2]
console.log(sum(range(1, 10)));
// → 55

Reversing an array

my solution (use Array.reverse)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function reverseArray(arr) {
var i;
var reverseedArr = [];

for (i = arr.length - 1; i >= 0; i--) {
reverseedArr.push(arr[i]);
}

return reverseedArr;
}

function reverseArrayInPlace(arr) {
arr = arr.reverse();
}

console.log(reverseArray(["A", "B", "C"]));
// → ["C", "B", "A"];
var arrayValue = [1, 2, 3, 4, 5];
reverseArrayInPlace(arrayValue);
console.log(arrayValue);
// → [5, 4, 3, 2, 1]

book’s standard solution (After my optimization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function reverseArray(arr) {
var i;
var output = [];

for (i = array.length - 1; i >= 0; i--) {
output.push(arr[i]);
}

return output;
}

function reverseArrayInPlace(arr) {
var i;
var len = arr.length;

for (i = 0; i < Math.floor(len / 2); i++) {
swap(arr, i, len - 1 - i);
}

return arr;
}

function swap(arr, indexA, indexB) {
var temp;

temp = arr[indexA];
arr[indexA] = arr[indexB];
arr[indexB] = temp;
}

console.log(reverseArray(["A", "B", "C"]));
// → ["C", "B", "A"];
var arrayValue = [1, 2, 3, 4, 5];
reverseArrayInPlace(arrayValue);
console.log(arrayValue);
// → [5, 4, 3, 2, 1]

A list

A list just like this:

1
2
3
4
5
6
7
8
9
10
var list = {
value: 1,
rest: {
value: 2,
rest: {
value: 3,
rest: null
}
}
};

not accomplish

book’s standard solution (awesome)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function arrayToList(array) {
var i;
var list = null; // first list-item's reset points to null

for (i = array.length - 1; i >= 0; i--) {
list = {
value: array[i],
rest: list
};
}

return list;
}

function listToArray(list) {
var arr = [];
var node = list;

while (node) {
arr.push(node.value);
node = node.rest;
}

return arr;
}

function prepend(value, list) {
return {
value: value,
reset: list
};
}

function nth(list, n) {
if (!list) {
return void 0;
} else if (n === 0) {
return list.value;
}

return nth(list.rest, n - 1);
}

console.log(arrayToList([10, 20]));
// → {value: 10, rest: {value: 20, rest: null}}
console.log(listToArray(arrayToList([10, 20, 30])));
// → [10, 20, 30]
console.log(prepend(10, prepend(20, null)));
// → {value: 10, rest:{value: 20, rest: null}}
console.log(nth(arrayToList([10, 20, 30]), 1));
// → 20

Deep comparison

my solution (incomprehensive, not good)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepEqual(para1, para2) {
if (para1 === para2) {
return true;
}
if (typeof para1 === 'object' && typeof para2 === 'object') {
for (pro in para1) {
if (pro in para2) {
return false;
}
}
return true;
}
return false;
}

book’s standard solution (After my optimization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function deepEqual(a, b) {
var prop;
var propsInA = 0;
var propsInB = 0;

if (a === b) {
return true;
}
if (a === null || b === null || !(a instanceof Object) || !(b instanceof Object)) {
return false;
}
propsInA = Object.keys(a).length;
for (prop in b) {
if (b.hasOwnProperty(prop)) {
propsInB += 1;
if (!(a.hasOwnProperty(prop)) || !deepEqual(a[prop], b[prop])) {
return false;
}
}
}

return propsInA === propsInB;
}

var obj = {here: {is: 'an'}, object: 2};
console.log(deepEqual(obj, obj));
// → true
console.log(deepEqual(obj, {here: 1, object: 2}));
// → false
console.log(deepEqual(obj, {here: {is: 'an'}, object: 2}));
// → true

Functional Programing

There are two ways of constructing a software design:

One way is to make it so simple that there are obviously no deficiencies,
and the other way is to make it so complicated that there are no obvious deficiencies.

C.A.R. Hoare, 1980 ACM Turing Award Lecture

Let’s briefly go back to the final two example programs in the introduction:

1
2
3
4
5
6
7
8
9
10
11
var total = 0;
var count = 1;

while (count <= 10) {
total += count;
count += 1;
}
console.log(total);

// functional programing
console.log(sum(range(1, 10)));

Which one is more likely to contain a bug?

The definitions of this vocabulary (the functions sum and range) will still involve loops, counters, and other incidental details. But because they are expressing simpler concepts than the program as a whole, they are easier to get right.

Abstraction

Abstractions hide details and give us the ability to talk about problems at a higher (or more abstract) level.

For a programmer, to notice when a concept is begging to be abstracted into a new word.

Composability

Higher-order functions start to shine when you need to compose functions. As an example, let’s write code that finds the average age for men and for women in the data set.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function average(array) {
function plus(a, b) {
return a + b;
}
return array.reduce(plus) / array.length;
}
function age(p) {
return p.died - p.born;
}
function male(p) {
return p.sex == "m";
}
function female(p) {
return p.sex == "f";
}
console.log(average(ancestry.filter(male).map(age))); // → 61.67
console.log(average(ancestry.filter(female).map(age))); // → 54.56

This is fabulous for writing clear code. Unfortunately, this clarity comes at a cost.

fabulous 极好的

The cost

In the happy land of elegant code and pretty rainbows, there lives a spoil-sport monster called inefficiency.

But function calls in JavaScript are costly compared to simple loop bodies.

And so it goes with a lot of techniques that help improve the clarity of a program. Abstractions add layers between the raw things the computer is doing and the concepts we are working with and thus cause the machine to perform more work.

Fortunately, most computers are insanely fast. If you are processing a modest set of data or doing something that has to happen only on a human time scale, then it does not matter whether you wrote a pretty solution that takes half a millisecond or a super-optimized solution that takes a tenth of a millisecond.

It is helpful to roughly keep track of how often a piece of your program is going to run If you have a loop inside a loop (either directly or through the outer loop calling a function that ends up performing the inner loop), the code inside the inner loop will end up running N×M times.

This can add up to large numbers, and when a program is slow, the problem can often be traced to only a small part of the code, which sits inside an inner loop.

Exercises

Flattening

my solution (both good)

1
2
3
4
5
6
7
8
9
function flatten(arr) {
return arr.reduce(function handleFlatten(flattenArr, item) {
return flattenArr.concat(item);
}, []);
}

// testing
var array = [[1, 2, 3], [4, 5], [6]];
flatten(array);

book’s standard solution (as same as mine)

1
2
3
4
5
var arrays = [[1, 2, 3], [4, 5], [6]];

console.log(arrays.reduce(function(flat, current) {
return flat.concat(current);
}, []));

Mother-child age difference

my solution (not understand the subject,unfinishied)

book’s standard solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function average(array) {
function plus(a, b) { return a + b; }
return array.reduce(plus) / array.length;
}

var byName = {};
ancestry.forEach(function(person) {
byName[person.name] = person;
});

var differences = ancestry.filter(function(person) {
return byName[person.mother] !== null;
}).map(function(person) {
return person.born - byName[person.mother].born;
});

console.log(average(differences));
// → 31.2

Historical life expectancy

my solution (not understand the subject, unfinishied)

book’s standard solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function average(arr) {
function plus(a, b) {
return a + b;
}

return arr.reduce(plus) / arr.length;
}

function groupBy(arr, groupOf) {
var groups = {};
var groupName;

arr.forEach(function(element) {
groupName = groupOf(element);
if (groupName in groups) {
groups[groupName].push(element);
} else {
groups[groupName] = [element];
}
});

return groups;
}

var byCentury = groupBy(ancestry, function(person) {
return Math.ceil(person.died / 100);
});

for (var century in byCentury) {
var ages = byCentury[century].map(function(person) {
return person.died - person.born;
});
console.log(century + ": " + average(ages));
}
// → 16: 43.5
// 17: 51.2
// 18: 52.8
// 19: 54.8
// 20: 84.7
// 21: 94

Every and then some

my solution (not consider forEach receive a function, so return false can’t end the outer forEach function)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function every(arr, func) {
arr.forEach(function (item) {
if (!func(item)) {
return false;
}
})
return true;
}

function some(arr, func) {
arr.forEach(function (item) {
if (func(item)) {
return true;
}
});
return false;
}

// testing
console.log(every([NaN, NaN, NaN], isNaN));
// → true
console.log(every([NaN, NaN, 4], isNaN));
// → false
console.log(some([NaN, 3, 4], isNaN));
// → true
console.log(some([2, 3, 4], isNaN));
// → false

book’s standard solution (after my optimization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function every(arr, predicate) {
var i;
var len = arr.length;

for (i = 0; i < len; i++) {
if (!predicate(arr[i])) {
return false;
}
}

return true;
}

function some(arr, predicate) {
var i;
var len = arr.length;

for (i = 0; i < len; i++) {
if (predicate(arr[i])) {
return true;
}
}

return false;
}

console.log(every([NaN, NaN, NaN], isNaN));
// → true
console.log(every([NaN, NaN, 4], isNaN));
// → false
console.log(some([NaN, 3, 4], isNaN));
// → true
console.log(some([2, 3, 4], isNaN));
// → false

The Secret Life of Objects

History

There are several useful concepts, most importantly that of encapsulation (distinguishing between internal complexity and external interface).

This chapter describes JavaScript’s rather eccentric take on objects and the way they relate to some classical object-oriented techniques.

Constructors

A more convenient way to create objects that derive from some shared prototype is to use a constructor.

An object created with new is said to be an instance of its constructor.

Rememeber capitalize the first letter of the constructor name.

1
2
3
4
5
6
7
8
function Rabbit(type) { 
this.type = type;
}

var killerRabbit = new Rabbit("killer");
var blackRabbit = new Rabbit("black");
console.log(blackRabbit.type);
// → black

(*)Object.defineProperty & object.hasOwnProperty()

All properties that we create by simply assigning to them are enumerble. The standard properties in Object.prototype are all nonenumerable, which is why they do not show up in such a for/in loop.

It is possible to defined our own nonenumerable properties by using the Object.defineProperty function, which allows us to control the type of property we are creating.

1
2
3
4
5
6
7
8
9
Object.defineProperty(Object.prototype , "hiddenNonsense", {
enumerable: false,
value: "hi"
});
for (var name in map) {
console.log(name);
}
// → pizza
// → touched tree console.log(map.hiddenNonsense); // → hi

hasOwnProperty tells us whether the object itself has the property, without looking at its prototypes. This is often a more useful piece of information than what the in operator gives us.

1
2
3
4
5
for (var name in map) {
if (map.hasOwnProperty(name)) {
// ... this is an own property
}
}

(*)Object.create(null)

we would actually prefer to have objects without prototypes. We saw the Object.create function, which allows us to create an object with a specific prototype.

You are allowed to pass null as the prototype to create a fresh object with no prototype:

1
2
3
4
var map = Object.create(null); 
map["pizza"] = 0.069;
console.log("toString" in map); // → false
console.log("pizza" in map); // → true

Polymorphism

When you call the String function, which converts a value to a string, on an object, it will call the toString method on that object to try to create a meaningful string to return.

I mentioned that some of the standard prototypes define their own version of toString so they can create a string that contains more useful information than “[object Object]”.

Polymorphic code can work with values of different shapes.

(*)Inheritance

1
2
3
4
5
6
7
8
function Person(name) {
this.name = name;
}

var Student = Object.create(Person.prototype);
Student.prototype.doHomework = function () {
console.log('work, work, work');
};

Inheritance is a fundamental part of the object-oriented tradition, alongside encapsulation and polymorphism. But while the latter two are now generally regarded as wonderful ideas, inheritance is somewhat controversial.

The main reason for this is that it is often confused with polymorphism, sold as a more powerful tool than it really is, and subsequently overused in all kinds of ugly ways.

Whereas encapsulation and polymorphism can be used to separate pieces of code from each other, reducing the tangledness of the overall program, inheritance fundamentally ties types together, creating more tangle.

I am not going to tell you to avoid inheritance entirely—I use it regularly in my own programs.

But you should see it as a slightly dodgy trick that can help you define new types with little code, not as a grand principle of code organization.

A preferable way to extend types is through composition.

Exercises

A vector type

my solution (犯了一个错误,在 prototype 上加属性的话只能加定值,不能加变量(在这里我的值就是 Math.pow(undefined) -> NaN ),而标准答案写的非常正确)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function Vector(x, y) {
this.x = x;
this.y = y;
}

Vector.prototype.plus = function (other) {
this.x += other.x;
this.y += other.y;

return this;
}


/* can also write like this
Vector.prototype.plus = function (vector) {
return new Vector(this.x + other.x, this.y + other.y);
}
*/

Vector.prototype.minus = function (other) {
this.x -= other.x;
this.y -= other.y;

return this;
}

Vector.prototype.length = Math.pow((Math.pow(this.x, 2) + Math.pow(this.y, 2)), 0.5);

console.log(new Vector(1, 2).plus(new Vector(2, 3)));
// → Vector{x: 3, y: 5}
console.log(new Vector(1, 2).minus(new Vector(2, 3)));
// → Vector{x: -1, y: -1}
console.log(new Vector(3, 4).length);
// → NaN (not the correct answer -> 5)

book’s standard solution (Object.defineProperty)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function Vector(x, y) {
this.x = x;
this.y = y;
}

Vector.prototype.plus = function (vector) {
this.x += vector.x;
this.y += vector.y;

return this;
};
/*
or:

Vector.prototype.plus = function (other) {
return new Vector(this.x + other.x, this.y + other.y);
};
*/

Vector.prototype.minus = function (other) {
this.x -= other.x;
this.y -= other.y;

return this;
};

Object.defineProperty(Vector.prototype, 'length', {
get: function () {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
});

console.log(new Vector(1, 2).plus(new Vector(2, 3)));
// → Vector{x: 3, y: 5}
console.log(new Vector(1, 2).minus(new Vector(2, 3)));
// → Vector{x: -1, y: -1}
console.log(new Vector(3, 4).length);
// → 5

Another cell

my solution (not finish, the topic is puzzled)

book’s standard solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function StretchCell(inner, width, height) {
this.inner = inner;
this.width = width;
this.height = height;
}

StretchCell.prototype.minWidth = function() {
return Math.max(this.width, this.inner.minWidth());
};
StretchCell.prototype.minHeight = function() {
return Math.max(this.height, this.inner.minHeight());
};
StretchCell.prototype.draw = function(width, height) {
return this.inner.draw(width, height);
};

var sc = new StretchCell(new TextCell("abc"), 1, 2);
console.log(sc.minWidth());
// → 3
console.log(sc.minHeight());
// → 2
console.log(sc.draw(3, 2));
// → ["abc", " "]

Sequence interface

book’s standard solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// I am going to use a system where a sequence object has two methods:
//
// * next(), which returns a boolean indicating whether there are more
// elements in the sequence, and moves it forward to the next
// element when there are.
//
// * current(), which returns the current element, and should only be
// called after next() has returned true at least once.

function logFive(sequence) {
for (var i = 0; i < 5; i++) {
if (!sequence.next())
break;
console.log(sequence.current());
}
}

function ArraySeq(array) {
this.pos = -1;
this.array = array;
}
ArraySeq.prototype.next = function() {
if (this.pos >= this.array.length - 1) {
return false;
}
this.pos += 1;

return true;
};
ArraySeq.prototype.current = function() {
return this.array[this.pos];
};

function RangeSeq(from, to) {
this.pos = from - 1;
this.to = to;
}
RangeSeq.prototype.next = function() {
if (this.pos >= this.to) {
return false;
}
this.pos++;
return true;
};
RangeSeq.prototype.current = function() {
return this.pos;
};

logFive(new ArraySeq([1, 2]));
// → 1
// → 2
logFive(new RangeSeq(100, 1000));
// → 100
// → 101
// → 102
// → 103
// → 104

// This alternative approach represents the empty sequence as null,
// and gives non-empty sequences two methods:
//
// * head() returns the element at the start of the sequence.
//
// * rest() returns the rest of the sequence, or null if there are no
// elemements left.
//
// Because a JavaScript constructor can not return null, we add a make
// function to constructors of this type of sequence, which constructs
// a sequence, or returns null if the resulting sequence would be
// empty.

function logFive2(sequence) {
for (var i = 0; i < 5 && sequence != null; i++) {
console.log(sequence.head());
sequence = sequence.rest();
}
}

function ArraySeq2(array, offset) {
this.array = array;
this.offset = offset;
}
ArraySeq2.prototype.rest = function() {
return ArraySeq2.make(this.array, this.offset + 1);
};
ArraySeq2.prototype.head = function() {
return this.array[this.offset];
};
ArraySeq2.make = function(array, offset) {
if (offset == null) offset = 0;
if (offset >= array.length) {
return null;
}
else {
return new ArraySeq2(array, offset);
}
};

function RangeSeq2(from, to) {
this.from = from;
this.to = to;
}
RangeSeq2.prototype.rest = function() {
return RangeSeq2.make(this.from + 1, this.to);
};
RangeSeq2.prototype.head = function() {
return this.from;
};
RangeSeq2.make = function(from, to) {
if (from > to) {
return null;
}
else {
return new RangeSeq2(from, to);
}
};

logFive2(ArraySeq2.make([1, 2]));
// → 1
// → 2
logFive2(RangeSeq2.make(100, 1000));
// → 100
// → 101
// → 102
// → 103
// → 104

Bugs and Error Handling

Debugging is twice as hard as writing the code in the first place.
Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.”

—Brian Kernighan and P.J. Plauger, The Elements of Programming Style

Flaws in a program are usually called bugs. Bugs can be:

  • programmer errors
  • problems in other systems that the program interacts with

Some bugs are immediately apparent, while others are subtle and might remain hidden in a system for years.

Often, problems surface only when a program encounters a situation that the programmer didn’t originally consider.

Sometimes such situations are unavoidable:

  • When the user is asked to input their age and types orange, this puts our program in a difficult position. The situation has to be anticipated and handled somehow.

Strict mode

1
2
3
4
5
6
7
8
function canYouSpotTheProblem() {
"use strict";
for (counter = 0; counter < 10; counter++)
console.log("Happy happy");
}

canYouSpotTheProblem();
// → ReferenceError: counter is not defined
1
2
3
4
5
6
7
"use strict";
function Person(name) {
this.name = name;
}
// Oops , forgot 'new '
var ferdinand = Person("Ferdinand");
// → TypeError: Cannot set property 'name' of undefined

In short, putting a “use strict” at the top of your program rarely hurts and might help you spot a problem.

Testing

Fortunately, there exist pieces of software that help you build and run collections of tests (test suites) by providing a language (in the form of functions and methods) suited to expressing tests and by outputting informative information when a test fails.

These are called testing frameworks.

(*)Debugging

This is where you must resist the urge to start making random changes to the code. Instead:

  1. Think. Analyze what is happening and come up with a theory of why it might be happening.
  2. Make additional observations to test this theory—or, if you don’t yet have a theory, make additional observations that might help you come up with one.
  3. Putting a few strategic console.log calls into the program is a good way to get additional information about what the program is doing.

(*)Exceptions

When a function cannot proceed normally, what we would like to do is just stop what we are doing and immediately jump back to a place that knows how to handle the problem. This is what exception handling does.

Raising an exception somewhat resembles a supercharged return from a function:

resembles 像,类似于

  • it jumps out of not just the current function
  • but also out of its callers, all the way down to the first call that started the current execution.

This is called unwinding the stack.

supercharged 超级有效的,增压的

Their power lies in the fact that

  • you can set “obstacles” along the stack to catch the exception as it is zooming down.
  • Then you can do something with it, after which the program continues running at the point where the exception was caught.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function promptDirection(question) {
var result = prompt(question, '').toLowerCase();

if (result === 'left') {
return 'L';
} else if (result === 'right') {
return 'R';
}
throw new Error('Invalid direction: ' + result);
}

function look() {
if (promptDirection('Which way?') === 'L') {
return 'a house';
}
return 'two angry bears';
}

The throw keyword is used to raise an exception.

When the code in the try block causes an exception to be raised, the catch block is evaluated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// test1
try {
console.log('You see', look());
} catch (e) {
console.log('Something went wrong: ' + e);
}

console.log('the exception don\'t influence normal flow');
// Something went wrong: Error: Invalid direction: 1
// the exception don't influence normal flow

// test2
look();
console.log('the exception don\'t influence normal flow');
// input 1 to console: Uncaught Error: Invalid direction: 1, break off and no console.log

In this case, we used the Error constructor to create our exception value. This is a standard JavaScript constructor that creates an object with a message property.

In modern JavaScript environments, instances of this constructor also gather information about the call stack that existed when the exception was created, a so-called stack trace. This information is stored in the stack property and can be helpful when trying to debug a problem: it tells us the precise function where the problem occurred and which other functions led up to the call that failed.

Note that the function look completely ignores the possibility that promptDirection might go wrong. This is the big advantage of exceptions:

  • error-handling code is necessary only at the point where the error occurs and at the point where it is handled. The functions in between can forget all about it.

(*)finally: cleaning up after exceptions

Consider: After this function finishes, context restores this variable to its old value.

1
2
3
4
5
6
7
8
9
10
11
12
var context = null;

function withContext(newContext, body) {
var oldContext = context;
var result;

context = newContext;
result = body();
context = oldContext;

return result;
}

What if body raises an exception? In that case, the call to withContext will be thrown off the stack by the exception, and context will never be set back to its old value.

A finally block means “No matter what happens, run this code after trying to run the code in the try block”.

If a function has to clean something up, the cleanup code should usually be put into a finally block.

1
2
3
4
5
6
7
8
9
10
function withContext(newContext, body) {
var oldContext = context;

context = newContext;
try {
return body();
} finally {
context = oldContext;
}
}

Note that we no longer have to store the result of body (which we want to return) in a variable. Even if we return directly from the try block, the finally block will be run.

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
withContext(5, function () {
if (context < 10) {
throw new Error('Not enough context!');
}
});
} catch (e) {
console.log('Ignoring: ' + e);
}
// → Ignoring: Error: Not enough context!

console.log(context);
// → null

Selective catching

problem

  • For: programmer mistakes or problems that the program cannot possibly handle

just letting the error go through is often okay.
An unhandled exception is a reasonable way to signal a broken program, and the JavaScript console will, on modern browsers, provide you with some information about which function calls were on the stack when the problem occurred.

  • For: problems that are expected to happen during routine use

crashing with an unhandled exception is not a very friendly response.

JavaScript doesn’t provide direct support for selectively catching exceptions: either you catch them all or you don’t catch any.

This makes it very easy to assume that the exception you get is the one you were thinking about when you wrote the catch block.

But it might not be.

Some other assumption might be violated, or you might have introduced a bug somewhere that is causing an exception.

Here is an example, which attempts to keep on calling promptDirection until it gets a valid answer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function promptDirection(question) {
var result = prompt(question, '').toLowerCase();

if (result === 'left') {
return 'L';
} else if (result === 'right') {
return 'R';
}
throw new Error('Invalid direction: ' + result);
}

for (;;) {
try {
var dir = promtDirection("Where?"); // ← typo! misspelled promptDirection

console.log("You chose ", dir);
break;
} catch (e) {
console.log("Not a valid direction. Try again.");
}
}

// infinite loop: "Not a valid direction. Try again."...

The for (;;) construct is a way to intentionally create a loop that doesn’t terminate on its own. We break out of the loop only when a valid direction is given (launch throw).

But we misspelled promptDirection, which will result in an “undefined variable” error. Because the catch block completely ignores its exception value, assuming it knows what the problem is, it wrongly treats the variable error as indicating bad input.

Not only does this cause an infinite loop, but it also “buries” the useful error message about the misspelled variable.

specific exceptions

As a general rule, don’t blanket catch exceptions unless it is for the purpose of “routing” them somewhere—for example, over the network to tell another system that our program crashed. And even then, think carefully about how you might be hiding information.

So we want to catch a specific kind of exception. We can do this by checking in the catch block whether the exception we got is the one we are interested in and by rethrowing it otherwise.

1
2
3
4
5
6
7
8
9
10
try {

} catch (e) {
if (e === condition) {
doSometing();
} else {
console.log("Not a valid direction. Try again.");
throw e;
}
}

But how do we recognize an exception?

Of course, we could match its message property against the error message we happen to expect.

1
2
3
4
5
6
7
8
9
try {

} catch (e) {
if (e.message === message) {

} else {

}
}

But that’s a shaky way to write code–we’d be using information that’s intended for human consumption (the message) to make a programmatic decision. As soon as someone changes (or translates) the message, the code will stop working.

Rather, let’s define a new type of error and use instanceof to identify it.

1
2
3
4
5
6
function InputError(message) {
this.message = message;
this.stack = (new Error()).stack;
}
InputError.prototype = Object.create(Error.prototype);
InputError.prototype.name = 'InputError';

Now promptDirection can throw such an error.

1
2
3
4
5
6
7
8
9
10
11
function promptDirection(question) {
var result = prompt(question, '').toLowerCase();

if (result === 'left') {
return 'L';
}
if (result === 'right') {
return 'R';
}
throw new InputError('Invalid direction: ' + result);
}

And the loop can catch it more carefully.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (;;) {
try {
var dir = promptDirection("Where?");
console.log("You chose ", dir);
break;
} catch (e) {
if (e instanceof InputError) {
console.log("Not a valid direction. Try again.");
}
else {
throw e;
}
}
}

This will catch only instances of InputError and let unrelated exceptions through. If you reintroduce the typo, the undefined variable error will be properly reported.

(*)When use exceptions?

Use it whenever code you are running might throw an exception. Remember that you can throw your own errors* — most of the try…catch stuff I use is for catching my own exceptions.

https://stackoverflow.com/questions/7148019/when-should-you-use-try-catch-in-JavaScript#answer-7148114

The try-catch statement should be executed only on sections of code where you suspect errors might occur, and due to the overwhelming number of possible circumstances, you cannot completely verify if an error will take place, or when it will do so.
In the latter case, it would be appropriate to use try-catch.

Assertions

Assertions are a tool to do basic sanity checking for programmer errors. Consider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function AssertionFailed(message) {
this.message = message;
}
AssertionFailed.prototype = Object.create(Error.prototype);

function assert(test, message) {
if (!test) {
throw new AssertionFailed(message);
}
}

function lastElement(arr) {
assert(arr.length > 0, "empty array in lastElement");

return arr[arr.length - 1];
}

This provides a compact way to enforce expectations, helpfully blowing up the program if the stated condition does not hold.

For instance, the lastElement function, which fetches the last element from an array, would return undefined on empty arrays if the assertion was omitted. Fetching the last element from an empty array does not make much sense, so it is almost certainly a programmer error to do so.

Assertions are a way to make sure mistakes cause failures at the point of the mistake, rather than silently producing nonsense values that may go on to cause trouble in an unrelated part of the system.

Summary

Mistakes and bad input are facts of life. Bugs in programs need to be found and fixed. They can become easier to notice by having automated test suites and adding assertions to your programs.

Problems caused by factors outside the program’s control should usually be handled gracefully. Sometimes,when the problem can be handled locally, special return values are a sane way to track them. Otherwise, exceptions are preferable.

Throwing an exception causes the call stack to be unwound until the next enclosing try/catch block or until the bottom of the stack. The exception value will be given to the catch block that catches it, which should verify that it is actually the expected kind of exception and then do something with it.

To deal with the unpredictable control flow caused by exceptions, finally blocks can be used to ensure a piece of code is always run when a block finishes.

Exercises

Retry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function MultiplicatorUnitFailure() {}
function primitiveMultiply(a, b) {
if (Math.random() < 0.5) {
return a * b;
}
throw new MultiplicatorUnitFailure();
}

function reliableMultiply(a, b) {
for (;;) {
try {
return primitiveMultiply(a, b);
} catch (e) {
if (!(e instanceof MultiplicatorUnitFailure)) {
throw e;
}
}
}
}

console.log(reliableMultiply(8, 8));
// → 64

The locked box

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function withBoxUnlocked(body) {
var locked = box.locked;
if (!locked) {
return body();
}

box.unlock();
try {
return body();
} finally {
box.lock();
}
}

withBoxUnlocked(function() {
box.content.push('gold piece');
});

try {
withBoxUnlocked(function() {
throw new Error('Pirates on the horizon! Abort!');
});
} catch (e) {
console.log('Error raised:', e);
}

console.log(box.locked);
// → true

Regular Expressions

  • Regular expressions are a way to describe patterns in string data.

They form a small, separate language that is part of JavaScript and many other languages and tools.

  • Regular expressions are both terribly awkward and extremely useful.

Their syntax is cryptic, and the programming interface JavaScript provides for them is clumsy.

But they are a powerful tool for inspecting and processing strings. Properly understanding regular expressions will make you a more effective programmer.

  • Testing for matches
1
2
3
4
console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false
  • Matching a set of characters
1
2
3
4
console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true

There are a number of common character groups that have their own built-in shortcuts. Digits are one of them: \d means the same thing as [0-9].

  • \d Any digit character
  • \D A character that is not a digit
  • \w An alphanumeric character (“word character”)
  • \W A nonalphanumeric character
  • \s Any whitespace character (space, tab, newline, and similar)
  • \S A nonwhitespace character
  • . Any character except for newline

These backslash codes can also be used inside square brackets. For example, [\d.] means any digit or a period character. But note that the period itself, when used between square brackets, loses its special meaning. The same goes for other special characters, such as +.

To invert a set of characters—that is, to express that you want to match any character except the ones in the set—you can write a caret (^) character after the opening bracket.

1
2
3
4
5
var notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true

Repeating parts of a pattern

We now know how to match a single digit. What if we want to match a whole number—a sequence of one or more digits?

When you put a plus sign (+) after something in a regular expression, it indicates that the element may be repeated more than once. Thus, /\d+/ matches one or more digit characters.

1
2
3
4
console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false

The star () has a similar meaning but also allows the pattern to match zero times*.

1
2
3
4
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true

A question mark makes a part of a pattern “optional”, meaning it may occur zero or one time.

1
2
3
4
5
var neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true

To indicate that a pattern should occur a precise number of times, use curly braces. Putting {4} after an element, for example, requires it to occur exactly four times. It is also possible to specify a range this way: {2,4} means the element must occur at least twice and at most four times.

1
2
3
var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("30-1-2003 8:45"));
// → true

You can also specify open-ended ranges when using curly braces by omitting the number after the comma. So {5,} means five or more times.

Grouping subexpressions

To use an operator like * or + on more than one element at a time, you can use parentheses. A part of a regular expression that is enclosed in parentheses counts as a single element as far as the operators following it are concerned.

1
2
3
var cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true

The i at the end of the expression in the previous example makes this regular expression case insensitive, allowing it to match the uppercase B in the input string, even though the pattern is itself all lowercase.

Matches and groups

The test method is the absolute simplest way to match a regular expression. It tells you only whether it matched and nothing else.

Regular expressions also have an exec (execute) method that will return null if no match was found and return an object with information about the match otherwise.

1
2
3
4
5
var match = /\d+/.exec("one two 100");
console.log(match);
// → ["100", index: 8, input: "one two 100"]
console.log(match.index);
// → 8

When the regular expression contains subexpressions grouped with parentheses, the text that matched those groups will also show up in the array. The whole match is always the first element. The next element is the part matched by the first group (the one whose opening parenthesis comes first in the expression), then the second group, and so on.

1
2
3
var quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]

When a group does not end up being matched at all (for example, when followed by a question mark), its position in the output array will hold undefined. its position in the output array will hold undefined. Similarly, when a group is matched multiple times, only the last match ends up in the array.

1
2
3
4
console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]

Groups can be useful for extracting parts of a string. If we don’t just want to verify whether a string contains a date but also extract it and construct an object that represents it, we can wrap parentheses around the digit patterns and directly pick the date out of the result of exec.

Choice patterns

We could write three regular expressions and test them in turn, but there is a nicer way. The pipe character (|) denotes a choice between the pattern to its left and the pattern to its right. So I can say this:

1
2
3
4
5
var animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false

The replace method

String values have a replace method, which can be used to replace part of the string with another string.

1
2
console.log("papa".replace("p", "m"));
// → mapa

The first argument can also be a regular expression, in which case the first match of the regular expression is replaced. When a g option (for global) is added to the regular expression, all matches in the string will be replaced, not just the first.

1
2
3
4
console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar

The dollar 1 and dollar 2 in the replacement string refer to the parenthesized groups in the pattern. 1 is replaced by the text that matched against the first group, dollar 2 by the second, and so on, up to dollar 9. The whole match can be referred to with $&.

1
2
3
4
5
6
console.log(
"Hopper, Grace\nMcCarthy, John\nRitchie, Dennis"
.replace(/([\w ]+), ([\w ]+)/g, "$2 $1"));
// → Grace Hopper
// John McCarthy
// Dennis Ritchie

It is also possible to pass a function, rather than a string, as the second argument to replace. For each replacement, the function will be called with the matched groups (as well as the whole match) as arguments, and its return value will be inserted into the new string.

1
2
3
4
5
var s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g, function(str) {
return str.toUpperCase();
}));
// → the CIA and FBI

Greed

change + or * to ?

1
2
3
4
5
6
7
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}

function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}

A lot of bugs in regular expression programs can be traced to unintentionally using a greedy operator where a nongreedy one would work better. When using a repetition operator, consider the nongreedy variant first.

The search method

The indexOf method on strings cannot be called with a regular expression. But there is another method, search, which does expect a regular expression. Like indexOf, it returns the first index on which the expression was found, or -1 when it wasn’t found.

1
2
3
4
console.log("  word".search(/\S/));
// → 2
console.log(" ".search(/\S/));
// → -1

Unfortunately, there is no way to indicate that the match should start at a given offset (like we can with the second argument to indexOf), which would often be useful.

The lastIndex property

The exec method similarly does not provide a convenient way to start searching from a given position in the string. But it does provide an inconvenient way.

1
2
3
4
5
6
7
var pattern = /y/g;
pattern.lastIndex = 3;
var match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

Looping over matches

1
2
3
4
5
6
7
8
var input = "A string with 3 numbers in it... 42 and 88.";
var number = /\b(\d+)\b/g;
var match;
while (match = number.exec(input))
console.log("Found", match[1], "at", match.index);
// → Found 3 at 14
// Found 42 at 33
// Found 88 at 40

Parsing an INI file

searchengine=http://www.google.com/search?q=$1
spitefulness=9.7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function parseINI(string) {
// Start with an object to hold the top-level fields
var currentSection = {name: null, fields: []};
var categories = [currentSection];

string.split(/\r?\n/).forEach(function(line) {
var match;
if (/^\s*(;.*)?$/.test(line)) {
return;
} else if (match = line.match(/^\[(.*)\]$/)) {
currentSection = {name: match[1], fields: []};
categories.push(currentSection);
} else if (match = line.match(/^(\w+)=(.*)$/)) {
currentSection.fields.push({name: match[1],
value: match[2]});
} else {
throw new Error("Line '" + line + "' is invalid.");
}
});

return categories;
}

International characters

Because of JavaScript’s initial simplistic implementation and the fact that this simplistic approach was later set in stone as standard behavior, JavaScript’s regular expressions are rather dumb about characters that do not appear in the English language. For example, as far as JavaScript’s regular expressions are concerned, a “word character” is only one of the 26 characters in the Latin alphabet (uppercase or lowercase) and, for some reason, the underscore character. Things like é or β, which most definitely are word characters, will not match \w (and will match uppercase \W, the nonword category).

By a strange historical accident, \s (whitespace) does not have this problem and matches all characters that the Unicode standard considers whitespace, including things like the nonbreaking space and the Mongolian vowel separator.

Some regular expression implementations in other programming languages have syntax to match specific Unicode character categories, such as “all uppercase letters”, “all punctuation”, or “control characters”. There are plans to add support for such categories to JavaScript, but it unfortunately looks like they won’t be realized in the near future.

Exercises

Regexp golf

Quoting style

Numbers again

Modules

A beginning programmer writes her programs like an ant builds her hill, one piece at a time, without thought for the bigger structure. Her programs will be like loose sand. They may stand for a while, but growing too big they fall apart.

Realizing this problem, the programmer will start to spend a lot of time thinking about structure. Her programs will be rigidly structured, like rock sculptures. They are solid, but when they must change, violence must be done to them.

The master programmer knows when to apply structure and when to leave things in their simple form. Her programs are like clay, solid yet malleable.

——Master Yuan-Ma, The Book of Programming

When looking at a larger program in its entirety, such a program can be made more readable if we have a larger unit of organization.

Objects as interfaces

1
2
3
4
5
6
7
8
9
10
11
var weekDay = function() {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
return {
name: function(number) { return names[number]; },
number: function(name) { return names.indexOf(name); }
};
}();

console.log(weekDay.name(weekDay.number("Sunday")));
// → Sunday

Layered interfaces

When designing an interface for a complex piece of functionality—sending email, for example—you often run into a dilemma.

  1. On the one hand, you do not want to overload the user of your interface with details. They shouldn’t have to study your interface for 20 minutes before they can send an email.
  2. On the other hand, you do not want to hide all the details either—when people need to do complicated things with your module, they should be able to.

Often the solution is to provide two interfaces:

  1. a detailed low-level one for complex situations and a simple high-level one for routine use.
  2. The second can usually be built easily using the tools provided by the first.

In the email module, the high-level interface could just be a function that takes a message, a sender address, and a receiver address and then sends the email. The low-level interface would allow full control over email headers, attachments, HTML mail, and so on.

Exercises

Month names

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var month = (function () {
var names = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

return {
name: function (number) { return names[number]; },
number: function (name) { return names.indexOf(name); }
};
}());


console.log(month.name(2));
// → March
console.log(month.number('November'));
// → 10

JavaScript and the Browser

The Web

The World Wide Web (not to be confused with the Internet as a whole) is a set of protocols and formats that allow us to visit web pages in a browser. The “Web” part in the name refers to the fact that such pages can easily link to each other, thus connecting into a huge mesh that users can move through.

To add content to the Web, all you need to do is connect a machine to the Internet, and have it listen on port 80, using the Hypertext Transfer Protocol (HTTP). This protocol allows other computers to request documents over the network.

Each document on the Web is named by a Uniform Resource Locator (URL), which looks something like this:

http://eloquentJavaScript.net/12_browser.html
| | | |
protocol server path

If you type the previous URL into your browser’s address bar, it will try to retrieve and display the document at that URL.

  1. First, your browser has to find out what address eloquentJavaScript.net refers to.
  2. Then, using the HTTP protocol, it makes a connection to the server at that address and asks for the resource /12_browser.html.

In the sandbox

Running programs downloaded from the Internet is potentially dangerous. You do not know much about the people behind most sites you visit, and they do not necessarily mean well. Running programs by people who do not mean well is how you get your computer infected by viruses, your data stolen, and your accounts hacked.

Isolating a programming environment in this way is called sandboxing, the idea being that the program is harmlessly playing in a sandbox. But you should imagine this particular kind of sandbox as having a cage of thick steel bars over it, which makes it somewhat different from your typical playground sandbox.

The hard part of sandboxing is allowing the programs enough room to be useful yet at the same time restricting them from doing anything dangerous. Lots of useful functionality, such as communicating with other servers or reading the content of the copy-paste clipboard, can also be used to do problematic, privacy-invading things.

The Document Object Model

The browser builds up a model of the document’s structure and then uses this model to draw the page on the screen.

This representation of the document is one of the toys that a JavaScript program has available in its sandbox. You can read from the model and also change it. It acts as a live data structure: when it is modified, the page on the screen is updated to reflect the changes.

Document structure

The global variable document gives us access to these objects. Its documentElement property refers to the object representing the tag. It also provides the properties head and body, which hold the objects for those elements.

Trees

We call a data structure a tree when it has a branching structure, has no cycles (a node may not contain itself, directly or indirectly), and has a single, well-defined “root”. In the case of the DOM, document.documentElement serves as the root.

Trees come up a lot in computer science. In addition to representing recursive structures such as HTML documents or programs, they are often used to maintain sorted sets of data because elements can usually be found or inserted more efficiently in a sorted tree than in a sorted flat array.

Each DOM node object has a nodeType property, which contains a numeric code that identifies the type of node.

  • Regular elements have the value 1
  • Attribute nodes have the value 2
  • Text nodes have the value 3

The standard

Using cryptic numeric codes to represent node types is not a very JavaScript-like thing to do. Later in this chapter, we’ll see that other parts of the DOM interface also feel cumbersome and alien.

The reason for this is that the DOM wasn’t designed for just JavaScript. Rather, it tries to define a language-neutral interface that can be used in other systems as well—not just HTML but also XML, which is a generic data format with an HTML-like syntax.

As an example of such poor integration, consider the childNodes property that element nodes in the DOM have. This property holds an array-like object, with a length property and properties labeled by numbers to access the child nodes. But it is an instance of the NodeList type, not a real array, so it does not have methods such as slice and forEach.

Moving through the tree

DOM nodes contain a wealth of links to other nearby nodes.

  • parentNode
  • childNodes
  • firstChild
  • lastChild
  • previousSibling
  • nextSibling

Finding elements

The complicating factor is that text nodes are created even for the whitespace between nodes. The example document’s body tag does not have just three children (h1 and two p elements) but actually has seven: those three, plus the spaces before, after, and between them.

Changing the document

  • removeChild
  • appendChild
  • insertBefore
  • replaceChild
1
2
3
4
5
6
7
8
<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
var paragraphs = document.body.getElementsByTagName("p");
document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

A node can exist in the document in only one place. Thus, inserting paragraph “Three” in front of paragraph “One” will first remove it from the end of the document and then insert it at the front, resulting in “Three/One/Two”. All operations that insert a node somewhere will, as a side effect, cause it to be removed from its current position (if it has one).

Creating nodes

  • createElement
  • createTextNode
  • appendChild
  • replaceChild
  • insertChild
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<p>The <img src="img/cat.png" alt="Cat"> in the
<img src="img/hat.png" alt="Hat">.</p>

<p><button onclick="replaceImages()">Replace</button></p>

<script>
function replaceImages() {
var images = document.body.getElementsByTagName("img");
for (var i = images.length - 1; i >= 0; i--) {
var image = images[i];
if (image.alt) {
var text = document.createTextNode(image.alt);
image.parentNode.replaceChild(text, image);
}
}
}
</script>

The loop that goes over the images starts at the end of the list of nodes. This is necessary because the node list returned by a method like getElementsByTagName (or a property like childNodes) is live. That is, it is updated as the document changes. If we started from the front, removing the first image would cause the list to lose its first element so that the second time the loop repeats, where i is 1, it would stop because the length of the collection is now also 1.

If you want a solid collection of nodes, as opposed to a live one, you can convert the collection to a real array by calling the array slice method on it.

1
2
3
4
5
var arrayish = {0: "one", 1: "two", length: 2};
var real = Array.prototype.slice.call(arrayish, 0);
real.forEach(function(elt) { console.log(elt); });
// → one
// two

or just use querySelector

Attributes

Some element attributes, such as href for links, can be accessed through a property of the same name on the element’s DOM object. This is the case for a limited set of commonly used standard attributes.

But HTML allows you to set any attribute you want on nodes. This can be useful because it allows you to store extra information in a document. If you make up your own attribute names, though, such attributes will not be present as a property on the element’s node. Instead, you’ll have to use the getAttribute and setAttribute methods to work with them.

1
2
3
4
5
6
7
8
9
10
<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>

<script>
var paras = document.body.getElementsByTagName("p");
Array.prototype.forEach.call(paras, function(para) {
if (para.getAttribute("data-classified") == "secret")
para.parentNode.removeChild(para);
});
</script>

I recommended prefixing the names of such made-up attributes with data- to ensure they do not conflict with any other attributes.

There is one commonly used attribute, class, which is a reserved word in the JavaScript language. For historical reasons—some old JavaScript implementations could not handle property names that matched keywords or reserved words—the property used to access this attribute is called className. You can also access it under its real name, “class”, by using the getAttribute and setAttribute methods.

Layout

For any given document, browsers are able to compute a layout, which gives each element a size and position based on its type and content. This layout is then used to actually draw the document.

The size and position of an element can be accessed from JavaScript. The offsetWidth and offsetHeight properties give you the space the element takes up in pixels. A pixel is the basic unit of measurement in the browser and typically corresponds to the smallest dot that your screen can display.

Similarly, clientWidth and clientHeight give you the size of the space inside the element, ignoring border width.

The most effective way to find the precise position of an element on the screen is the getBoundingClientRect method. It returns an object with top, bottom, left, and right properties, indicating the pixel positions of the sides of the element relative to the top left of the screen. If you want them relative to the whole document, you must add the current scroll position, found under the global pageXOffset and pageYOffset variables.

1
2
3
4
var a = $('p');
a.getBoundingClientRect()
// DOMRect {x: 114, y: -12298.90625, width: 730, height: 116, top: -12298.90625, …
a.getBoundingClientRect().x; // 114

Styling

“color: red; border: none”

1
2
3
This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.

JavaScript code can directly manipulate the style of an element through the node’s style property. This property holds an object that has properties for all possible style properties.

1
2
3
4
5
6
7
8
9
<p id="para" style="color: purple">
Pretty text
</p>

<script>
var para = document.getElementById("para");
console.log(para.style.color);
para.style.color = "magenta";
</script>

Query selectors

The querySelectorAll method, which is defined both on the document object and on element nodes, takes a selector string and returns an array-like object containing all the elements that it matches.

Unlike methods such as getElementsByTagName, the object returned by querySelectorAll is not live. It won’t change when you change the document.

Positioning and animating

The position style property influences layout in a powerful way.

By default it has a value of static, meaning the element sits in its normal place in the document.
When it is set to relative, the element still takes up space in the document, but now the top and left style properties can be used to move it relative to its normal place.
When position is set to absolute, the element is removed from the normal document flow—that is, it no longer takes up space and may overlap with other elements. Also, its top and left properties can be used to absolutely position it relative to the top-left corner of the nearest enclosing element whose position property isn’t static, or relative to the document if no such enclosing element exists.

Summary

JavaScript programs may inspect and interfere with the current document that a browser is displaying through a data structure called the DOM. This data structure represents the browser’s model of the document, and a JavaScript program can modify it to change the visible document.

The DOM is organized like a tree, in which elements are arranged hierarchically according to the structure of the document. The objects representing elements have properties such as parentNode and childNodes, which can be used to navigate through this tree.

The way a document is displayed can be influenced by styling, both by attaching styles to nodes directly and by defining rules that match certain nodes. There are many different style properties, such as color or display. JavaScript can manipulate an element’s style directly through its style property.

Exercises

Elements by tag name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!doctype html>
<script src="code/mountains.js"></script>
<script src="code/chapter/13_dom.js"></script>

<style>
/* Defines a cleaner look for tables */
table { border-collapse: collapse; }
td, th { border: 1px solid black; padding: 3px 8px; }
th { text-align: left; }
</style>

<body>
<script>
function buildTable(data) {
var table = document.createElement("table");

var fields = Object.keys(data[0]);
var headRow = document.createElement("tr");
fields.forEach(function(field) {
var headCell = document.createElement("th");
headCell.textContent = field;
headRow.appendChild(headCell);
});
table.appendChild(headRow);

data.forEach(function(object) {
var row = document.createElement("tr");
fields.forEach(function(field) {
var cell = document.createElement("td");
cell.textContent = object[field];
if (typeof object[field] == "number")
cell.style.textAlign = "right";
row.appendChild(cell);
});
table.appendChild(row);
});

return table;
}

document.body.appendChild(buildTable(MOUNTAINS));
</script>
</body>

Elements by tag name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!doctype html>
<script src="code/mountains.js"></script>
<script src="code/chapter/13_dom.js"></script>

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
spans.</p>

<script>
function byTagName(node, tagName) {
var found = [];
tagName = tagName.toUpperCase();

function explore(node) {
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType == document.ELEMENT_NODE) {
if (child.nodeName == tagName) {
found.push(child);
}
explore(child);
}
}
}

explore(node);
return found;
}

console.log(byTagName(document.body, "h1").length);
// → 1
console.log(byTagName(document.body, "span").length);
// → 3
var para = document.querySelector("p");
console.log(byTagName(para, "span").length);
// → 2
</script>

The cat’s hat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!doctype html>
<script src="code/mountains.js"></script>
<script src="code/chapter/13_dom.js"></script>

<body style="min-height: 200px">

<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">

<script>
var cat = document.querySelector("#cat");
var hat = document.querySelector("#hat");

var angle = 0, lastTime = null;
function animate(time) {
if (lastTime != null) {
angle += (time - lastTime) * 0.0015;
}
lastTime = time;

cat.style.top = (Math.sin(angle) * 50 + 80) + "px";
cat.style.left = (Math.cos(angle) * 200 + 230) + "px";
// By adding π to the angle, the hat ends up half a circle ahead of the cat
var hatAngle = angle + Math.PI;
hat.style.top = (Math.sin(hatAngle) * 50 + 80) + "px";
hat.style.left = (Math.cos(hatAngle) * 200 + 230) + "px";

requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>

</body>

Handling Events

Event handlers

A better mechanism is for the underlying system to give our code a chance to react to events as they occur. Browsers do this by allowing us to register functions as handlers for specific events.

1
2
3
4
5
6
<p>Click this document to activate the handler.</p>
<script>
addEventListener("click", function() {
console.log("You clicked!");
});
</script>

The addEventListener function registers its second argument to be called whenever the event described by its first argument occurs.

Events and DOM nodes

Giving a node an onclick attribute has a similar effect. But a node has only one onclick attribute, so you can register only one handler per node that way. The addEventListener method allows you to add any number of handlers, so you can’t accidentally replace a handler that has already been registered.

The removeEventListener method, called with arguments similar to as addEventListener, removes a handler.

1
2
3
4
5
6
7
8
9
<button>Act-once button</button>
<script>
var button = document.querySelector("button");
function once() {
console.log("Done.");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>

To be able to unregister a handler function, we give it a name (such as once) so that we can pass it to both addEventListener and removeEventListener.

Event objects

Though we have ignored it in the previous examples, event handler functions are passed an argument: the event object. This object gives us additional information about the event. For example, if we want to know which mouse button was pressed, we can look at the event object’s which property.

1
2
3
4
5
6
7
8
9
10
11
12
13
<button>Click me any way you want</button>
<script>
var button = document.querySelector("button");
button.addEventListener("mousedown", function(event) {
if (event.which == 1) {
console.log("Left button");
} else if (event.which == 2) {
console.log("Middle button");
} else if (event.which == 3) {
console.log("Right button");
}
});
</script>

The information stored in an event object differs per type of event. The object’s type property always holds a string identifying the event (for example “click” or “mousedown”).

Propagation

Event handlers registered on nodes with children will also receive some events that happen in the children. If a button inside a paragraph is clicked, event handlers on the paragraph will also receive the click event.

But if both the paragraph and the button have a handler, the more specific handler—the one on the button—gets to go first.
The event is said to propagate outward, from the node where it happened to that node’s parent node and on to the root of the document.

At any point, an event handler can call the stopPropagation method on the event object to prevent handlers “further up” from receiving the event.
This can be useful when, for example, you have a button inside another clickable element and you don’t want clicks on the button to activate the outer element’s click behavior.

The following example registers “mousedown” handlers on both a button and the paragraph around it. When clicked with the right mouse button, the handler for the button calls stopPropagation, which will prevent the handler on the paragraph from running. When the button is clicked with another mouse button, both handlers will run.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<p>A paragraph with a <button>button</button>.</p>
<script>
var para = document.querySelector("p");
var button = document.querySelector("button");

para.addEventListener("mousedown", function() {
console.log("Handler for paragraph.");
});
button.addEventListener("mousedown", function(event) {
console.log("Handler for button.");
if (event.which == 3) {
event.stopPropagation();
}
});
</script>

Most event objects have a target property that refers to the node where they originated. You can use this property to ensure that you’re not accidentally handling something that propagated up from a node you do not want to handle.

It is also possible to use the target property to cast a wide net for a specific type of event. For example, if you have a node containing a long list of buttons, it may be more convenient to register a single click handler on the outer node and have it use the target property to figure out whether a button was clicked, rather than register individual handlers on all of the buttons.

It called Event-Delegation, attention that the nodeName’s value is In the form of capital;

1
2
3
4
5
6
7
8
9
10
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", function(event) {
if (event.target.nodeName === "BUTTON") {
console.log("Clicked", event.target.textContent);
}
});
</script>

Default actions

Many events have a default action associated with them. If you click a link, you will be taken to the link’s target. If you press the down arrow, the browser will scroll the page down. If you right-click, you’ll get a context menu. And so on.

For most types of events, the JavaScript event handlers are called before the default behavior is performed. If the handler doesn’t want the normal behavior to happen, typically because it has already taken care of handling the event, it can call the preventDefault method on the event object.

This can be used to implement your own keyboard shortcuts or context menu. It can also be used to obnoxiously interfere with the behavior that users expect. For example, here is a link that cannot be followed:

1
2
3
4
5
6
7
8
<a href="https://developer.mozilla.org/">MDN</a>
<script>
var link = document.querySelector("a");
link.addEventListener("click", function(event) {
console.log("Nope.");
event.preventDefault();
});
</script>

Try not to do such things unless you have a really good reason to. For people using your page, it can be unpleasant when the behavior they expect is broken.

Depending on the browser, some events can’t be intercepted. On Chrome, for example, keyboard shortcuts to close the current tab (Ctrl-W or Command-W) cannot be handled by JavaScript.

Load event

When a page is closed or navigated away from (for example by following a link), a “beforeunload“ event fires.

The main use of this event is to prevent the user from accidentally losing work by closing a document.

Script execution timeline

There are various things that can cause a script to start executing. Reading a script tag is one such thing. An event firing is another. Chapter 13 discussed the requestAnimationFrame function, which schedules a function to be called before the next page redraw. That is yet another way in which a script can start running.

It is important to understand that even though events can fire at any time, no two scripts in a single document ever run at the same moment. If a script is already running, event handlers and pieces of code scheduled in other ways have to wait for their turn. This is the reason why a document will freeze when a script runs for a long time.

For cases where you really do want to do some time-consuming thing in the background without freezing the page, browsers provide something called web workers. A worker is an isolated JavaScript environment that runs alongside the main program for a document and can communicate with it only by sending and receiving messages.

Its global scope—which is a new global scope, not shared with the original script.

Setting timers

1
2
3
4
5
6
7
8
var bombTimer = setTimeout(function() {
console.log("BOOM!");
}, 500);

if (Math.random() < 0.5) { // 50% chance
console.log("Defused.");
clearTimeout(bombTimer);
}

A similar set of functions, setInterval and clearInterval are used to set timers that should repeat every X milliseconds.

1
2
3
4
5
6
7
8
var ticks = 0;
var clock = setInterval(function() {
console.log("tick", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("stop.");
}
}, 200);

Debouncing

Some types of events have the potential to fire rapidly, many times in a row (the “mousemove” and “scroll” events, for example). When handling such events, you must be careful not to do anything too time-consuming or your handler will take up so much time that interaction with the document starts to feel slow and choppy.

1
2
3
4
5
6
7
8
9
10
11
<textarea>Type something here...</textarea>
<script>
var textarea = document.querySelector("textarea");
var timeout;
textarea.addEventListener("keydown", function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
console.log("You stopped typing.");
}, 500);
});
</script>

Giving an undefined value to clearTimeout or calling it on a timeout that has already fired has no effect. Thus, we don’t have to be careful about when to call it, and we simply do so for every event.

We can use a slightly different pattern if we want to space responses so that they’re separated by at least a certain length of time but want to fire them during a series of events, not just afterward. For example, we might want to respond to “mousemove” events by showing the current coordinates of the mouse, but only every 250 milliseconds.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
function displayCoords(event) {
document.body.textContent =
"Mouse at " + event.pageX + ", " + event.pageY;
}

var scheduled = false, lastEvent;
addEventListener("mousemove", function(event) {
lastEvent = event;
if (!scheduled) {
scheduled = true;
setTimeout(function() {
scheduled = false;
displayCoords(lastEvent);
}, 250);
}
});
</script>

Summary

Event handlers make it possible to detect and react to events we have no direct control over. The addEventListener method is used to register such a handler.

Each event has a type (“keydown”, “focus”, and so on) that identifies it. Most events are called on a specific DOM element and then propagate to that element’s ancestors, allowing handlers associated with those elements to handle them.

When an event handler is called, it is passed an event object with additional information about the event. This object also has methods that allow us to stop further propagation (stopPropagation) and prevent the browser’s default handling of the event (preventDefault).

Pressing a key fires “keydown”, “keypress”, and “keyup” events. Pressing a mouse button fires “mousedown”, “mouseup”, and “click” events. Moving the mouse fires “mousemove” and possibly “mouseenter” and “mouseout” events.

Scrolling can be detected with the “scroll” event, and focus changes can be detected with the “focus” and “blur” events. When the document finishes loading, a “load” event fires on the window.

Only one piece of JavaScript program can run at a time (Web workers). Thus, event handlers and other scheduled scripts have to wait until other scripts finish before they get their turn.

Exercises

Censored keyboard

1
2
3
4
5
6
7
8
9
10
11
12
13
<!doctype html>

<input type="text">
<script>
var field = document.querySelector("input");
field.addEventListener("keydown", function(event) {
if (event.keyCode === "Q".charCodeAt(0) ||
event.keyCode === "W".charCodeAt(0) ||
event.keyCode === "X".charCodeAt(0)) {
event.preventDefault();
}
});
</script>

Mouse trail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!doctype html>

<style>
.trail { /* className for the trail elements */
position: absolute;
height: 6px; width: 6px;
border-radius: 3px;
background: teal;
}
body {
height: 300px;
}
</style>

<body>
<script>
var dots = [];
var i;
var node = document.createElement("div");

for (i = 0; i < 12; i++) {
node.className = "trail";
dots.push(node);
}

document.body.appendChild(dots);

addEventListener("mousemove", function(event) {
var currentDot = 0;
var dot = dots[currentDot];

dot.style.left = (event.pageX - 3) + "px";
dot.style.top = (event.pageY - 3) + "px";
currentDot = (currentDot + 1) % dots.length;
});
</script>
</body>

Tabs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!doctype html>

<div id="wrapper">
<div data-tabname="one">Tab one</div>
<div data-tabname="two">Tab two</div>
<div data-tabname="three">Tab three</div>
</div>
<script>
function asTabs(node) {
var tabs = [];
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType === 1) {
tabs.push(child);
}
}

var tabList = document.createElement("div");
tabs.forEach(function(tab, i) {
var button = document.createElement("button");
button.textContent = tab.getAttribute("data-tabname");
button.addEventListener("click", function() {
selectTab(i);
});
tabList.appendChild(button);
});
node.insertBefore(tabList, node.firstChild);

function selectTab(n) {
tabs.forEach(function(tab, i) {
if (i === n) {
tab.style.display = "";
} else {
tab.style.display = "none";
}
});
for (var i = 0; i < tabList.childNodes.length; i++) {
if (i === n) {
tabList.childNodes[i].style.background = "violet";
} else {
tabList.childNodes[i].style.background = "";
}
}
}
selectTab(0);
}
asTabs(document.querySelector("#wrapper"));
</script>

Drawing on Canvas

Browsers give us several ways to display graphics. The simplest way is to use styles to position and color regular DOM elements. This can get you quite far, as the game in the previous chapter showed. By adding partially transparent background images to the nodes, we can make them look exactly the way we want. It is even possible to rotate or skew nodes by using the transform style.

But we’d be using the DOM for something that it wasn’t originally designed for. Some tasks, such as drawing a line between arbitrary points, are extremely awkward to do with regular HTML elements.

There are two alternatives. The first is DOM-based but utilizes Scalable Vector Graphics (SVG), rather than HTML elements. Think of SVG as a dialect for describing documents that focuses on shapes rather than text. You can embed an SVG document in an HTML document, or you can include it through an img tag.

The second alternative is called a canvas. A canvas is a single DOM element that encapsulates a picture. It provides a programming interface for drawing shapes onto the space taken up by the node.

The main difference between a canvas and an SVG picture is that

  • in SVG the original description of the shapes is preserved so that they can be moved or resized at any time.
  • A canvas, on the other hand, converts the shapes to pixels (colored dots on a raster) as soon as they are drawn and does not remember what these pixels represent. The only way to move a shape on a canvas is to clear the canvas (or the part of the canvas around the shape) and redraw it with the shape in a new position.

SVG

This book will not go into SVG in detail, but I will briefly explain how it works. At the end of the chapter, I’ll come back to the trade-offs that you must consider when deciding which drawing mechanism is appropriate for a given application.

This is an HTML document with a simple SVG picture in it:

1
2
3
4
5
6
<p>Normal HTML here.</p>
<svg xmlns="http://www.w3.org/2000/svg">
<circle r="50" cx="50" cy="50" fill="red"/>
<rect x="120" y="5" width="90" height="90"
stroke="blue" fill="none"/>
</svg>

The xmlns attribute changes an element (and its children) to a different XML namespace. This namespace, identified by a URL, specifies the dialect that we are currently speaking. The and tags, which do not exist in HTML, do have a meaning in SVG—they draw shapes using the style and position specified by their attributes.

These tags create DOM elements, just like HTML tags. For example, this changes the element to be colored cyan instead:

1
2
var circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");

The canvas element

Canvas graphics can be drawn onto a element. You can give such an element width and height attributes to determine its size in pixels.

A new canvas is empty, meaning it is entirely transparent and thus shows up simply as empty space in the document.

The canvas tag is intended to support different styles of drawing. To get access to an actual drawing interface, we first need to create a context, which is an object whose methods provide the drawing interface.

There are currently two widely supported drawing styles: “2d” for two-dimensional graphics and “WebGL” for three-dimensional graphics through the OpenGL interface.

This book won’t discuss WebGL. We stick to two dimensions. But if you are interested in three-dimensional graphics, I do encourage you to look into WebGL. It provides a very direct interface to modern graphics hardware and thus allows you to render even complicated scenes efficiently, using JavaScript.

A context is created through the getContext method on the canvas element.

1
2
3
4
5
6
7
8
9
<p>Before canvas.</p>
<canvas width="120" height="60"></canvas>
<p>After canvas.</p>
<script>
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');
context.fillStyle = 'cyan';
context.fillRect(20, 20, 200, 200);
</script>

After creating the context object, the example draws a red rectangle 100 pixels wide and 50 pixels high, with its top-left corner at coordinates (10,10).

Just like in HTML (and SVG), the coordinate system that the canvas uses puts (0,0) at the top-left corner, and the positive y-axis goes down from there.

Filling and stroking

A similar method, strokeRect, draws the outline of a rectangle.

Neither method takes any further parameters. The color of the fill, thickness of the stroke, and so on are not determined by an argument to the method (as you might justly expect) but rather by properties of the context object.

1
2
3
4
5
6
7
8
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.strokeStyle = "blue";
cx.strokeRect(5, 5, 50, 50);
cx.lineWidth = 5;
cx.strokeRect(135, 5, 50, 50);
</script>

When no width or height attribute is specified, as in the previous example, a canvas element gets a default width of 300 pixels and height of 150 pixels.

Paths

A path is a sequence of lines. The 2D canvas interface takes a peculiar approach to describing such a path. It is done entirely through side effects. Paths are not values that can be stored and passed around. Instead, if you want to do something with a path, you make a sequence of method calls to describe its shape. (stroke method)

1
2
3
4
5
6
7
8
9
10
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
for (var y = 10; y < 100; y += 10) {
cx.moveTo(10, y);
cx.lineTo(90, y);
}
cx.stroke();
</script>

When filling a path (using the fill method), each shape is filled separately. A path can contain multiple shapes—each moveTo motion starts a new one. But the path needs to be closed (meaning its start and end are in the same position) before it can be filled. If the path is not already closed, a line is added from its end to its start, and the shape enclosed by the completed path is filled. (fill method)

1
2
3
4
5
6
7
8
9
10
<canvas></canvas>
<script>
var cx = document.querySelector('canvas').getContext('2d');
cx.beginPath();
cx.moveTo(50, 10);
cx.lineTo(10, 70);
cx.lineTo(90, 70);
cx.closePath();
cx.fill();
</script>

Curves

quadraticCurveTo

1
2
3
4
5
6
7
8
9
10
11
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control=(60,10) goal=(90,90)
cx.quadraticCurveTo(60, 10, 90, 90);
cx.lineTo(60, 10);
cx.closePath();
cx.stroke();
</script>

bezierCurveTo

The bezierCurveTo method draws a similar kind of curve. Instead of a single control point, this one has two—one for each of the line’s endpoints. Here is a similar sketch to illustrate the behavior of such a curve:

1
2
3
4
5
6
7
8
9
10
11
12
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control1=(10,10) control2=(90,10) goal=(50,90)
cx.bezierCurveTo(10, 10, 90, 10, 50, 90);
cx.lineTo(90, 10);
cx.lineTo(10, 10);
cx.closePath();
cx.stroke();
</script>

Such curves can be hard to work with—it’s not always clear how to find the control points that provide the shape you are looking for. Sometimes you can compute them, and sometimes you’ll just have to find a suitable value by trial and error.

Arcs—fragments of a circle—are easier to reason about.

1
2
3
4
5
6
7
8
9
10
11
12
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 10);
// control=(90,10) goal=(90,90) radius=20
cx.arcTo(90, 10, 90, 90, 20);
cx.moveTo(10, 10);
// control=(90,10) goal=(90,90) radius=80
cx.arcTo(90, 10, 90, 90, 80);
cx.stroke();
</script>

To draw a circle, you could use four calls to arcTo (each turning 90 degrees). But the arc method provides a simpler way. It takes a pair of coordinates for the arc’s center, a radius, and then a start and end angle.

1
2
3
4
5
6
7
8
9
10
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
// center=(50,50) radius=40 angle=0 to 7
cx.arc(50, 50, 40, 0, 7);
// center=(150,50) radius=40 angle=0 to ½π
cx.arc(150, 50, 40, 0, 0.5 * Math.PI);
cx.stroke();
</script>

Drawing a pie chart

To draw a pie chart, we draw a number of pie slices, each made up of an arc and a pair of lines to the center of that arc. We can compute the angle taken up by each arc by dividing a full circle (2π) by the total number of responses and then multiplying that number (the angle per response) by the number of people who picked a given choice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<canvas width="200" height="200"></canvas>
<script>
var results = [
{name: "Satisfied", count: 1043, color: "lightblue"},
{name: "Neutral", count: 563, color: "lightgreen"},
{name: "Unsatisfied", count: 510, color: "pink"},
{name: "No comment", count: 175, color: "silver"}
];
var cx = document.querySelector("canvas").getContext("2d");
var total = results.reduce(function(sum, choice) {
return sum + choice.count;
}, 0);
// Start at the top
var currentAngle = -0.5 * Math.PI;
results.forEach(function(result) {
var sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
// center=100,100, radius=100
// from current angle, clockwise by slice's angle
cx.arc(100, 100, 100,
currentAngle, currentAngle + sliceAngle);
currentAngle += sliceAngle;
cx.lineTo(100, 100);
cx.fillStyle = result.color;
cx.fill();
});
</script>

But a chart that doesn’t tell us what it means isn’t very helpful. We need a way to draw text to the canvas.

Text

A 2D canvas drawing context provides the methods fillText and strokeText. The latter can be useful for outlining letters, but usually fillText is what you need. It will fill the given text with the current fillColor.

1
2
3
4
5
6
7
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.font = "28px Georgia";
cx.fillStyle = "fuchsia";
cx.fillText("I can draw text, too!", 10, 50);
</script>

Images

In computer graphics, a distinction is often made between vector graphics and bitmap graphics. The first is what we have been doing so far in this chapter—specifying a picture by giving a logical description of shapes. Bitmap graphics, on the other hand, don’t specify actual shapes but rather work with pixel data (rasters of colored dots).

Images

Transformation

Summary

HTTP

The Hypertext Transfer Protocol(HTTP) is the mechanism through which data is requested and provided on the World Wide Web. This chapter describes the protocol in more detail and explains the way browser JavaScript has access to it.

The protocol

  • Status codes starting with a 2 indicate that the request succeeded.
  • Codes starting with 4 mean there was something wrong with the request. 404 is probably the most famous HTTP status code—it means that the resource that was requested could not be found.
  • Codes that start with 5 mean an error happened on the server and the request is not to blame.

Browsers and HTTP

A moderately complicated website can easily include anywhere from 10 to 200 resources. To be able to fetch those quickly, browsers will make several requests simultaneously, rather than waiting for the responses one at a time. Such documents are always fetched using GET requests.

HTML pages may include forms, which allow the user to fill out information and send it to the server. This is an example of a form:

1
2
3
4
5
<form method="GET" action="example/message.html">
<p>Name: <input type="text" name="name"></p>
<p>Message:<br><textarea name="message"></textarea></p>
<p><button type="submit">Send</button></p>
</form>

When the form element’s method attribute is GET (or is omitted), that query string is tacked onto the action URL, and the browser makes a GET request to that URL.

1
GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1

URL encoding

JavaScript provides the encodeURIComponent and decodeURIComponent functions to encode and decode this format.

1
2
3
4
console.log(encodeURIComponent("Hello & goodbye"));
// → Hello%20%26%20goodbye
console.log(decodeURIComponent("Hello%20%26%20goodbye"));
// → Hello & goodbye

POST

By convention, the GET method is used for requests that do not have side effects, such as doing a search. Requests that change something on the server, such as creating a new account or posting a message, should be expressed with other methods, such as POST.

Client-side software, such as a browser, knows that it shouldn’t blindly make POST requests but will often implicitly make GET requests—for example, to prefetch a resource it believes the user will soon need.

XMLHttpRequest

The interface through which browser JavaScript can make HTTP requests is called XMLHttpRequest.

When the XMLHttpRequest interface was added to Internet Explorer, it allowed people to do things with JavaScript that had been very hard before. For example, websites started showing lists of suggestions when the user was typing something into a text field. The script would send the text to the server over HTTP as the user typed. The server, which had some database of possible inputs, would match the database entries against the partial input and send back possible completions to show the user. This was considered spectacular—people were used to waiting for a full page reload for every interaction with a website.

Sending a request

On Mac:

1
open /Applications/Chromium.app --args --disable-web-security

To make a simple request, we create a request object with the XMLHttpRequest constructor and call its open and send methods.

1
2
3
4
5
var req = new XMLHttpRequest();
req.open("GET", "http://www.rayjune.me", false);
req.send(null);
console.log(req.responseText);
// → This is the content of data.txt

The open method configures the request. In this case, we choose to make a GET request for the example/data.txt file. URLs that don’t start with a protocol name (such as http:) are relative, which means that they are interpreted relative to the current document.

After opening the request, we can send it with the send method. The argument to send is the request body. For GET requests, we can pass null.

If the third argument to open was false, send will return only after the response to our request was received. We can read the request object’s responseText property to get the response body.

The other information included in the response can also be extracted from this object. The status code is accessible through the status property, and the human-readable status text is accessible through statusText. Headers can be read with getResponseHeader.

1
2
3
4
5
6
7
var req = new XMLHttpRequest();
req.open("GET", "http://www.rayjune.me", false);
req.send(null);
console.log(req.status, req.statusText);
// → 200 OK
console.log(req.getResponseHeader("content-type"));
// → text/plain

Asynchronous Requests

If we pass true as the third argument to open, the request is asynchronous. This means that when we call send, the only thing that happens right away is that the request is scheduled to be sent. Our program can continue, and the browser will take care of the sending and receiving of data in the background.

1
2
3
4
5
6
var req = new XMLHttpRequest();
req.open("GET", "http://www.rayjune.me", true);
req.addEventListener("load", function() {
console.log("Done:", req.status);
});
req.send(null);

Fetching XML Data

1
2
3
4
5
<fruits>
<fruit name="banana" color="yellow"/>
<fruit name="lemon" color="yellow"/>
<fruit name="cherry" color="red"/>
</fruits>
1
2
3
4
5
var req = new XMLHttpRequest();
req.open("GET", "example/fruit.xml", false);
req.send(null);
console.log(req.responseXML.querySelectorAll("fruit").length);
// → 3

XML documents can be used to exchange structured information with the server. Their form—tags nested inside other tags—lends itself well to storing most types of data, or at least better than flat text files.

The DOM interface is rather clumsy for extracting information, though, and XML documents tend to be verbose. It is often a better idea to communicate using JSON data, which is easier to read and write, both for programs and for humans.

1
2
3
4
5
var req = new XMLHttpRequest();
req.open("GET", "http://www.rayjune.me/JustToDo/package.json", false);
req.send(null);
console.log(JSON.parse(req.responseText));
// Object {name: "just-to-do", version: "1.0.0", description: " 就是去做 ", main: ".eslintrc.js", scripts: Object…}

HTTP sandboxing

Making HTTP requests in web page scripts once again raises concerns about security. The person who controls the script might not have the same interests as the person on whose computer it is running. More specifically, if I visit themafia.org, I do not want its scripts to be able to make a request to mybank.com, using identifying information from my browser, with instructions to transfer all my money to some random mafia account.

It is possible for websites to protect themselves against such attacks, but that requires effort, and many websites fail to do it. For this reason, browsers protect us by disallowing scripts to make HTTP requests to other domains (names such as themafia.org and mybank.com).

This can be an annoying problem when building systems that want to access several domains for legitimate reasons. Fortunately, servers can include a header like this in their response to explicitly indicate to browsers that it is okay for the request to come from other domains:

Access-Control-Allow-Origin: *

Abstracting requests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function backgroundReadFile(url, callback) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function() {
if (req.status < 400) {
callback(req.responseText);
}
});
req.send(null);
}

// invoke it
backgroundReadFile('http://www.rayjune.me', function (req) {
console.log(req);
});

This simple abstraction makes it easier to use XMLHttpRequest for simple GET requests. If you are writing a program that has to make HTTP requests, it is a good idea to use a helper function so that you don’t end up repeating the ugly XMLHttpRequest pattern all through your code.

The previous one does only GET requests and doesn’t give us control over the headers or the request body. You could write another variant for POST requests or a more generic one that supports various kinds of requests. Many JavaScript libraries also provide wrappers for XMLHttpRequest.

The main problem with the previous wrapper is its handling of failure. When the request returns a status code that indicates an error (400 and up), it does nothing. This might be okay, in some circumstances, but imagine we put a “loading” indicator on the page to indicate that we are fetching information. If the request fails because the server crashed or the connection is briefly interrupted, the page will just sit there, misleadingly looking like it is doing something. The user will wait for a while, get impatient, and consider the site uselessly flaky.

We should also have an option to be notified when the request fails so that we can take appropriate action. For example, we could remove the “loading” message and inform the user that something went wrong.

Error handling in asynchronous code is even trickier than error handling in synchronous code. Because we often need to defer part of our work, putting it in a callback function, the scope of a try block becomes meaningless. In the following code, the exception will not be caught because the call to backgroundReadFile returns immediately. Control then leaves the try block, and the function it was given won’t be called until later.

1
2
3
4
5
6
7
8
9
try {
backgroundReadFile("http://www.rayjune.me", function(text) {
if (text !== "expected") {
throw new Error("That was unexpected");
}
});
} catch (e) {
console.log("Hello from the catch block");
}

To handle failing requests, we have to allow an additional function to be passed to our wrapper and call that when a request goes wrong. Alternatively, we can use the convention that if the request fails, an additional argument describing the problem is passed to the regular callback function. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getURL(url, callback) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function() {
if (req.status < 400)
callback(req.responseText);
else
callback(null, new Error("Request failed: " +
req.statusText));
});
req.addEventListener("error", function() {
callback(null, new Error("Network error"));
});
req.send(null);
}

We have added a handler for the “error” event, which will be signaled when the request fails entirely.
We also call the callback function with an error argument when the request completes with a status code that indicates an error.

Code using getURL must then check whether an error was given and, if it finds one, handle it.

1
2
3
4
5
6
7
getURL("data/nonsense.txt", function(content, error) {
if (error != null) {
console.log("Failed to fetch nonsense.txt: " + error);
} else {
console.log("nonsense.txt: " + content);
}
});

Promises

For complicated projects, writing asynchronous code in plain callback style is hard to do correctly. It is easy to forget to check for an error or to allow an unexpected exception to cut the program short in a crude way. Additionally, arranging for correct error handling when the error has to flow through multiple callback functions and catch blocks is tedious.

One of the more successful solve abstractions ones is called promises. Promises wrap an asynchronous action in an object, which can be passed around and told to do certain things when the action finishes or fails. This interface is set to become part of the next version of the JavaScript language but can already be used as a library.

The interface for promises isn’t entirely intuitive, but it is powerful.

To create a promise object, we call the Promise constructor, giving it a function that initializes the asynchronous action. The constructor calls that function, passing it two arguments, which are themselves functions. The first should be called when the action finishes successfully, and the second should be called when it fails.

Once again, here is our wrapper for GET requests, this time returning a promise. We’ll simply call it get this time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function get(url) {
return new Promise(function(succeed, fail) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function() {
if (req.status < 400) {
succeed(req.responseText);
} else {
fail(new Error("Request failed: " + req.statusText));
}
});
req.addEventListener("error", function() {
fail(new Error("Network error"));
});
req.send(null);
});
}

Note that the interface to the function itself is now a lot simpler.
You give it a URL, and it returns a promise.

That promise acts as a handle to the request’s outcome. It has a then method that you can call with two functions: one to handle success and one to handle failure.

1
2
3
4
5
get("example/data.txt").then(function(text) {
console.log("data.txt: " + text);
}, function(error) {
console.log("Failed to fetch data.txt: " + error);
});

You can think of the promise interface as implementing its own language for asynchronous control flow.

The extra method calls and function expressions needed to achieve this make the code look somewhat awkward but not remotely as awkward as it would look if we took care of all the error handling ourselves.

Calling then produces a new promise, whose result (the value passed to success handlers) depends on the return value of the first function we passed to then. This function may return another promise to indicate that more asynchronous work is being done.

1
2
3
function getJSON(url) {
return get(url).then(JSON.parse);
}

That last call to then did not specify a failure handler. This is allowed. The error will be passed to the promise returned by then, which is exactly what we want—getJSON does not know what to do when something goes wrong, but hopefully its caller does.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
function showMessage(msg) {
var elt = document.createElement("div");
elt.textContent = msg;
return document.body.appendChild(elt);
}

var loading = showMessage("Loading...");
getJSON("example/bert.json").then(function(bert) {
return getJSON(bert.spouse);
}).then(function(spouse) {
return getJSON(spouse.mother);
}).then(function(mother) {
showMessage("The name is " + mother.name);
}).catch(function(error) {
showMessage(String(error));
}).then(function() {
document.body.removeChild(loading);
});
</script>

Security and HTTPS

The secure HTTP protocol, whose URLs start with https://, wraps HTTP traffic in a way that makes it harder to read and tamper with. First, the client verifies that the server is who it claims to be by requiring that server to prove that it has a cryptographic certificate issued by a certificate authority that the browser recognizes. Next, all data going over the connection is encrypted in a way that should prevent eavesdropping and tampering.

It is not perfect, and there have been various incidents where HTTPS failed because of forged or stolen certificates and broken software. Still, plain HTTP is trivial to mess with, whereas breaking HTTPS requires the kind of effort that only states or sophisticated criminal organizations can hope to make.

Summary

In this chapter, we saw that HTTP is a protocol for accessing resources over the Internet. A client sends a request, which contains a method (usually GET) and a path that identifies a resource. The server then decides what to do with the request and responds with a status code and a response body. Both requests and responses may contain headers that provide additional information.

Browsers make GET requests to fetch the resources needed to display a web page. A web page may also contain forms, which allow information entered by the user to be sent along in the request made when the form is submitted. You will learn more about that in the next chapter.

The interface through which browser JavaScript can make HTTP requests is called XMLHttpRequest. You can usually ignore the “XML” part of that name (but you still have to type it). There are two ways in which it can be used—synchronous, which blocks everything until the request finishes, and asynchronous, which requires an event handler to notice that the response came in. In almost all cases, asynchronous is preferable. Making a request looks like this:

1
2
3
4
5
6
var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", true);
req.addEventListener("load", function() {
console.log(req.status);
});
req.send(null);

Asynchronous programming is tricky. Promises are an interface that makes it slightly easier by helping route error conditions and exceptions to the right handler and by abstracting away some of the more repetitive and error-prone elements in this style of programming.

Exercises

Content negotiation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function requestAuthor(type) {
var req = new XMLHttpRequest();
req.open("GET", "http://eloquentJavaScript.net/author", false);
req.setRequestHeader("accept", type);
req.send(null);
return req.responseText;
}

var types = ["text/plain",
"text/html",
"application/json",
"application/rainbows+unicorns"];

types.forEach(function(type) {
try {
console.log(type + ":\n", requestAuthor(type), "\n");
} catch (e) {
console.log("Raised error: " + e);
}
});

Waiting for multiple promises

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function all(promises) {
return new Promise(function(succeed, fail) {
var results = [], pending = promises.length;
promises.forEach(function(promise, i) {
promise.then(function(result) {
results[i] = result;
pending -= 1;
if (pending == 0) {
succeed(results);
}
}, function(error) {
fail(error);
});
});
if (promises.length == 0) {
succeed(results);
}
});
}

// Test code.
all([]).then(function(array) {
console.log("This should be []:", array);
});
function soon(val) {
return new Promise(function(success) {
setTimeout(function() { success(val); },
Math.random() * 500);
});
}
all([soon(1), soon(2), soon(3)]).then(function(array) {
console.log("This should be [1, 2, 3]:", array);
});
function fail() {
return new Promise(function(success, fail) {
fail(new Error("boom"));
});
}
all([soon(1), fail(), soon(3)]).then(function(array) {
console.log("We should not get here");
}, function(error) {
if (error.message != "boom")
console.log("Unexpected failure:", error);
});

Forms and Form Fields

Fields

A web form consists of any number of input fields grouped in a form tag.
A lot of field types use the input tag.

Form fields do not necessarily have to appear in a

tag. You can put them anywhere in a page. Such fields cannot be submitted (only a form as a whole can), but when responding to input with JavaScript, we often do not want to submit our fields normally anyway.

1
2
3
4
5
6
7
<p><input type="text" value="abc"> (text)</p>
<p><input type="password" value="abc"> (password)</p>
<p><input type="checkbox" checked> (checkbox)</p>
<p><input type="radio" value="A" name="choice">
<input type="radio" value="B" name="choice" checked>
<input type="radio" value="C" name="choice"> (radio)</p>
<p><input type="file"> (file)</p>

and textarea tag

1
2
3
4
5
<textarea>
one
two
three
</textarea>

Whenever the value of a form field changes, select fires a “change” event.

1
2
3
4
5
<select>
<option>Pancakes</option>
<option>Pudding</option>
<option>Ice cream</option>
</select>

Focus

Unlike most elements in an HTML document, form fields can get keyboard focus. When clicked—or activated in some other way—they become the currently active element, the main recipient of keyboard input.

We can control focus from JavaScript with the focus and blur methods. The first moves focus to the DOM element it is called on, and the second removes focus. The value in document.activeElement corresponds to the currently focused element.

1
2
3
4
5
6
7
8
9
10
11
12
13
<input type="text">
<script>
document.querySelector("input").focus();
console.log(document.activeElement.tagName);
// → INPUT
setTimeout(testBlur, 2000);

function testBlur() {
document.querySelector("input").blur();
console.log(document.activeElement.tagName);
// → BODY
}
</script>

For some pages, the user is expected to want to interact with a form field immediately. JavaScript can be used to focus this field when the document is loaded, but HTML also provides the autofocus attribute, which produces the same effect but lets the browser know what we are trying to achieve. This makes it possible for the browser to disable the behavior when it is not appropriate, such as when the user has focused something else.

1
<input type="text" autofocus>

Browsers traditionally also allow the user to move the focus through the document by pressing the Tab key. We can influence the order in which elements receive focus with the tabindex attribute. The following example document will let focus jump from the text input to the OK button, rather than going through the help link first:

1
2
<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>

By default, most types of HTML elements cannot be focused. But you can add a tabindex attribute to any element, which will make it focusable.

Disabled fields

All form fields can be disabled through their disabled attribute, which also exists as a property on the element’s DOM object.

1
2
<button>I'm all right</button>
<button disabled>I'm out</button>

Disabled fields cannot be focused or changed, and unlike active fields, they usually look gray and faded.

When a program is in the process of handling an action caused by some button or other control, which might require communication with the server and thus take a while, it can be a good idea to disable the control until the action finishes. That way, when the user gets impatient and clicks it again, they don’t accidentally repeat their action.

The form as a whole

When a field is contained in a

element, its DOM element will have a property form linking back to the form’s DOM element. The element, in turn, has a property called elements that contains an array-like collection of the fields inside it.

1
2
3
4
5
6
7
8
9
10
11
12
13
<form action="example/submit.html">
Name: <input type="text" name="name"><br>
Password: <input type="password" name="password"><br>
<button type="submit">Log in</button>
</form>
<script>
var form = document.querySelector('form');
console.log(form);
console.log(form.elements);
console.log(form.elements[1].type);
console.log(form.elements.password.type);
console.log(form.elements.name.form === form);
</script>

A button with a type attribute of submit will, when pressed, cause the form to be submitted. Pressing Enter when a form field is focused has the same effect.

Submitting a form normally means that the browser navigates to the page indicated by the form’s action attribute, using either a GET or a POST request. But before that happens, a “submit” event is fired. This event can be handled by JavaScript, and the handler can prevent the default behavior by calling preventDefault on the event object.

1
2
3
4
5
6
7
8
9
10
11
<form action="example/submit.html">
Value: <input type="text" name="value">
<button type="submit">Save</button>
</form>
<script>
var form = document.querySelector("form");
form.addEventListener("submit", function(event) {
console.log("Saving value", form.elements.value.value);
event.preventDefault();
});
</script>

Intercepting “submit” events in JavaScript has various uses. We can write code to verify that the values the user entered make sense and immediately show an error message instead of submitting the form when they don’t.

Or we can disable the regular way of submitting the form entirely, as in the previous example, and have our program handle the input, possibly using XMLHttpRequest to send it over to a server without reloading the page.

Text fields

Fields created by input tags with a type of text or password, as well as textarea tags, share a common interface. Their DOM elements have a value property that holds their current content as a string value. Setting this property to another string changes the field’s content.

The selectionStart and selectionEnd properties of text fields give us information about the cursor and selection in the text. When nothing is selected, these two properties hold the same number, indicating the position of the cursor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<textarea></textarea>
<script>
var textarea = document.querySelector("textarea");
textarea.addEventListener("keydown", function(event) {
// The key code for left-shift happens to be 16
if (event.keyCode === 16) {
replaceSelection(textarea, "www.rayjune.me");
event.preventDefault();
}
});
function replaceSelection(field, word) {
var from = field.selectionStart;
var to = field.selectionEnd;
field.value = field.value.slice(0, from) + word +
field.value.slice(to);
// Put the cursor after the word
field.selectionStart = from + word.length;
field.selectionEnd = field.selectionStart;
}
</script>

The “change” event for a text field does not fire every time something is typed. Rather, it fires when the field loses focus after its content was changed.

To respond immediately to changes in a text field, you should register a handler for the “input” event instead, which fires for every time the user types a character, deletes text, or otherwise manipulates the field’s content.

1
2
3
4
5
6
7
8
<input type="text"> length: <span id="length">0</span>
<script>
var text = document.querySelector("input");
var output = document.querySelector("#length");
text.addEventListener("input", function() {
output.textContent = text.value.length;
});
</script>

Checkboxes and radio buttons

1
2
3
4
5
6
7
8
<input type="checkbox" id="teal">
<label for="teal">Make this page teal</label>
<script>
var checkbox = document.querySelector('#teal');
checkbox.addEventListener('change', function () {
document.body.style.background = checkbox.checked ? 'teal' : '';
});
</script>

another example

1
2
3
4
5
6
7
8
9
10
11
12
13
Color:
<input type="radio" name="color" value="mediumpurple"> Purple
<input type="radio" name="color" value="lightgreen"> Green
<input type="radio" name="color" value="lightblue"> Blue
<script>
var buttons = document.getElementsByName("color");
function setColor(e) {
document.body.style.background = e.target.value;
}
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('change', setColor);
}
</script>

The document.getElementsByName method gives us all elements with a given name attribute.

The example loops over those (with a regular for loop, not forEach, because the returned collection is not a real array)

Select fields

1
2
3
4
5
<select multiple>
<option>Pancakes</option>
<option>Pudding</option>
<option>Ice cream</option>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<select multiple>
<option value="1">0001</option>
<option value="2">0010</option>
<option value="4">0100</option>
<option value="8">1000</option>
</select> = <span id="output">0</span>
<script>
var select = document.querySelector("select");
var output = document.querySelector("#output");
select.addEventListener("change", function() {
var number = 0;
for (var i = 0; i < select.options.length; i++) {
var option = select.options[i];
if (option.selected) {
number += Number(option.value);
}
}
output.textContent = number;
});
</script>

File fields

1
2
3
4
5
6
7
8
9
10
11
12
13
<input type="file">
<script>
var input = document.querySelector("input");
input.addEventListener("change", function() {
if (input.files.length > 0) {
var file = input.files[0];
console.log("You chose", file.name);
if (file.type) {
console.log("It has type", file.type);
}
}
});
</script>

What it does not have is a property that contains the content of the file. Getting at that is a little more involved. Since reading a file from disk can take time, the interface will have to be asynchronous to avoid freezing the document. You can think of the FileReader constructor as being similar to XMLHttpRequest but for files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<input type="file" multiple>
<script>
var input = document.querySelector("input");
input.addEventListener("change", function() {
Array.prototype.forEach.call(input.files, function(file) {
var reader = new FileReader();
reader.addEventListener("load", function() {
console.log("File", file.name, "starts with",
reader.result.slice(0, 20));
});
reader.readAsText(file);
});
});
</script>

FileReaders also fire an “error” event when reading the file fails for any reason. The error object itself will end up in the reader’s error property. If you don’t want to remember the details of yet another inconsistent asynchronous interface, you could wrap it in a Promise like this:

1
2
3
4
5
6
7
8
9
10
11
12
function readFile(file) {
return new Promise(function(succeed, fail) {
var reader = new FileReader();
reader.addEventListener("load", function() {
succeed(reader.result);
});
reader.addEventListener("error", function() {
fail(reader.error);
});
reader.readAsText(file);
});
}

Storing data client-side

When such an application needs to remember something between sessions, you cannot use JavaScript variables since those are thrown away every time a page is closed. You could set up a server, connect it to the Internet, and have your application store something there.

But this adds a lot of extra work and complexity. Sometimes it is enough to just keep the data in the browser. But how?

You can store string data in a way that survives page reloads by putting it in the localStorage object. This object allows you to file string values under names (also strings), as in this example:

1
2
3
4
localStorage.setItem("username", "marijn");
console.log(localStorage.getItem("username"));
// → marijn
localStorage.removeItem("username");

There is another object similar to localStorage called sessionStorage. The difference between the two is that the content of sessionStorage is forgotten at the end of each session, which for most browsers means whenever the browser is closed.

Summary

HTML can express various types of form fields, such as text fields, checkboxes, multiple-choice fields, and file pickers.

Such fields can be inspected and manipulated with JavaScript. They fire the “change” event when changed, the “input” event when text is typed, and various keyboard events (keydown). These events allow us to notice when the user is interacting with the fields. Properties like value (for text and select fields) or checked (for checkboxes and radio buttons) are used to read or set the field’s content.

When a form is submitted, its “submit” event fires. A JavaScript handler can call preventDefault on that event to prevent the submission from happening. Form field elements do not have to be wrapped in form tags.

When the user has selected a file from their local file system in a file picker field, the FileReader interface can be used to access the content of this file from a JavaScript program.

The localStorage and sessionStorage objects can be used to save information in a way that survives page reloads. The first saves the data forever (or until the user decides to clear it), and the second saves it until the browser is closed.

Exercises

A JavaScript workbench

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<textarea id="code">return "hi";</textarea>
<button id="button">Run</button>
<pre id="output"></pre>

<script>
document.querySelector('#button').addEventListener('click', function () {
var code = document.querySelector('#code').value;
var outputNode = document.querySelector('#output');
try {
var result = new Function(code)();
outputNode.innerText = String(result);
} catch (e) {
outputNode.innerText = 'Error: ' + e;
}
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!doctype html>
<script src="code/promise.js"></script>

<input type="text" id="field">
<div id="suggestions" style="cursor: pointer"></div>

<script>
// Builds up an array with global variable names, like
// 'alert', 'document', and 'scrollTo'
var terms = [];
for (var name in window)
terms.push(name);

var textfield = document.querySelector("#field");
var suggestions = document.querySelector("#suggestions");

textfield.addEventListener("input", function() {
var matching = terms.filter(function(term) {
return term.indexOf(textfield.value) == 0;
});
suggestions.textContent = "";
matching.slice(0, 20).forEach(function(term) {
var node = document.createElement("div");
node.textContent = term;
node.addEventListener("click", function() {
textfield.value = term;
suggestions.textContent = "";
});
suggestions.appendChild(node);
});
});
</script>
1
2
3
4
5
6
7
8
9
<!doctype html>
<script src="code/promise.js"></script>

<div id="grid"></div>
<button id="next">Next generation</button>
<button id="run">Auto run</button>

<script>
</script>

文章标题:Eloquent JavaScript 小记

文章作者:RayJune

时间地点:上午 9:01,于又玄图书馆

原始链接:https://www.rayjune.me/2017/09/21/Eloquent-JavaScript-note/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。