Michael Ficarra
July 03, 2013
150

Select Ways to Harness the Power of JavaScript

A GRPN brown bag talk based on Dave Herman's excellent Effective JavaScript book.

July 03, 2013

Transcript

1. Select Ways to Harness the Power of JavaScript based largely

on Effective JavaScript by David Herman
2. Always Declare Local Variables function score(player) { sum = 0;

for (i = 0, n = player.levels.length; i < n; i++) { sum += player.levels[i].score; } return sum; } function averageScore(players) { sum = 0; for (i = 0, n = players.length; i < n; i++) { sum += score(players[i]); } return sum / n; }
3. Minimize Use of the Global Object var i, n, sum;

// globals function score(player) { sum = 0; for (i = 0, n = player.levels.length; i < n; i++) { sum += player.levels[i].score; } return sum; } var i, n, sum; // same globals as score! function averageScore(players) { sum = 0; for (i = 0, n = players.length; i < n; i++) { sum += score(players[i]); } return sum / n; }
4. Minimize Use of the Global Object function score(player) { var

i, n, sum; sum = 0; for (i = 0, n = player.levels.length; i < n; i++) { sum += player.levels[i].score; } return sum; } function averageScore(players) { var i, n, sum; sum = 0; for (i = 0, n = players.length; i < n; i++) { sum += score(players[i]); } return sum / n; }
5. Basic JavaScript Scope Chains var x = 10; (function foo()

{ var y = 20; (function bar() { var z = 30; console.log(x + y + z); })(); })();
6. Global Object is Root of Scope Chain this; // [global

