Skip to content

Instantly share code, notes, and snippets.

@conanza
Last active March 3, 2017 10:33
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save conanza/828b131de50f7b7aa3ab to your computer and use it in GitHub Desktop.
Save conanza/828b131de50f7b7aa3ab to your computer and use it in GitHub Desktop.
Effective JavaScript notes

Effective JavaScript

Contents

1. Accustoming Yourself to JavaScript

2. Variable Scope

3. Working with Functions

4. Objects and Prototypes

5. Arrays and Dictionaries

6. Library and API Design

7. Concurrency

1. Accustoming Yourself to JavaScript

Item 1: Know which JavaScript You're Using

"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:

  1. Never concatenate strict files and nonstrict files
  2. concatenate files by wrapping their bodies in IIFE's
  3. 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

Item 2: Understand JavaScript's Floating-Point Numbers

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.

Item 3: Beware of Implicit Coercions

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 via toString
  • Objects with valueOf methods should implement a toString method that provides a string representation of the number produced by valueOf
  • Use typeof or comparison to undefined rather than truthiness to test for undefined values

Item 4: Prefer Primitives to Object Wrappers

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

Item 5: Avoid Using == with Mixed Types

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

Item 6: Learn the Limits of Semicolon Insertion

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

Item 7: Think of Strings As Sequences of 16-Bit Code Units

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

2. Variable Scope

Item 8: Minimize Use of the Global Object

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

Item 9: Always Declare Local Variables

Things to Remember:

  • Always declare new local variables with var
  • consider using lint tools to help check for unbound variables

Item 10: Avoid with

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

Item 11: Get Comfy with Closures

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

Item 12: Understand Variable Hoisting

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

Item 13: Use Immediately Invoked Function Expressions to Create Local Scopes

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

Item 14: Beware of Unportable Scoping of Named Function Expressions

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

Item 15: Beware of Unportable Scoping of Block-Local Function Declarations

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

Item 16: Avoid Creating Local Variables with eval

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

Item 17: Prefer Indirect eval to Direct eval

Things to Remember:

  • wrap eval in a sequence expression with a useless literal to force the use of indirect eval
(0, eval)(src);
  • prefer indirect eval to direct eval whenever possible

3. Working with Functions

Item 18: Understand the Difference between Function, Method, and Constructor Calls

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

Item 19: Get Comfy Using Higher-Order Functions

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

Item 20: Use call to Call Methods with a Custom Receiver

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

Item 21: Use apply to Call Functions with Different Numbers of Arguments

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

Item 22: Use arguments to Create Variadic Functions

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;
}

Item 23: Never Modify the arguments Object

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

Item 24: Use a Variable to Save a Reference to arguments

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++];
    }
  }
}

Item 25: Use bind to Extract Methods with a Fixed Receiver

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

Item 26: Use bind to Curry Functions

Things to Remember:

  • use bind to curry a function, to create a delegating function with a fixed subset of the required args
  • pass null or undefined 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));

Item 27: Prefer Closures to Strings for Encapsulating Code

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

Item 28: Avoid Relying on the toString Method of Functions

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

Item 29: Avoid Nonstandard Stack Inspection Properties

Things to Remember:

  • avoid the nonstandard arguments.caller and arguments.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

4. Objects and Prototypes

Item 30: Understand the Difference between prototype, getPrototypeOf, and __proto__

Things to Remember:

  • C.prototype determines the prototype of objects created by new C()
  • Object.getPrototypeOf(obj) is the standard ES5 function for retrieving prototype of an object
  • obj.__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

Item 31: Prefer Object.getPrototypeOf to __proto__

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__;
  };
}

Item 32: Never Modify __proto__

Things to Remember:

  • never modify an object's __proto__ property
  • use Object.create to provide a custom prototype for new objects

Item 33: Make Your Constructors new-Agnostic

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 with Object.create
  • Document clearly when a function expects to be called with new

Item 34: Store Methods on Prototypes

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

Item 35: Use Closures to Store Private Data

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

Item 36: Store Instance State Only on Instance Objects

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

Item 37: Recognize the Implicit Binding of this

Things to Remember:

  • the scope of this is always determined by its nearest enclosing function
  • use a local variable, such as (traditionally) self, me, or that to make a this-binding available to inner functions

Item 38: Call Superclass Constructors from Subclass Constructors

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.

Item 39: Never Reuse Superclass Property Names

Things to Remember:

  • Be aware of all property names used by your superclasses
  • Never reuse a superclass property name in a subclass

Item 40: Avoid Inheriting from Standard Classes

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

Item 41: Treat Prototypes As an Implementation Detail

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

Item 42: Avoid Reckless Monkey-Patching

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;
  };
}

5. Arrays and Dictionaries

Item 43: Build Lightweight Dictionaries from Direct Instances of Object

Things to Remember:

  • 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

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

Item 45: Use hasOwnProperty to Protect Against Prototype Pollution

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 the hasOwnProperty 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

Item 46: Prefer Arrays to Dictionaries for Ordered Collections

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

Item 47: Never Add Enumerable Properties to Object.prototype

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's Object.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
});

Item 48: Avoid Modifying an Object During Enumeration

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 a for...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

Item 49: Prefer for Loops to for...in Loops for Array Iteration

Things to Remember:

  • always use a for loop rather than a for...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++) { ... }

Item 50: Prefer Iteration Methods to Loops

Things to Remember:

  • use iteration methods such as Array.prototype.forEach and Array.prototype.map in place of for 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 and every 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;
}

Item 51: Reuse Generic Array Methods on Array-Like Objects

  • Reuse generic Array methods on array-like objects by extracting method objects and using their call method
  • any object can be used with generic Array methods if it has indexed properties and an appropriate length 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"]

Item 52: Prefer Array Literals to the Array Constructor

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
  • use array literals instead of the Array constructor

6. Library and API Design

Item 53: Maintain Consistent Conventions

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

Item 54: Treat undefined as "No Value"

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 or null, to represent application-specific flags
  • test for undefined instead of checking arguments.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

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;
}

Item 56: Avoid Unnecessary State

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

Item 57: Use Structural Typing for Flexible Interfaces

"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

Item 58: Distinguish between Array and Array-Like

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);
    }
  }
};

Item 59: Avoid Excessive Coercion

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;
}

Item 60: Support Method Chaining

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

7. Concurrency

Item 61: Don't Block the Event Queue on I/O

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

Item 62: Use Nested or Named Callbacks for Asynchronous Sequencing

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

Item 63: Be Aware of Dropped Errors

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

Item 64: Use Recursion for Asynchronous Loops

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);
}

Item 65: Don't Block the Event Queue on Computation

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

Item 66: Use a Counter to Perform Concurrent Operations

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.

Item 67: Never Call Asynchronous Callbacks Synchronously

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);
}

Item 68: Use Promises for Cleaner Asynchronous Logic

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);
});

top

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment