- Chapter 1, Accustoming Yourself to JavaScript
- Chapter 2, Variable Scope
- Chapter 3, Working with Functions
- Chapter 4, Objects and Prototypes
- Chapter 5, Arrays and Dictionaries
- Chapter 6, Library and API Design
- Chapter 7, Concurrency
Chapter 1, Accustoming Yourself to JavaScript
Item 1: Know Which JavaScript You Are Using
ES5 introduced strict mode
// at the very beginning of the program
"use strict";
// or at the beginning of function body
function f(x) {
"use strict";
// ...
}
"use strict"
directive is only recognized at the top of a script or function. solution is to wrap your code in functions so strict mode could always be applied.
(function(){ "use strict"; function f(){ // ... } })();
Item 2: Understand JavaScript's Floating-Point Numbers
all numbers in javascript are double-precision floating-point numbers - commonly known as "doubles".
most arithmetic operators work with integers, real numbers, or a combination of the two:
21 - 12.3; // 8.7
the bitwise arithmetic operators, however are special. they implicitly convert them to 32-bit integers.
8 | 1; // 9
(8).toString(2); // "1000", the result drops extra 0 bits on the left since they don't affect the value.
parseInt("1001", 2); // 9
while 64 bits of precision is reasonably large, doubles can still only represent a finite set of numbers, rather than the infinite set of real numbers. floating-point arithmetic can only product approximate results.
(0.1 + 0.2) + 0.3; // 0.6000000000000001 0.1 + (0.2 + 0.3); // 0.6
one workaround is to work with integer values wherever possible.
(10 + 20) + 30; // 60 10 + (20 + 30); // 60
Item 3: Beware of Implicit Coercions
in javascript, you can do this
3 + true; // 4
javascript coerces a value to the expected type by various automatic conversion protocols.
arithmetic operators -
, *
, /
and %
all attempt to convert their arguments to numbers before doing calculation.
the operator +
is subtler:
"2" + 3; // "23" 1 + "2" + 3; // "123"
bitwise operators not only convert to numbers but to the subset of numbers that can be represented as 32-bit integers.
a null
will not fall in an arithmeticcalculation, but silently convert to 0
an undefined
will convert to NaN
, and javascript follows IEEE floating-point standard that NaN
be treated as unequal to itself.
var x = NaN; x === NaN; // false
the isNaN()
function is not very reliable because it comes with its own implicit coercion, converting its argument to a number before testing the value.
isNaN(NaN); // true
// these are not NaN isNaN("foo"); // true isNaN(undefined); // true isNaN({}); // true isNaN({valueOf: "foo"}); // true
there is an idiom that is both reliable and concise for testing for NaN
. since NaN
is the only javascript value that is treated as unequal to itself, we can do:
var a = NaN; a !== a; // true
var b = "foo"; b !== b; // false
var c = undefined; c !== c; // false
var d = {}; d !== d; // false
to abstract the pattern:
function isReallyNaN(x) { return x !== x; }
Objects can also be coerced to primitives by calling toString()
and valueOf()
(convert string and number)
if an object contains both toString
and valueOf
, javascript will blindly choosing valueOf
over toString
.
var obj = { toString: function() { return "[object MyObject]"; }, valueOf: function() { return 17; } };
"object: " + obj; // "object: 17"
in general, coercion to strings if far more common and useful than to numbers. it's best to avoid valueOf
unless your object really is a numberic abstraction and obj.toString()
produces a string representation of obj.valueOf()
.
last kind of coercion is truthiness.(operators such as if
, ||
, and &&
).
there are 7 falsy values: false
, 0
, -0
, ""
, NaN
, null
, and undefined
. all other values are truthy.
Item 4: Prefer Primitives to Object Wrappers
in addition to obejcts, javascript has 5 types of primitive values: booleans
, numbers
, strings
, null
, and undefined
. (typeof
reports the type of null
as "object", but ES5 fixes it)
var s = new String("hello");
typeof s; // "object" typeof "hello"; // "string"
this is an important difference
var s1 = new String("hello"); var s2 = new String("hello"); s1 === s2; // false s1 == s2; // false; even nonstrict comparison is false
since these wrappers don't behave quite right, they don't serve much of a purpose.
"hello".someProperty = 17; "hello".someProperty; // undefined
since the implicit wrapping produces a new String object each time it occurs, the update to the first wrapper object has no lasting effect.
if you set properties on what you expect to be an object, but use a primitive value by mistake, your program will simply silently ignore the update and continue.
Item 5: Avoid using ==
with Mixed Types
"1.0e0" == { valueOf: function(){ return true; } }; // true, both parses as number 1
it's easy to convert value to numbers explicitly using +
:
var today = new Date();
if (+form.month.value == (today.getMonth() + 1)) {
// ...
}
when the two arguments are of the same type, there is no difference between ==
and ===
. but using strict equality is a good way to make it clear to readers that there is no conversion involved in the comparison.
Item 6: Learn the Limits of Semicolon Insertion
the first rule semicolon insertion is:
Semicolons are only ever inserted before a }
token, after one or more new lines, or at the end of the program input.
which means you can only leave out semicolons at the end of a line, block, or program.
the second rule is:
Semicolons are only ever inserted when the next input token cannot be parsed.
means semicolon insertion is an error correction mechanism. but you always have to pay attention to the start of the next statement to detect whether you can legally omit a semicolon.
if the next line starts with any of (
, [
, +
, -
, and /
, no semicolon will be inserted. (or you can add semicolon at the beginning of the line)
(function() {
// ...
})()
(function() {
// ...
})()
// will become
(function() {
// ...
})()(function() {
// ...
})();
// add semicolon before to avoid
;(function() {
// ...
})()
;(function() {
// ...
})()
and never put new lines before return
, throw
, break
, continue
, ++
, or --
, semicolons will be inserted and may with no errors.
a
++
b
// will be parsed as
a;
++b;
the third and final rule:
Semicolons are never inserted as separators in the head of a for loop or as empty statements.
function infiniteloop() { while (true) } // parse error
function infiniteloop() { while (true); } // semicolon is needed
Item 7: Think of Strings As Sequences of 16-Bit Code Units
The basic of Unicode are simple: Every unit of text of all the world's writting system is assigned a unique integer between 0 and 1,114,111, known as a code point in Unicode terminology.
code unit is a simple, one-to-one mapping between code points and the elements of the encoding. UCS-2 the original 16-bit encoding, was made up of individual 16-bit code units, each of which corresponded to a single Unicode code point.
later the standard expanded to 220 code points(17 subranges of 216 code points), Basic Multilingual Plane (BMP) consists of the original 216 code points, the addtional 16 ranges are known as the supplementary planes.
UTF-16, is mostly the same, but with the additional of what are known as surrogate pairs: pairs of 16-bit code units that together encode a single code point 216 or greater.
UTF-16 is a variable-length encoding: each code poing in a UTF-16 encoding may require either one or two 16-byte code units. finding the nth code point of a string is no longer a constant-time operation: it generally requires searching from the beginning of the string.
But by the time Unicode expanded in size, JavaScript had already committed to 16-bit string elements. String methods like length
, charAt
, and chatCodeAt
all work at the level of code units rather than code points
An element of a JavaScript string is a 16-bit code unit.
Unicode code points 216 and above are represented in JavaScript by two code units, known as a surrogate pair. surrogate pairs throw off string element counts, affecting length
, charAt
, charCodeAt
, and regular expression patterns such as .
Chapter 2, Variable Scope
Item 8: Minimize Use of the Global Object
you have two mechanisms to create a global variable:
var foo = "foo";
// or
this.foo = "foo";
the var
declaration has the benefit of more clearly conveying the effect on the program's scope.
Item 9: Always Declare Local Variables
unintentional global variable:
function swap(a, i, j) {
temp = a[i]; // global
a[i] = a[j];
a[j] = temp;
}
// always declare with var
var temp = a[i]; // local
use lint tools to help check for unbound variables.
Item 10: Avoid with
function f(x, y) {
with (Math) {
return min(round(x), sqrt(y)); // ambiguous references
}
}
nothing in the syntax distinguishes two types of variables (Math
's and function local's). JavaScript looks variables up in with
block first, then outer scope. you can not ensure variables don't already exists in the inner scope:
Math.x = 0;
Math.y = 0;
f(2, 9); // 0
the best alternative is simply bind with short variable name:
function f(x, y) {
var min = Math.min, round = Math.round, sqrt = Math.sqrt;
return min(round(x), sqrt(y));
}
Math.x = 0;
Math.y = 0;
f(2, 9); // 2
Item 11: Get Comfortable with Closures
The first fact is that JavaScript allows you to refer to variables that were defined outside of the current function.
function makeSandwich() {
var magicIngredient = "peanut butter";
function make(filling) {
return magicIngredient + " and " + filling;
}
return make("jelly");
}
makeSandwich(); // "peanut butter and jelly"
The second fact is that functions can refer to variables defined in outer functions even after those outer functions have returned.
var f = sandwi
function sandwichMaker() {
var magicIngredient = "peanut butter";
function make(filling) {
return magicIngredient + " and " + filling;
}
return make;
}
var f = sandwichMaker();
f("jelly"); // "peanut butter and jelly"
f("bananas"); // "peanut butter and bananas"
JavaScript function values contain more information than just the code required to execute when they're called. They also internally store any variables they may refer to that are defined in their enclosing scopes. Functions that keep track of variables from their containing scopes are known as closures.
The third and final fact to learn about closures is that they can update the values of outer variables. Closures actually store references to their outer variable.
function box() {
var val = undefined;
return {
set: function(newVal) { val = newVal; },
get: function() { return val; },
type: function() { return typeof val; }
};
}
var b = box();
b.type(); // "undefined"
b.set(98.6);
b.get(); // 98.6
b.type(); // "number"
three closures share the same val variable. (by reference)
Item 12: Understand Variable Hoisting
JavaScript supports lexical scoping: with only a few exceptions, A reference to a variable foo
is bound to the nearest scope in which foo
was declared. However, JavaScript does not support block scoping: Variable definitions are not scoped to their nearest enclosing statement or block, but rather to their containing function.
JavaScript implicitly hoists the declaration part to the top of the enclosing function and leaves the assignment in place.
function f() {
// ...
{
// ...
var x = /* ... */
// ...
}
// ...
}
is actually
function f() {
var x;
// ...
{
// ...
x = /* ... */
// ...
}
// ...
}
the one exception to JavaScript's lack of block scoping is the try..catch
, it binds a caught exception to a variable that is scoped just to the catch
block.
Item 13: Use Immediately Invoked Function Expressions to Create Local Scopes
Closures store their outer variables by reference, not by value.
function wrapElement(a) {
var result = [];
for (var i = 0, n = a.length; i < n; i++) {
result[i] = function() { return a[i]; };
}
return result;
}
var wrapped = wrapElement([10, 20, 30, 40, 50]);
var f = wrapped[0];
f(); // undefined
Immediately invoked function expression (IIFE, pronounced "iffy"), is an indispensable workaround for JavaScript's lack of block scoping.
function wrapElement(a) {
var result = [];
for (var i = 0, n = a.length; i < n; i++) {
(function() { // new scope
var j = i;
result[i] = function() { return a[j]; };
})();
}
return result;
}
// or bind the local variable as a parameter to the IIFE and pass its value as an argument:
(function(j) { // new scope
result[i] = function() { return a[j]; };
})(i);
however, wrapping a block in a function can introduce some subtle changes to the block. first, the block cannot contain any break
or continue
statements that jump outside of the block, since it's illegal to break
or continue
outside of a function.
second, if the block refers to this
or the special arguments
varaible, the IIFE changes their meanings. (chapter 3 discusses techniques for working with this
and arguments
)
Item 14: Beware of Unportable Scoping of Named Function Expressions
function double(x) { return x * 2; }
depending where it appears, this could be either a function declaration or a named function expression.
var f = function double(x) { return x * 2; };
this binds the function to f
rather than double
. named function expressions is not useful except for debugging.
named function expressions also have a notorious source of scoping and compatibility issue.
var constructor = function() { return null; };
var f = function f() {
return constructor();
};
f(); // {} (in ES3 environments)
it should produce null
, but the named function expression inherits Object.prototype.constructor
in its scope, it produces a new object instead (ES3 environments)
ES5 corrected it, but some environments still have unexpected result for even anonymous function expressions:
var constructor = function() { return null; };
var f = function() {
return constructor();
};
f(); // {} (in nonconformant environments)
it should produce null
, but the named function expression inherits Object.prototype.constructor
in its scope, it produces a new object instead (ES3 environments)
Item 15: Beware of Unportable Scoping of Block-Local Function Declarations
always keep function declaration at the outermost level of a program or a containing function to avoid unportable behavior.
use var
declarations with conditional assignment instead of conditional function declarations.
Item 16: Avoid Creating Local Variables with eval
Item 17: Prefer Indirect eval
to Direct eval
(i don't use eval
)
Chapter 3, Working with Functions
Item 18: Understand the Difference between Function, Method, and Constructor Calls
in JavaScript, functions, methods, and class constructors are three different usage patterns of one single construct: functions.
as normal function
function hello(name) {
return "hello, " + name;
}
hello("foo"); // "hello, foo"
as method
var obj = {
hello: function() {
return "hello, " + this.name;
},
name: "foo"
};
obj.hello(); // "hello, foo"
var obj2 = {
hello: obj.hello,
name: "bar"
};
obj2.hello(); // "hello, bar"
the method call expression itself determines the binding of this
, also known as the call's receiver.
obj.hello()
looks up the hello
property of obj
and calls it with receiver obj
.
obj2.hello()
looks up the hello
property of obj2
, which to be the same function as obj.hello
, but calls it with receiver obj2
.
in general, calling a method on an object looks up the method and then uses the object as the method's receiver.
ES5 strict mode changes the default binding of this
to undefined:
function hello() {
"use strict";
return "hello" + this.name;
}
hello(); // error
as constructor
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
var u = new User("me", "xxx");
u.name; // "me"
unlike function calls and method calls, a constructor call (with new
) passes a brand-new object as the value of this
, and implicitly returns the new object as its result (receiver). the constructor function's primary role is to initialize the object.
Item 19: Get Comfortable Using Higher-Order Functions
(no notes)
Item 20: Use call
to Call Methods with a Custom Receiver
f.call(obj, arg1, arg2, arg3);
// behaves similarly to calling it directly:
f(arg1, arg2, arg3);
you can call method even it is not stored anywhere in the object:
dict.hasOwnProperty = 1;
dict.hasOwnProperty("foo"); // error, 1 is not a function
var hasOwnProperty = {}.hasOwnProperty;
dict.foo = 1;
delete dict.hasOwnProperty; // dict has no hasOwnProperty property now
hasOwnProperty.call(dict, "foo"); // true
hasOwnProperty.call(dict, "hasOwnProperty"); // false
- use
call
method to call a function with a custom receiver - use
call
method for calling methods that may not exist on a given object - use
call
method for defining higher-order functions that allow clients to provide a receiver for the callback.
Item 21: Use apply
to Call Functions with Different Numbers of Arguments
variadic or variable-arity function: a function can take any number of arguments.
var scores = getAllScores();
average.apply(null, scores); // pass null since the function does not refer to this
use the first argument of apply
to provide a receiver for variadic methods.
Item 22: Use arguments
to Create Variadic Functions
JavaScript provides every function with an implicit local variable called arguments
: it contains indexed properties for each actual argument and a length
property indicating how many arguments where provided.
A good rule of thumb is that whenever you provide a variable-arity function for convenience, you should also provide a fixed-arity version that takes an explicit array.
function average() { // takes any number of arguments
return averageOfArray(arguments); // takes an array argument
}
Item 23: Never Modify the arguments Object
the arguments
object is not a copy of the function's arguments. all named arguments are aliases to their corresponding indices in the arguments object. (don't try to shift()
the arguments
object, it won't change the aliases)
however, in ES5 strict mode, function parameters do not alias their arguments object.
function strict(x) {
"use strict";
arguments[0] = "modified";
return x === arguments[0];
}
function nonstrict(x){
arguments[0] = "modified";
return x === arguments[0];
}
strict("unmodified"); // false
nonstrict("unmodified"); // true
copy the arguments to a real array before modifying it:
function callMethod(obj, method) {
var args = [].slice.call(arguments, 2); // shift 2, a true Array instance
return object[method].apply(obj, args);
}
Item 24: Use a Variable to Save a Reference to arguments
if you use arguments
when making an iterator, bind a local variable to the arguments
object. since a new arguments
variable is implicitly bound in the body of each function.
function values() {
var i = 0, n = arguments.length, a = arguments;
return {
hasNext: function() {
return i < n;
},
next: function() {
if (i >= n) {
throw new Error("end of iteration");
}
return a[i++]; // if use arguments[i++] here, it won't work, since the function has a new arguments object each time it is called
}
};
}
Item 25: Use bind
to Extract Methods with a Fixed Receiver
var buffer = {
entries: [],
add: function(s) {
this.entries.push(s);
},
join: function() {
return this.entries.join("");
}
};
var source = ["867", "-", "5309"];
source.forEach(buffer.add); // error, entries is undefined
here, the receiver of buffer.add
is not buffer
, a function's receiver is determined by how it is called. when we pass it to forEach
, we don't know the implementation (forEach
uses the global object as the default receiver)
// we can fix it by pass an extra argument as receiver
source.forEach(buffer.add, buffer);
but not every higher-order functions offer their clients the courtesy of providing a receiver for their callbacks.
// we can call buffer.add explicitly
source.forEach(function(s) {
buffer.add(s);
});
ES5 added bind
for the pattern
// use bind
source.forEach(buffer.add.bind(buffer));
keep in mind that buffer.add.bind(buffer)
creates a new function rather than modifying the buffer.add
function:
buffer.add == buffer.add.bind(buffer); // false
this is a subtle but crucial point: bind
is safe to call even on a function that may be shared by other parts of a program.
Item 26: Use bind
to Curry Functions
function simpleURL(protocol, domain, path) {
return protocol + "://" + domain + "/" + path;
}
var urls = paths.map(function(path) {
return simpleURL("http", siteDomain, path);
})
here, protocol
and domain
are fixed for each iteration, only the third argument is needed. we can use bind
on simpleURL
to construct it:
var urls = paths.map(simpleURL.bind(null, "http", siteDomain));
the call to simpleURL.bind
produces a new function that delegates to simpleURL
.
Item 27: Prefer Closures to Strings for Encapsulating Code
perfer APIs that accept functions to call rather than strings to eval.
Item 28: Avoid Relying on the toString Method of Functions
(no notes)
Item 29: Avoid Nonstandard Stack Inspection Properties
avoid the nonstandard arguments.caller
and arguments.callee`
Chapter 4, Objects and Prototypes
Item 30: Understand the Difference between prototype
, getPrototypeOf
, and __proto__
Prototypes involve 3 separate but related accessors:
C.prototype
is used to establish the prototype of objects created bynew C()
Object.getPrototypeOf(obj)
is the standard ES5 mechanism for retrieving obj's prototype object.obj.__proto__
is a nonstandard mechanism for retrieving obj's prototype object.
A class is a design pattern consisting of a constructor function and an associated prototype.
Item 31: Prefer Object.getPrototypeOf
to __proto__
for JavaScript environments that do not provide the ES5 API
if (typeof Object.getPrototypeOf === "undefined") {
Object.getPrototypeOf = function(obj) {
var t = typeof obj;
if (!obj || (t !== "object" && t !== "function")) {
throw new TypeError("not an object);
}
return obj.__proto__;
};
}
Item 32: Never Modify __proto__
use Object.create
to provide a custom prototype for new objects.
Item 33: Make Your Constructors new
-Agnostic
ES5 introduces Object.create
function User(name, passwordHash) {
var self = this instanceof User
? this
: Object.create(User.prototype);
self.name = name;
self.passwordHash = passwordHash;
return self;
}
for old environments
if (typeof Object.create === "undefined") {
Object.create = function(prototype) {
function C() { }
C.prototype = prototype;
return new C();
};
}
Item 34: Store Methods on Prototypes
storing methods on instance objects creates multiple copies of the functions, one per instance object.
perfer storing methods on prototypes over storing them on instance objects.
Item 35: Use Closures to Store Private Data
(same as the pattern book)
Item 36: Store Instance State Only on Instance Objects
mutable data can be problematic when shared, and prototypes are shared between all their instances.
store mutable per-instance state on instance objects.
Item 37: Recognize the Implicit Binding of this
function CSVReader(separators) {
this.separators = separators || [","];
this.regexp = new RegExp(this.separators.map(function(sep) {
return "\\" + sep[0];
}).join("|"));
}
CSVReader.prototype.read = function(str) {
var lines = str.trim().split(/\n/);
return lines.map(function(line) {
return line.split(this.regexp); // wrong this
});
};
map
binds its callback's receiver to lines
array.
CSVReader.prototype.read = function(str) {
var lines = str.trim().split(/\n/);
var self = this; // save a reference to outer this-binding
return lines.map(function(line) {
return line.split(self.regexp); // use outer this
});
};
ES5 can use bind
CSVReader.prototype.read = function(str) {
var lines = str.trim().split(/\n/);
return lines.map(function(line) {
return line.split(this.regexp);
}.bind(this)); // bind to outer this-binding
};
Item 38: Call Superclass Constructors from Subclass Constructors
(this chapter contains simple objects for game: Scene, Actor, check gitst)
Call the superclass constructor explicitly from subclass constructors, passing this
as the explicit receiver.
function SpaceShip(scene, x, y) {
Actor.call(this, scene, x, y);
this.points = 0;
}
Use Object.create
to construct the subclass prototype object to avoid calling the superclass constructor.
SpaceShip.prototype = Object.create(Actor.prototype);
Item 39: Never Reuse Superclass Property Names
(no notes)
Item 40: Avoid Inheriting from Standard Classes
Inheriting from standard classes tends to break due to special internal properties such as [[Class]] (check book).
Perfer delegating to properties instead of inheriting from standard classes.
Item 41: Treat Prototypes As an Implementation Detail
Objects are interfaces; prototypes are implementations.
Avoid inspecting the prototype structure of objects you don't control.
Avoid inspecting properties that implement the internals of objects you don't control.
Item 42: Avoid Reckless Monkey-Patching
(no notes)
Chapter 5, Arrays and Dictionaries
Item 43: Build Lightweight Dictionaries from Direct Instances of Object
use object literals to construct lightweight dictionaries
lightweight dictionaries should be direct descendants of Object.prototype
to protect against prototype pollution in for..in
loops
Item 44: Use null Prototypes to Prevent Prototype Pollution
ES5 offers the first standard way to create an object with no prototype.
var x = Object.create(null);
Object.getPrototypeOf(x) === null; // true
for old environments
var x = { __proto__: null };
x instanceof Object; // false (non-standard)
Item 45: Use hasOwnProperty
to Protect Against Prototype Pollution
(no notes)
Item 46: Prefer Arrays to Dictionaries for Ordered Collections
avoid relying on the order in which for...in
loops enumerate object properties.
if you aggregate data in a dictionary, make sure the aggregate operations are order-insensitive.
use arrays instead of dictionary objects for ordered collections.
Item 47: Never Add Enumerable Properties to Object.prototype
consider writing a function instead of an Object.prototype
method.
if you do add properties to Object.prototype
, use ES5's Object.defineProperty
to define them as nonenumerable properties.
Item 48: Avoid Modifying an Object during Enumeration
make sure not to modify any object while enumerating its properties with a for...in
loop.
use a while
or for
loop instead of a for...in
loop when iterating over an object whose contents might change during the loop.
Item 49: Prefer for
Loops to for...in
Loops for Array Iteration
(no notes)
Item 50: Prefer Iteration Methods to Loops
ES5 provides Array.prototype.forEach
for (var i = 0, n = players.length; i < n; i++) {
players[i].score++;
}
can be written as
players.forEach(function(p){
p.score++;
});
also provides Array.prototype.map
var timmed = input.map(function(s) {
return s.trim();
});
Array.prototype.filter
:
listings.filter(function(listing) {
return listing.price >= min && listing.price <= max;
});
Array.prototype.some
and Array.prototype.every
:
[1, 10, 100].some(function(x) { return x > 5; }); // true
[1, 10, 100].some(function(x) { return x < 0; }); // false
[1, 2, 3, 4, 5].every(function(x) { return x > 0; }); // true
[1, 2, 3, 4, 5].every(function(x) { return x < 3; }); // false
**Item 51: Reuse Generic Array
Methods on Array-Like Objects***
Any object can be used with generic Array
methods if it has indexed properties and an appropriate length
property.
Item 52: Prefer Array Literals to the Array Constructor
The Array
constructor behaves differently if its first argument is a Numbers: [17]
and Array[17]
do completely different things. Array[17]
create an array with no elements but the length
property is 17
.
Chapter 6, Library and API Design
Item 53: Maintain Consistent Conventions
argument order, always keep the same order.
use consistent convertions for variable names and function signatures.
Item 54: Treat undefined
As "No Value"
- avoid using
undefined
to represent anything other than the absence of a specific value. - use descriptive string values or objects with named boolean properties, rather than
undefined
ornull
, to represent application-specific flags. - Test for
undefined
instead of checkingarguments.length
to provide parameter default values. - Never use truthiness tests for parameter default values that should allow
0
,NaN
, or the empty string as valid arguments.
Item 55: Accept Options Objects for Keyword Arguments
- use options object to make APIs more readable and memorable.
- the arguments provided by an options object should all be treated as optional.
- use an
extend
utility function to abstract out the logic of extracting values from options object.
function extend(target, source) {
if (source) {
for (var key in source) {
var val = source[key];
if (typeof val !== "undefined") {
target[key] = val;
}
}
}
return target;
}
// usage
function Alert(parent, message, opts) {
opts = extend({
width: 320,
height: 240
});
opts = extend({
title: "Alert",
// ...
}, opts);
extend(this, opts);
}
Item 56: Avoid Unnecessary State
prefer stateless APIs where possible
c.fillStyle = "blue";
c.fillText("text", 0, 30);
// this is better, and stateless
c.fillText("text", 0, 30, { fillStyle: "blue" });
Item 57: Use Structural Typing for Flexible Interfaces
structural typing or duck typing: any object will do so long as it has the expected structure (if it looks like a duck, swims like a duck, and quacks like a duck...).
avoid inheritance when structural interface are more flexible and lightweight.
Item 58: Distinguish between Array and Array-Like
APIs should never overload structural types with other overlapping types.
accept true arrays instead of array-like objects when overloading with other object types.
use ES5's Array.isArray
to test for true arrays.
Item 59: Avoid Excessive Coercion
- avoid mixing coercions with overloading
- consider defensively guarding against unexpected inputs
Item 60: Support Method Chaining
- support method chaining by designing stateless methods what produce new objects
- support method chaining in stateful methods by returning
this
Chapter 7, Concurrency
Item 61: Don't Block the Event Queue on I/O
JavaScript is providing a run-to-completion grarantee: any user code that is currently running in a shared context is allowed to finish executing before the next event handler is invoked.
the drawback of run-to-completion is that any and all code you write effectively holds up the rest of the application from proceeding.
the single most important rule of concurrent JavaScript is never to use any blocking I/O APIs in the middle of an application's event queue.
Asynchronous APIs take callbacks to defer processing of expensive operations and avoid blocking the main application.
JavaScript accepts events concurrently but processes event handlers sequentially using an event queue.
Item 62: Use Nested or Named Callbacks for Asynchronous Sequencing
- use nested or named callbacks to perform serveral asynchronous operations in sequence.
- avoid sequencing operations that can be performed concurrently
Item 63: Be Aware of Dropped Errors
asynchronous APIs cannot throw exceptions because at the time an asynchronous error occurs, there is no obvious execution context to throw the exception to.
asynchronous APIs tend to represent errors as special arguments to callbacks, or take additional error-handling callbacks (errbacks)
avoid copying and pasting error-handling code by writing shared error-handling functions.
make sure to handle all error conditions explicitly to avoid dropped errors.
Item 64: Use Recursion for Asynchronous Loops
- loops cannot be asynchronous.
- use recursive functions to perform iterations in separate turns of the event loop.
- recursion performed in separate turns of the event loop does not overflow the call stack.
Item 65: Don't Block the Event Queue on Computation
to perform expensive computations, one simple approach is to use a concurrency mechanism like the web client platform's Worker
API.
var ai = new Worker("ai.js");
this has the effect of spawning a new concurrent thread of execution with its own separate event queue, using the source file ai.js
as the worker's script.
the application and worker can communicate with each other by sending messages
to each other.
// application
var userMove = /* ... */;
ai.postMessage(JSON.stringify({
userMove: userMove
}));
ai.onmessage = function(event) {
executeMove(JSON.parse(event.data).computerMove);
};
// ai.js
self.onmessage = function(event) {
// parse user move
var userMove = JSON.parse(event.data).userMove;
// generate next computer move
var computerMove = computeNextMove(userMove);
// format the computer move
var message = JSON.stringify({
computerMove: computerMove
});
self.postMessage(message);
};
function computeNextMove(userMove) {
// ...
}
when the Worker
API is not available or is too costly, consider breaking up computations across multiple turns of the event loop.
Item 66: Use a Counter to Perform Concurrent Operations
- events in a JavaScript application occur nondeterministically, that is, in unpredictable order.
- use a counter to avoid data races in concurrent operations.
Item 67: Never Call Asynchronous Callbacks Synchronously
- never call an asynchronous callback synchronously, even if the data is immediately available.
- calling an asynchronous callback synchronously disrupts the expected sequence of operations and can lead to unexpected interleaving of code.
- calling an asynchronous callback synchronously can lead to stack overflows or mishandled exceptions.
- use a asynchronous API such as
setTimeout
to schedule an synchronous callback to run in another turn.
function downloadCachingAsync(url, onsuccess, onerror) {
if (cache.has(url)) {
onsuccess(cache.get(url)); // synchronous call
return;
}
return downloadAsync(url, function(file) {
cache.set(url, file);
onsuccess(file);
}, onerror);
}
to ensure the callback is always invoked asynchronously, use setTimeout
:
// ...
var cached = cache.get(url);
setTimeout(onsuccess.bind(null, cached), 0);
return;
//...
Item 68: Use Promises for Cleaner Asynchronous Logic