object] foo; // ReferenceError this.foo; // undefined foo = "global foo"; this.foo; // "global foo" var bar = "global bar"; this.bar= "changed"; bar; // "changed" typeof hasOwnProperty; // "function"
7. Avoid with var w = 20, y = 30; (function

foo() { var w = 40, x = 100; with (objWithNondeterministicProps) { console.log(w, x, y, z); // ?, ?, ?, ? } })();
8. Avoid with var w = 20, y = 30; (function

foo() { var w = 40, x = 100; with (objWithNondeterministicProps) { console.log(w, x, y); // ?, ?, ? z = 40 } console.log(z); // ? })();
9. Avoid with Object.prototype.x = 10; var w = 20, y

= 30; // "x" is from `Object.prototype` because // the global object inherits from it console.log(x); // 10 (function foo() { var w = 40, x = 100; // "x" is from `Object.prototype` // because {z: 50} inherits from it with ({z: 50}) { console.log(w, x, y, z); // 40, 10, 30, 50 } console.log(x, w); // 100, 40 console.log(global.w); // 20 })();
10. Get Comfortable with Closures function sandwichMaker() { var magicIngredient =

"peanut butter"; return function make(filling) { return magicIngredient + " and " + filling; }; } var f = sandwichMaker(); f("jelly"); // "peanut butter and jelly" f("bananas"); // "peanut butter and bananas" f("marshmallows"); // "peanut butter and marshmallows"
11. Get Comfortable with Closures function sandwichMaker(magicIngredient) { return function make(filling)

{ return magicIngredient + " and " + filling; }; } var hamAnd = sandwichMaker("ham"); hamAnd("cheese"); // "ham and cheese" hamAnd("mustard"); // "ham and mustard" var turkeyAnd = sandwichMaker("turkey"); turkeyAnd("Swiss"); // "turkey and Swiss" turkeyAnd("Provolone"); // "turkey and Provolone"
12. Get Comfortable with Closures function baz() { var x =

1; return { foo: function foo() { return ++x; }, bar: function bar() { return --x; } }; } var closures = baz(); closures.foo(); // 2 closures.bar(); // 1

14. Understand Variable Hoisting var outerErr; try { throw "error"; }

catch(innerErr) { typeof outerErr; // "undefined" typeof innerErr; // "string" outerErr = innerErr; } typeof outerErr; // "string" typeof innerErr; // "undefined"
15. Use IIFEs to Create Local Scopes function wrapElements(a) { var

result = []; for (var i = 0, n = a.length; i < n; i++) { result[i] = function() { return a[i]; }; } return result; } var array = [10, 20, 30, 40, 50]; var wrapped = wrapElements(array); wrapped[0](); // undefined wrapped[1](); // undefined // etc. array.push(60); wrapped[0](); // 60 wrapped[1](); // 60 // etc.
16. Use IIFEs to Create Local Scopes function wrapElements(a) { var

result = []; for (var i = 0, n = a.length; i < n; i++) { (function(k) { result[i] = function() { return a[k]; }; })(i); } return result; } var array = [10, 20, 30, 40, 50]; var wrapped = wrapElements(array); wrapped[0](); // 10 wrapped[1](); // 20 wrapped[2](); // 30 // etc.
17. Understand the Differences Between FDs, NFEs, and FEs // Function

Declarations typeof a; // "function" function a(){} function b(){ typeof a; // "function" typeof b; // "function" typeof c; // "function" typeof d; // "function" function c(){} // ... function d(){} }
18. Understand the Differences Between FDs, NFEs, and FEs // Named

Function Expressions (function a(){}); void function a(){}; fn(function a(){}); typeof a; // "undefined" typeof f; // "undefined" typeof g; // "undefined" var f = function g(){ typeof f; // "function" typeof g; // "function" f === g; // true }; typeof f; // "function" typeof g; // "undefined"
19. Understand the Differences Between FDs, NFEs, and FEs // Function

Expressions (function(){}); void function(){}; 0, function(){}; +function(){}; fn(function(){}); typeof f; // "undefined" var f = function(){ typeof f; // "function" }; typeof f; // "function"
20. Beware of Block-Local FDs function f() { return "global"; }

function test(x) { var result = []; if (x) { function f() { return "local"; } // illegal syntax result.push(f()); } result.push(f()); return result; } test(true); // undefined behaviour test(false); // undefined behaviour
21. Prefer Indirect eval to Direct eval var x = "global";

function testDirect() { var x = "local"; return eval("x"); // direct eval } testDirect(); // "local" function testIndirect() { var x = "local"; var f = eval; return f("x"); // indirect eval } testIndirect(); // "global" (0,eval)("string"); // terse indirect eval
22. Use call/apply to Call Methods With a Given Context //

goal: call `f` with `o` as the context o.temporary = f; // what if o.temporary already existed? var result = o.temporary(arg1, arg2, arg3); delete o.temporary; // what if o.temporary already existed? // instead, use `call` f.call(o, arg1, arg2, arg3);
23. Use apply to Call Methods With a Dynamic Number of

Arguments // mean is a variadic function function mean(){ var sum = [].reduce.call(arguments, function(memo, n){ return memo + n; }, 0); return sum / arguments.length; } mean(1, 2, 3); // 2 mean(1); // 1 mean(3, 1, 4, 1, 5, 9, 2, 6, 5); // 4 mean(2, 7, 1, 8, 2, 8, 1, 8); // 4.625 // goal: compute the mean of the values in this list var array = [2, 7, 1, 8, 2, 8, 1, 8]; mean.apply(null, array); // 4.625
24. Use apply to Call Methods With a Dynamic Number of

Arguments // this becomes a little more difficult with constructors function NumberContainer(){ this.nums = [].map.call(arguments, function(n){ return +n; }); } NumberContainer.prototype.mean = function(){ return [].reduce.call(arguments, function(memo, n){ return memo + n; }, 0) / arguments.length; }; new NumberContainer(1, 2, 3).mean(); // 2 // goal: compute the mean of the values in this list var array = [2, 7, 1, 8, 2, 8, 1, 8]; var o = Object.create(NumberContainer.prototype); NumberContainer.apply(o, array); o.mean(); // 4.625
25. Understand the Differences Between Functions, Methods, and Constructors // functions

don't mention `this` function hello(username) { return "hello, " + username; } hello("Keyser Söze"); // "hello, Keyser Söze" // "methods" mention `this` var o = { hello: function() { return "hello, " + this.username; }, username: "Hans Gruber" }; o.hello(); // "hello, Hans Gruber" // constructors are meant to be used with `new` and // usually have a meaningful prototype property function Cow(){} Cow.prototype = Object.create(Animal.prototype); new Cow;
26. Understand the Differences Between Functions, Methods, and Constructors // watch

your constructor return values var returnValue; function A(){ return returnValue; } Object(returnValue) === returnValue; // false new A instanceof A; // true returnValue = null; Object(returnValue) === returnValue; // false new A instanceof A; // true returnValue = "string"; Object(returnValue) === returnValue; // false new A instanceof A; // true returnValue = {}; Object(returnValue) === returnValue; // true new A instanceof A; // false
27. Understand the Difference between prototype, getPrototypeOf, and __proto__ ▪ C.prototype

is used to establish the [[Prototype]] of objects created by new C. ▪ Object.getPrototypeOf(o) is the standard ES5 mechanism for retrieving o’s [[Prototype]] object. ▪ o.__proto__ is a non-standard mechanism for retrieving or re-assigning o’s [[Prototype]] object.
28. Understand the Difference between prototype, getPrototypeOf, and __proto__ function User(name,

passwordHash) { this.name = name; this.passwordHash = passwordHash; } User.prototype.checkPassword = function(password) { return hash(password) === this.passwordHash; }; var u = new User("sfalken", "0ef33ae791068ec64b502d6cb0191387"); // two `u.[[Prototype]] === User.prototype` tests u instanceof User; // true Object.getPrototypeOf(u) === User.prototype; // true // avoid __proto__; different behaviours in different environments: "__proto__" in {}; // ? "__proto__" in Object.create(null); // ?
29. Never Modify the 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 // instead, make a shallow copy with Array.prototype.slice var args = [].slice.call(arguments); // bonus: `args` is a *real* array typeof args.pop; // "function" typeof arguments.pop; // "undefined"
30. Store Methods on Prototypes function User(name, passwordHash) { this.name =

name; this.toString = function() { return "[User " + this.name + "]"; }; this.checkPassword = function(password) { return hash(password) === passwordHash; }; } var u0 = new User(/* ... */); var u1 = new User(/* ... */); var u2 = new User(/* ... */);
31. Store Methods on Prototypes function User(name, passwordHash) { this.name =

name; this.checkPassword = function(password) { return hash(password) === passwordHash; }; } User.prototype.toString = function() { return "[User " + this.name + "]"; }; var u0 = new User(/* ... */); var u1 = new User(/* ... */); var u2 = new User(/* ... */);
32. 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! }); };
33. 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); }, this); // forward outer this-binding to callback };
34. 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/); var self = this; // save a reference to outer this-binding return lines.map(function(line) { return line.split(self.regexp); // use outer this }); };
35. 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); }.bind(this)); // bind to outer this-binding };
36. Do Not Call Superclass Constructors Outside Subclass Constructors function Actor(scene,

x, y) { this.scene = scene; this.x = x; this.y = y; scene.register(this); } function SpaceShip(scene, x, y) { Actor.call(this, scene, x, y); this.points = 0; } // In order for SpaceShip to be a proper subclass of Actor, // its prototype must inherit from Actor.prototype. SpaceShip.prototype = new Actor(/* ?? */); SpaceShip.prototype = Object.create(Actor.prototype);
37. Do Not Call Superclass Constructors Outside Subclass Constructors function Actor(scene,

x, y) { this.scene = scene; this.x = x; this.y = y; scene.register(this); } function SpaceShip(scene, x, y) { Actor.call(this, scene, x, y); this.points = 0; } // ES3 environments don't have `Object.create`, so we'll have to fake it SpaceShip.prototype = (function(ctor, superclass){ return new (ctor.prototype = superclass.prototype, ctor); }(function(){}, Actor));
38. Avoid Inheriting from Standard Classes function Dir(path, entries) { this.path

= path; for (var i = 0, n = entries.length; i < n; i++) { this[i] = entries[i]; } } // attempt to subclass Array Dir.prototype = Object.create(Array.prototype); var dir = new Dir("/tmp/mysite", ["index.html", "script.js"]); // Unfortunately, this approach breaks the `length` and // internal `[[Class]]` properties of arrays: dir.length; // 0 {}.toString.call(dir); // "[object Object]" {}.toString.call([]); // "[object Array]"
39. Use null Prototypes to Prevent Pollution var o; function C()

