1. Accustoming Yourself to JavaScript
- Item 1: Know which JavaScript You're Using
- Item 2: Understand JavaScript's Floating-Point Numbers
- Item 3: Beware of Implicit Coercions
- Item 4: Prefer Primitives to Object Wrappers
- Item 5: Avoid Using == with Mixed Types
- Item 6: Learn the Limits of Semicolon Insertion
- Item 7: Think of Strings As Sequences of 16-Bit Code Units
"use strict"
strict mode allows you to opt in to a restricted version of JavaScript that disallows some of the more problematic or error-prone features of the full language. Designed to be backward compatible so that environments that do not implement the strict-mode checks can still execute strict code.
Pitfall:
- only recognized at the top of a script or function, making it sensitive to script concatenation
Solutions:
- Never concatenate strict files and nonstrict files
- concatenate files by wrapping their bodies in IIFE's
- write files so that they behave the same in either mode
Things to Remember:
- Decide which version of JavaScript your app supports
- Be sure that any JavaScript features you use are supported by all environments where your app runs
- Always test strict code in environments that perform the strict-mode checks
- Beware of concatenating scripts that differ in their expectations about strict mode
Things to Remember:
- JavaScript numbers are double-precision floating-point numbers
- Integers in JavaScript are just a subset of doubles rather than a separate datatype
- Bitwise operators treat numbers as if they were 32-bit signed Integers
- Be aware of limitations of precisions in floating-point arithmetic.
Things to Remember:
- Type errors can be silently hidden by implicit coercions
- The
+
operator is overloaded to do addition or string concatenation depending on its argument types - Objects are coerced to numbers via
valueOf
and to strings viatoString
- Objects with
valueOf
methods should implement atoString
method that provides a string representation of the number produced byvalueOf
- Use
typeof
or comparison toundefined
rather than truthiness to test for undefined values
Things to Remember:
- Object wrappers for primitive types do not have the same behavior as their primitive values when compared for equality
- Getting and setting properties on primitives implicitly creates object wrappers
Coercion Rules for ==
Operator
Argument Type 1 | Argument Type 2 | Coercions |
---|---|---|
null |
undefined |
None; always true |
null or undefined |
Any other than null or undefined |
None; always false |
Primitive string, number, or boolean | Date object |
Primitive => number, Date => object => primitive (try toString and then valueOf ) |
Primitive string, number, or boolean | Non-Date object |
Primitive => number, non-Date object => primitive (try valueOf and then toString ) |
Primitive string, number, or boolean | Primitive string, number, or boolean | Primitive => number |
Things to Remember:
- The
==
operator applies a confusing set of implicit coercions when its arguments are of different types - Use
===
to make it clear to your readers that your comparison does not involve any implicit coercions - Use your own explicit coercions when comparing values of different types to make your program's behavior clearer
Things to Remember:
- semicolons are ever inferred before a
}
, at the end of a line, or at the end of a program - semicolons are only ever inferred when the next token can't be parsed
Things to Remember:
- JavaScript strings consist of 16-bit code units, not Unicode code points.
- Unicode code points 2^16 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.
- Use third-party libraries for writing code point-aware string manipulation
- whenever you are using a library that works with strings, consult the docs to see how it handles the full range of code points
Things to Remember:
- Avoid declaring global variables
- Declare variables as locally as possible
- Avoid adding properties to the global object
- Use the global object for platform feature detection
Things to Remember:
- Always declare new local variables with
var
- consider using lint tools to help check for unbound variables
Things to Remember:
- avoid using
with
statements - use short variable names for repeated access to an object
- explicitly bind local variables to object properties instead of implicitly binding them with a
with
statement
Things to Remember:
- functions can refer to variables defined in outer scopes
- closures can outlive the function that creates them
- closures internally store their outer variables by reference, and can both read and update those variables
JS doesn't support block scoping: variable definitions aren't scoped to their nearest enclosing statement or block, but rather to their containing function
Exception: exceptions - try
...catch
binds a caught exception to a variable that is scoped to just the catch
block
Things to Remember:
- Variable declarations within a block are implicitly hoisted to the top of the enclosing function
- redeclarations of a variable are treated as a single variable
- consider manually hoisting local variable declarations to avoid confusion
Things to Remember:
- Understand the difference between binding and assignment
- closures capture their outer variables by reference, not by value
- Use IIFEs to create local scope
- be aware of the cases where wrapping a block in an IIFE can change its behavior
Things to Remember:
- use named function expressions to improve stack traces in
Error
objects and debuggers - beware of pollution of function expression scope with
object.prototype
in ES3 and buggy JS environments - Beware of hoisting and duplicate allocation of named function expressions in buggy JS environments
- consider avoiding named function expressions or removing them before shipping
- if you're shipping in properly implemented ES5 environments, you've got nothing to worry about
Things to Remember:
- always keep function declarations 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
Things to Remember:
- avoid creating variables with
eval
that pollute the caller's scope - if
eval
code might create global variables, wrap the call in a nested function to prevent scope pollution
Things to Remember:
- wrap
eval
in a sequence expression with a useless literal to force the use of indirecteval
(0, eval)(src);
- prefer indirect
eval
to directeval
whenever possible
Things to Remember:
- method calls provide the object in which the method property is looked up as their receiver.
- Rarely useful: function calls provide the global object (or
undefined
for strict functions) as their receiver. - Constructors are called with
new
and receive a fresh object as their receiver
Things to Remember:
- Higher-order functions are functions that take other functions (callbacks) as args or return functions as their result
- familiarize yourself with higher-order functions in existing libraries
- learn to detect common coding patterns that can be replaced by higher-order functions
Things to Remember:
- Use
call
to call a function with a custom receiver - use
call
for calling methods that may not exist on a given object - use
call
for defining higher-order functions that allow clients to provide a receiver for the callback
variadic or variable-arity functions take any number of args (arity of function is the number of args it expects)
Things to Remember:
- use
apply
to call variadic functions with a computed array of elements - use first arg of
apply
to provide a receiver for variadic methods
Things to Remember:
- use implicit
arguments
object to implement variable-arity functions - consider providing additional fixed-arity versions of the variadic functions you provide so that consumers don't need to use
apply
method
function average() {
return averageOfArray(arguments);
}
function averageOfArray(arr) {
for (var i = 0, sum = 0, n = arr.length; i < n; i++) {
sum += a[i];
}
return sum / n;
}
named arguments for a function are aliases to their corresponding indices in the arguments
object
function callMethod(obj, method)
in this function, obj is an alias for arguments[0]
and method is an alias for arguments[1]
Things to Remember:
- never modify the
arguments
object - copy
arguments
object to a real array using[].slice.call(arguments)
before modifying it
Things to Remember:
- be aware of the function nesting level when referring to
arguments
- bind explicitly scoped reference to
arguments
in order to refer to it from nested functions
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++];
}
}
}
Things to Remember:
- beware that extracting a method doesn't bind the method's receiver to its object
- when passing an object's method to a higher-order function, use an anonymous function to call the method on the appropriate receiver
- use
bind
as a shorthand for creating a function bound to the appropriate receiver
Things to Remember:
- use
bind
to curry a function, to create a delegating function with a fixed subset of the required args - pass
null
orundefined
as the receiver argument to curry a function that ignores its receiver
function simpleURL(protocol, domain, path) {
return protocol + "://" + domain + "/" + path;
}
var urls = paths.map(function(path) {
return simpleUrl("http", siteDomain, path);
});
vs.
var urls = paths.map(simpleURL.bind(null, "http", siteDomain));
Things to Remember:
- never include local references in strings when sending them to APIs that execute them with
eval
- prefer APIs that accept functions to call rather than strings to
eval
Things to Remember:
- JS engines aren't required to produce accurate reflections of function source code via
toString
- never rely on precise details of function source, since different engines may produce different results from
toString
- the results of
toString
do not expose the values of local variables stored in a closure - in general, avoid using
toString
on functions
Things to Remember:
- avoid the nonstandard
arguments.caller
andarguments.callee
, because they are not reliably portable - avoid the nonstandard
caller
property of functions, because it does not reliably contain complete information about the stack
Things to Remember:
C.prototype
determines the prototype of objects created bynew C()
Object.getPrototypeOf(obj)
is the standard ES5 function for retrieving prototype of an objectobj.__proto__
is a nonstandard mechanism for retrieving the prototype of an object- a class is a design pattern consisting of a constructor function and an associated prototype
Things to Remember:
- prefer the standards-compliant
Object.getPrototypeOf
to the non-standard__proto__
property - implement
Object.getPrototypeOf
in non-ES5 environments that support__proto__
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__;
};
}
Things to Remember:
- never modify an object's
__proto__
property - use
Object.create
to provide a custom prototype for new objects
function User (name, passwordHash) {
if (!(this instanceof User)) {
return new User(name, passwordHash);
}
this.name = name;
this.passwordHash = passwordHash;
}
or
function User (name, passwordHash) {
var self = this instanceof User ? this : Object.create(User.prototype);
self.name = name;
self.passwordHash = passwordHash;
return self;
}
Object.create
is only available in ES5, it can be approximated in older environments by creating a local constructor and instantiating it with new
if (typeof Object.create === "undefined") {
Object.create = function (prototype) {
function C () {}
C.prototype = prototype;
return new C();
};
}
Things to Remember:
- Make a constructor agnostic to its caller's syntax by reinvoking itself with
new
or withObject.create
- Document clearly when a function expects to be called with new
Things to Remember:
- Storing methods on instance objects creates multiple copies of the functions, one per instance object
- prefer storing methods on prototypes over storing them on instance objects
Things to Remember:
- Closure variables are private, accessible only to local references.
- Use local variables as private data to enforce information hiding within methods.
function User (name, passwordHash) {
this.toString = function () {
return "[User ]" + name + "]";
};
this.checkPassword = function (password) {
return hash(password) === passwordHash;
};
}
Downside: In order for variables of the constructor to be in scope of the methods that use them, the methods must be placed on the instance object
Things to Remember:
- Mutable data can be problematic when shared, and prototypes are shared between all their instances.
- Store mutable per-instance state on instance objects
Things to Remember:
- the scope of
this
is always determined by its nearest enclosing function - use a local variable, such as (traditionally)
self
,me
, orthat
to make athis
-binding available to inner functions
Things to Remember:
- Call the superclass constructor explicitly from subclass constructors, passing
this
as the explicit receiver - use
Object.create
to construct the subclass prototype object to avoid calling the superclass constructor.
Things to Remember:
- Be aware of all property names used by your superclasses
- Never reuse a superclass property name in a subclass
Things to Remember:
- Inheriting from standard classes tend to break due to special internal properties such as [[Class]]
- Prefer delegating to properties instead of inheriting from standard classes
Things to Remember:
- Objects are interfaces; prototypes are implementations.
- Avoid inspecting the prototype structure of objects you don't control
- Avoid inspect properties that implement the internals of objects you don't control
Things to Remember:
- Avoid reckless monkey-patching
- document any monkey-patching performed by a library
- consider making monkey-patching optional by performing the modifications in an exported function
- use monkey-patching to provide polyfills for missing standard APIs
if (typeof Array.prototype.map !== "function") {
Array.prototype.map = function (f, thisArg) {
var result = [];
for (var i = 0, n = this.length; i < n; i++) {
result[i] = f.call(thisArg, this[i], i);
}
return result;
};
}
Things to Remember:
- Use object literals to construct lightweight dictionaries
- lightweight dictionaries should be direct descendants of
Object.prototype
to protect against prototype pollution infor...in
loops
Things to Remember:
- in ES5, use
Object.create(null)
to create prototype-free empty objects that are less susceptible to pollution - in older environments, consider using
{ __proto__: null }
- beware that
__proto__
is neither standard nor entirely portable and may be removed in future JavaScript environments - never use the name "
__proto__
" as a dictionary key since some environments treat this property specially
function Dict (elements) {
this.elements = elements || {};
this.hasSpecialProto = false;
this.specialProto = undefined;
}
Dict.prototype.has = function (key) {
if (key === "__proto__") {
return this.hasSpecialProto;
}
return {}.hasOwnProperty.call(this.elements, key);
};
Dict.prototype.get = function (key) {
if (key === "__proto__") {
return this.specialProto;
}
return this.has(key)
? this.elements[key]
: undefined;
};
Dict.prototype.set = function (key, val) {
if (key === "__proto__") {
this.hasSpecialProto = true;
this.specialProto = val;
} else {
this.elements[key] = val;
}
};
Dict.prototype.remove = function (key) {
if (key === "__proto__") {
this.hasSpecialProto = false;
this.specialProto = undefined;
} else {
delete this.elements[key];
}
};
Things to Remember:
- Use
hasOwnProperty
to protect against prototype pollution - use lexical scope and
call
to protect against overriding of thehasOwnProperty
method - consider implementing dictionary operations in a class that encapsulates the boilerplate
hasOwnProperty
tests - use a dictionary class to protect against the use of "
__proto__
" as a key
Things to Remember:
- 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
Things to Remember:
- avoid adding properties to
Object.prototype
- consider writing a function instead of an
Object.prototype
method - if you do add properties to
Object.prototype
, use ES5'sObject.defineProperty
to define them as nonenumerable properties.
Object.defineProperty(Object.prototype, "allKeys", {
value: function() {
var resul = [];
for (var key in this) {
result.push(key);
}
return result;
},
writable: true,
enumerable: false,
configurable: true
});
Things to Remember:
- make sure not to modify an object while enumerating its properties with a
for...in
loop - use a
while
loop or classic for loop instead of afor...in
loop when iterating over an object whose contents might change during the loop - for predictable enumeration over a changing data structure, consider using a sequential data structure such as an array instead of a dictionary object
Things to Remember:
- always use a
for
loop rather than afor...in
loop for iterating over the indexed properties of an array - consider storing the
length
property of an array in a local variable before a loop to avoid recomputing the property lookup
for (var i = 0, n = scores.length; i < n; i++) { ... }
vs.
for (var i = 0; i < scores.length; i++) { ... }
Things to Remember:
- use iteration methods such as
Array.prototype.forEach
andArray.prototype.map
in place offor
loops to make code more readable and avoid duplicating loop control logic. - use custom iteration functions to abstract common loop patterns that aren't provided by the standard library
- traditional loops can still be appropriate in cases where early exit is necessary; alternatively,
some
andevery
methods can be used for early exit via short-circuiting
function takeWhile(a, pred) {
var result = [];
a.every(function(x, i) {
if (!pred(x)) {
return false;
}
result[i] = x;
return true;
});
return result;
}
- Reuse generic
Array
methods on array-like objects by extracting method objects and using theircall
method - any object can be used with generic
Array
methods if it has indexed properties and an appropriatelength
property
Examples:
var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
var result = Array.prototype.map.call(arrayLike, function (s) {
return s.toUpperCase();
});
// ["A", "B", "C"]
// Strings act like immutable arrays.
// Array.prototype methods that don't modify their array work with strings.
var result = Array.prototype.map.call("abc", function (s) {
return s.toUpperCase();
});
// ["A", "B", "C"]
// [].concat is special.
// if [[Class]] of its arguments is a true array, its contents are concatenated,
// else they are added as a single element
// use slice to convert an array like object into an array
function namesColumn() {
return ["Names"].concat(arguments);
}
namesColumn("Alice", "Bob", "Chris");
// ["Names", { 0: "Alice", 1: "Bob", 2: "Chris" }]
vs.
function namesColumn() {
return ["Names"].concat([].slice.call(arguments));
}
namesColumn("Alice", "Bob", "Chris");
// ["Names", "Alice", "Bob", "Chris"]
Issues:
// Array variable can be rebound
function f (Array) {
return new Array(1, 2, 3, 4, 5);
}
f(String); // new String(1)
// global Array variable can be modified
Array = String;
new Array(1, 2, 3, 4, 5); // new String(1)
// same as ["hello"]
new Array("hello")
// NOT the same as [17]
new Array(17)
Things to Remember:
- the
Array
constructor behaves differently if its only argument is a number- it attempts to create an array with no elements whose
length
property is the given argument
- it attempts to create an array with no elements whose
- use array literals instead of the
Array
constructor
Things to Remember:
- use consistent conventions for variable names and function signatures
- don't deviate from convetions your users are likely to encounter in other parts of their development platform
Things to Remember:
- 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
Things to Remember:
- use options objects 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 objects
function Alert(parent, message, opts) {
opts = opts || {};
this.width = opts.width === "undefined" ? 320 : opts.width;
this.height = opts.height === "undefined" ? 240 : opts.height;
this.x = opts.x === "undefined" ? (parent.width / 2) - (this.width / 2) : opts.x;
this.y = opts.y === "undefined" ? (parent.height / 2) - (this.height / 2) : opts.y;
this.title = opts.title || "Alert";
this.titleColor = opts.titleColor || "gray";
this.modal = !!opts.modal;
this.message = message;
}
Or
// in some libraries
function Alert(parent, message, opts) {
opts = extend({
width: 320,
height: 240
}, opts);
opts = extend({
x: (parent.width / 2) - (opts.width / 2),
y: (parent.height / 2) - (opts.height / 2),
title: "Alert",
titleColor: "gray",
modal: false
}, opts);
extend(this, opts);
this.message = message;
}
A typical extend
function extend(target, source) {
if (source) {
for (var key in source) {
var val = source[key];
if (typeof val !== "undefined") {
target[key] = val;
}
}
}
return target;
}
A famous stateful API - HTML Canvas library
Example: Canvas holds some state about attributes of color, text style etc.
c.fillText("text1", 0, 0); // default color
c.fillStyle = "blue";
c.fillText("text2", 0, 30); // blue text
c.fillStyle = "black";
c.fillText("text3", 0, 60); // back to black
Issues:
c.fillStyle = "blue";
drawMyImage(c); // has drawMyImage changed state of c elsewhere in the program?
c.fillText("hello world", 0, 0);
What'f we could do...
// much better
c.fillText("text1", 0, 0); // default color
c.fillText("text2", 0, 30, { fillStyle: "blue" }); // blue
c.fillText("text3", 0, 60);
Things to Remember:
- prefer stateless APIs where possible
- when providing stateful APIs, document the relevant state that each operation depends on
"In duck typing, an object's suitability is determined by the presence of certain methods and properties (with appropriate meaning), rather than the actual type of the object...a programmer is only concerned with ensuring that objects behave as demanded of them in a given context, rather than ensuring that they are of a specific type. For example, in a non-duck-typed language, one would create a function that requires that the object passed into it be of type Duck, in order to ensure that that function can then use the object's walk and quack methods. In a duck-typed language, the function would take an object of any type and simply call its walk and quack methods, producing a run-time error if they are not defined. Instead of specifying types formally, duck typing practices rely on documentation, clear code, and testing to ensure correct use." -- wikipedia
"In Ruby, we rely less on the type (or class) of an object and more on its capabilities. Hence, Duck Typing means an object type is defined by what it can do, not by what it is. Duck Typing refers to the tendency of Ruby to be less concerned with the class of an object and more concerned with what methods can be called on it and what operations can be performed on it. In Ruby, we would use respond_to? or might simply pass an object to a method and know that an exception will be raised if it is used inappropriately." -- ruby example
Things to Remember:
- use structural typing (duck typing) for flexible object interfaces
- avoid inheritance when structural interfaces are more flexible and lightweight
- use mock objects, alternative implementations of interfaces that provide repeatable behavior, for unit testing
How to test for true arrays:
ES5 supports Array.isArray
function, testing whether the internal [[Class]] property of an object is "Array"
In environments that don't support ES5:
function isArray(x) {
return Object.prototype.toString.call(x) === "[object Array]";
}
Things to Remember:
- never overload structural types with other overlapping types
- when overloading a structural type with other types, test for the other types first
- accept true arrays instead of array-like objects when overloading with other object types
- document whether your API accepts true arrays or array-like values
- use ES5's
Array.isArray
to test for true arrays
// accepts numbers and array-like objects
BitVector.prototype.enable = function(x) {
if (typeof === "number") {
this.enableBit(x);
} else {
for (var i = 0, n = x.length; i < n; i++) {
this.enableBit(x[i]);
}
}
};
// accepts strings, true arrays, and (nonarray) objects
StringSet.prototype.add = function(x) {
if (typeof x === "string") {
this.addString(x);
} else if (Array.isArray(x)) {
x.forEach(function(s) {
this.addString(s);
}, this);
} else {
for (var key in x) {
this.addString(key);
}
}
};
Things to Remember:
- avoid mixing coercions with overloading
- consider defensively guarding against unexpected inputs
var guard = {
guard: function(x) {
if (!this.test(x)) {
throw new TypeError("expected " + this);
}
}
};
var uint32 = Object.create(guard);
uint32.test = function(x) {
return typeof x === "number" && x === (x >>> 0);
};
uint32.toString = function() {
return "unint32";
};
// arrayLike guard object
var arrayLike = Object.create(guard);
arrayLike.test = function(x) {
return typeof x === "object" && x && uint32.test(x.length);
};
arrayLike.toString = function(x) {
return "array-like object";
};
// chain methods
guard.or = function(other) {
var result = Object.create(guard);
var self = this;
result.test = function(x) {
return self.test(x) || other.test(x);
};
var desciption = this + " or " + other;
result.toString = function() {
return description;
};
return result;
}
Things to Remember:
- use method chaining to combine stateless operations
- support method chaining by designing stateless methods that produce new objects
- support method chaining in stateful methods by returning this
Things to Remember:
- Asynchronous APIs take callbacks to defer processing of expensive operations and avoid blocking the main app
- JavaScript accepts events concurrently but processes event handlers sequentially using an event queue
- never use blocking I/O in an application's event queue
Things to Remember:
- use nested or named callbacks to perform several async operatins in sequence
- try to strike a balance between excessive nesting of callbacks and awkward naming of non-nested callbacks
- avoid sequencing operations that can be performed concurrently
Things to Remember:
- 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
Things to Remember:
- loops can't be async
- use recursive functions to perform iterations in separate turns of the event loop
- recursion performed in separate turns of the event loop doesn't overflow the call stack
// the callback is always invoked in a separate turn of the event loop
// each turn of the event loop invokes its event handler with the call stack initially empty
// therefore, this won't eat up call stack space, no matter the # of iterations
function downloadOneAsync(urls, onsuccess, onfailure) {
var n = urls.length;
function tryNextURL(i) {
if (i >= n) {
onfailure("all downloads failed");
return;
}
downloadAsync(urls[i], onsuccess, function() {
tryNextURL(i + 1);
});
}
tryNextURL(0);
}
Things to Remember:
- avoid expensive algorithms in the main event queue
- on platforms that support it, the
Worker
API can be used for running long computations in a separate event queue - when the
Worker
API isn't available or is too costly, considering breaking up computations across multiple turns of the event loop
Things to Remember:
- events in a JavaScript app occur nondeterministically (in an unpredictable order)
- use a counter to avoid data races in concurrent operations
function downloadAllAsync(urls, onsuccess, onerror) {
var pending = urls.length;
var result = [];
if (pending === 0) {
setTimeout(onsuccess.bind(null, result), 0);
return;
}
urls.forEach(function(url, i) {
downloadAsync(url, function(text) {
if (result) {
result[i] = text; // store at fixed index
pending--; // register the success
if (pending === 0) { // vs checking urls.length === result.length
onsuccess(result);
}
}
}, function(error) {
if (result) {
result = null;
onerror(error);
}
});
});
}
Use a counter to track pending operations. Otherwise, if the operation at index 2 finishes first, for example, result
acquires a property at index 2, and result.length
will immediately be updated to 3. Checking result.length
doesn't give us accurate picture as to how many operations have finished.
Things to Remember:
- never call an async callback synchronously, even if the data is immediately available
- calling an async callback synchronously disrupts the expected sequence of operations and can lead to unexpected interleaving of code
- calling an async callback synchronously can lead to stack overflows or mishandled exceptions
- use an async API such as
setTimeout
to schedule an async callback to run in another turn
var cache = new Dict();
function downloadCachingAsync(url, onsuccess, onerror) {
if (cache.has(url)) {
var cached = cache.get(url);
setTimeout(onsuccess.bind(null, cached), 0); // see Item 25 for this pattern
return;
}
return downloadAsync(url, function(file) {
cache.set(url, file);
onsuccess(file);
}, onerror);
}
promises, deferreds, futures
Things to Remember:
- Promises represent eventual values, or concurrent computations that eventually produce a result
- use promises to compose different concurrent operations
- use promise APIs to avoid data races
- use
select
(a.k.a.choose
) for situations where an intentional race condition is required
Examples: Promises can be used to not only cause effect, but produce results. Construct new promises from existing promises:
var fileP = downloadP("file.txt");
var lengthP = fileP.then(function(file) {
return file.length;
});
lengthP.then(function(length) {
console.log("length: " + length);
});
Joining results of multiple promises:
var filesP = join(downloadP("file1.txt"),
downloadP("file2.txt"),
downloadP("file3.txt"));
filesP.then(function(files) {
console.log("file1: " + files[0]);
console.log("file2: " + files[1]);
console.log("file3: " + files[2]);
});
similarly, using when
...
var fileP1 = downloadP("file1.txt");
var fileP2 = downloadP("file2.txt");
var fileP3 = downloadP("file3.txt");
when([fileP1, fileP2, fileP3], function(files) {
console.log("file1: " + files[0]);
console.log("file2: " + files[1]);
console.log("file3: " + files[2]);
});
Create race conditions purposefully.
select
takes several promises and produces a promise whose value is whichever result becomes available first. It 'races' several promises against each other
var fileP = select(downloadP("http://example1.com/file.txt"),
downloadP("http://example2.com/file.txt"),
downloadP("http://example3.com/file.txt"));
fileP.then(function(file) {
console.log("file: " + file);
});
Provide timeouts to abort operations that take too long, and pass a single error callback for the entire sequence of operations
var fileP = select(downloadP("file.txt"), timeoutErrorP(2000));
fileP.then(function(file) {
console.log("file: " + file);
}, function(error) {
console.log("I/O error or timeout: " + error);
});