{ } C.prototype = null; o = new C; Object.getPrototypeOf(o) === null; // false Object.getPrototypeOf(o) === Object.prototype; // true o = Object.create(null); Object.getPrototypeOf(o) === null ; // true o instanceof Object; // false
40. Use hasOwnProperty to Protect Against Prototype Pollution var dict =

{}; "alice" in dict; // membership test (including prototype) dict.alice; // retrieval dict.alice = 24; // update delete dict.alice; // deletion "alice" in dict; // false "toString" in dict; // true dict.hasOwnProperty("alice"); // false dict.hasOwnProperty("toString"); // false dict.hasOwnProperty = 10; dict.hasOwnProperty("alice"); // ERROR // safe own-property membership test {}.hasOwnProperty.call(dict, "alice"); // false {}.hasOwnProperty.call(dict, "toString"); // false
41. Use hasOwnProperty to Protect Against Prototype Pollution function Map() {

var data = {}; function keyName(userKey) { return '\$' + userKey; // protect against setting `data.__proto__` } this.set = function(key, value) { data[keyName(key)] = value; return this; }; this.get = function(key) { return this.has(key) ? data[keyName(key)] : null; }; this.has = (function(){ var hop = {}.hasOwnProperty; return function(key) { return hop.call(data, keyName(key)); }; }()); }
42. Do Not Rely On for-in Enumeration Order function report(highScores) {

var lines = []; for (var i in highScores) { // undefined enumeration order var score = highScores[i]; lines.push((+i + 1) + ". " + score.name + ": " + score.points); } return lines.join("\n"); } report([ { name: "Hank", points: 1110100 }, { name: "Steve", points: 1064500 }, { name: "Billy", points: 1050200 } ]);
43. Do Not Rely On for-in Enumeration Order function report(highScores) {

var lines = []; for (var i = 0, n = highScores.length; i < n; ++i) { var score = highScores[i]; lines.push((i + 1) + ". " + score.name + ": " + score.points); } return lines.join("\n"); } report([ { name: "Hank", points: 1110100 }, { name: "Steve", points: 1064500 }, { name: "Billy", points: 1050200 } ]);
44. Do Not Rely On for-in Enumeration Order var scores =

[98, 74, 85, 77, 93, 100, 89], mean, total; total = 0; for (var score in scores) { total += score; } mean = total / scores.length; mean; // 17636.571428571428 total; // "00123456" total = 0; for (var i = 0 , n = scores.length; i < n; ++i) { total += scores[i]; } mean = total / scores.length; // 88
45. Never Add Enumerable Properties to Object.prototype Object.prototype.allKeys = function ()

{ var result = []; for (var key in this) { result.push(key); } return result; }; // Sadly, this method pollutes even its own result: ({ a: 1, b: 2, c: 3 }).allKeys(); // ["allKeys", "a", "b", "c"]

to Object.prototype function allKeys(obj) { var result = []; for (var key in obj) { result.push(key); } return result; } allKeys({ a: 1, b: 2, c: 3 }); // ["a", "b", "c"]
47. Never Add Enumerable Properties to Object.prototype // if you absolutely

must add it to Object.prototype, use defineProperty Object.defineProperty(Object.prototype, "allKeys", { value: function () { var result = []; for (var key in this) { result.push(key); } return result; }, writable: true, enumerable: false, // non-enumerable configurable: true }); ({ a: 1, b: 2, c: 3 }).allKeys(); // ["a", "b", "c"]
48. Prefer Iteration Methods to Loops // off-by-one errors suck for

(var i = 0; i <= n; ++i) { ... } // extra end iteration for (var i = 1; i < n; ++i) { ... } // missing first iteration for (var i = n; i >= 0; --i) { ... } // extra start iteration for (var i = n - 1; i > 0; --i) { ... } // missing last iteration // replace loops with iteration methods for (var i = 0, n = players.length; i < n; i++) { ++players[i].score; } players.forEach(function(player) { ++player.score; }); // iteration methods also abstract common patterns var trimmed = []; for (var i = 0, n = input.length; i < n; i++) { trimmed.push(input[i].trim()); } var trimmed = input.map(function(s) { return s.trim(); });
49. Reuse Generic Array Methods on Array- Like Objects // array-like:

uint32 `length` property, length > largest numeric property function highlight() { [].forEach.call(arguments, function(widget) { widget.setBackground("yellow"); }); } var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 }; var result = [].map.call(arrayLike, function(s) { return s.toUpperCase(); }); // ["A", "B", "C"] // strings are array-like, too var result = [].map.call("abc", function(s) { return s.toUpperCase(); }); // ["A", "B", "C"]
50. Reuse Generic Array Methods on Array- Like Objects // beware

Array.prototype.concat; it is not generic function namesColumn() { return ["Names"].concat(arguments); } namesColumn("Alice", "Bob", "Chris"); // ["Names", { 0: "Alice", 1: "Bob", 2: "Chris" }] // shallow copy `arguments` so we have a real array function namesColumn() { return ["Names"].concat([].slice.call(arguments)); } // ["Names", "Alice", "Bob", "Chris"]
51. Prefer Array Literals to the Array Constructor new Array(2, 3,

4); // [2, 3, 4] new Array(2, 3); // [2, 3] new Array(2); // no array initialiser representation new Array(2) instanceof Array; // true new Array(2).length; // 2 dense = [void 0, void 0]; sparse = new Array(2); dense.length === sparse.length; // true dense[0] === sparse[0]; // true dense[1] === sparse[1]; // true "0" in dense; // true "0" in sparse; // false dense.reduce(function(count) { return count + 1; }, 0); // 2 sparse.reduce(function(count) { return count + 1; }, 0); // 0
52. Accept Options Objects for Keyword Arguments new Alert( 100, 75,

300, 200, "Error", message, "blue", "white", "black", "error", true ); // what is this I don't even new Alert({ x: 100, y: 75, width: 300, height: 200, title: "Error", message: message, titleColor: "blue", bgColor: "white", textColor: "black", icon: "error", modal: true });
53. Accept Options Objects for Keyword Arguments // pull out required

options as formal parameters if there's only a few function Alert(parent, message, opts) { this.message = message; opts = opts || {}; // default to an empty options object this.width = opts.width == null ? 320 : opts.width; this.height = opts.height == null ? 240 : opts.height; this.x = opts.x == null ? (parent.width / 2) - (this.width / 2) : opts.x; this.y = opts.y == null ? (parent.height / 2) - (this.height / 2) : opts.y; this.title = opts.title || "Alert"; this.titleColor = opts.titleColor || "gray"; this.bgColor = opts.bgColor || "white"; this.textColor = opts.textColor || "black"; this.icon = opts.icon || "info"; this.modal = !!opts.modal; }
54. Accept Options Objects for Keyword Arguments // an `extend` helper

has cleaned this up 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", bgColor: "white", textColor: "black", icon: "info", modal: false }, opts); extend(this, opts); }
55. Do Not Block the Event Queue on I/O var text

= downloadSync("http://example.com/file.txt"); // blocked console.log(text); downloadAsync("http://example.com/file.txt", function(text) { // continues here when I/O has completed console.log(text); }); // queues I/O and arrives here immediately console.log("guaranteed to execute before the above continuation");
56. Resources • Herman, David. Effective JavaScript: 68 Specific Ways to

Harness the Power of JavaScript; Addison-Wesley Professional; 1st Edition, 2012. • Soshnikov, Dmitry. JavaScript. The Core.; <http://dmitrysoshnikov. com/ecmascript/javascript-the-core/>