Notes on "You Don't Know JS" by Kyle Simpson
A great book for learning JS internals.
June 7, 2018
The best way to understand and avoid language “gotchas” is by understanding what causes them. It’s also a great book to learn how to optimize your JS programs with very little effort.
Those notes cover the whole book series and are meant as a review, so I can learn things more effectively and quickly review some details.
- Up & Going
- Scope & Closures
this
& Object Prototypes- Types & Grammar
- Async & Performance
- ES6 & Beyond
¶ Up & Going
This first book is great for getting started into the series, specially if you have a shallow understanding of JS internals. That is because each of the other books will assume you have some basic understanding of the language mechanisms explained deeply in the other books of the series.
But if you already have a good understanding of the language, maybe it’s best to just skip to the other books.
Always use the "use strict";
in your code. Keeps code safer and optimized.
¶ Scope & Closures
¶ Scope and Lexical Scope
- JavaScript compiles statements kind-of individually and shortly before they are executed. JS engines also use JIT, lazy compilation and hot compilation.
- We can devide JavaScript compilation in three “actors”: Engine, Compiler and Scope.
- Types of Scope look-ups:
- LHS: when the variable is the target of the assignment. If it doesn’t exist, the
Scope
creates a new global variable (when strict mode is not enabled). If strict mode is enabled, it will throw aReferenceError
. - RHS: when the variable is the source of the assigment.
Engine
wants the value of the variable. If it doesn’t exist, it will throw aReferenceError
.
- LHS: when the variable is the target of the assignment. If it doesn’t exist, the
- JavaScript scopes by using Lexical Scope, as opposed to Dynamic Scope. This means that the scopes are nested and are solely determined by the code structure, rather than by the execution stack.
- Lexical scope can be cheated at runtime through
eval
andwith
, but they are almost completely disabled in strict mode and the simple fact you are using them will disable several optimizations. So, don’t use them.
¶ Function and Block Scopes
- Author recommends using named functions all the time, even in expressions. Improves code readability and also stack traces.
- JavaScript only restricts variable scope inside functions:
if (true) { var a = 42; } (function () { var b = 42; })(); console.log(a); // I can access a. console.log(b); // Error.
- You should follow the “Principle of Least
PrivilegeExposure”. - IIFEs (Immediately Invoked Function Expressions):
(function() { ... })();
(function() { ... }());
(function(foo) { foo("bar"); })(function(str) { ... });
- Use the ES6 keyword
let
to do block-scoping:{ let foo = "bar"; } console.log(foo); // ReferenceError
- Variables declared with
let
are never hoisted. - This also improves performance, as values are garbage collected earlier.
- The
const
keyword works just likelet
, but is used for constants. - This ES6 behaviour is transpilled using
try/catch
blocks. - JavaScript does hoisting with functions and variables. Functions first:
foo(); var a = 1; // Declaration will go up, but not assignment. function foo() { // This will go above everything. console.log(a); // undefined -- only "a" declaration was hoisted. }
¶ Closures
- Loops + Closure: This code prints
6
five times:
You can solve this by using a closure:for (var i = 1; i <= 5; i++) { setTimeout(function() { console.log(i); }, i*100); }
You can solve this usingfor (var i = 1; i <= 5; i++) { (function() { var j = i; setTimeout(function() { console.log(j); }, j*100); })(); }
let
:
There’s a special behaviour defined forfor (let i = 1; i <= 5; i++) { // you could use the `let j = i;` trick here, but... setTimeout(function() { console.log(i); }, i*100); }
let
declarations used in the head of a for-loop: the variable is declared each iteration and it’s initialized with the value from the previous iteration. - Closures: when a function can remember and access its lexical scope even when it’s invoked outside its lexical scope.
- Modules require two characteristics:
- An outer wrapper function being invoked, to create the enclosing scope.
- The return value of the function should include a reference to at least one inner function that then has closure over the private inner scope.
var foo = (function MyCoolModule() { var a, b, c; function doSomething() { ... }; function doAnother() { ... }; return { doSomething: doSomething, doAnother: doAnother }; })();
- You can use ES6 file modules through
export
,import
andmodule
.
¶ Arrow Functions (ES6)
- They bind
this
to the lexical context they are in:var id = "not awesome"; var bad = { id: "awesome", cool: function coolFn() { console.log(this.id); } }; var good1 = { count: 0, cool: function coolFn() { if (this.count < 1) { setTimeout(() => { // arrow-function ftw? this.count++; console.log("awesome?"); }, 100 ); } } }; var good2 = { count: 0, cool: function coolFn() { if (this.count < 1) { setTimeout(function timer(){ this.count++; // `this` is safe because of `bind(..)` console.log("more awesome"); }.bind(this), 100 ); // look, `bind()`! } } }; bad.cool(); // awesome setTimeout(bad.cool, 100); // not awesome good1.cool(); // awesome? good2.cool(); // more awesome
¶ this
& Object Prototypes
¶ this
-
this
isn’t a reference to the function itself. -
this
isn’t a reference to the function’s lexical scope. -
this
is a binding that is made when a function is invoked, and what it references is determined by the call-site. -
There are four rules for how
this
gets set:function foo() { console.log(this.bar); } var bar = "global"; var obj1 = { bar: "obj1", foo: foo }; var obj2 = { bar: "obj2" }; foo(); // "global" obj1.foo(); // "obj1" foo.call( obj2 ); // "obj2" new foo(); // undefined
- Default Binding: Standalone function invocation. In the example above,
foo()
ends up setting this to the global object in non-strict mode. In strict mode,this
would be undefined. - Implicit Binding: The call-site has a context object. In the example above,
obj1.foo()
setsthis
to theobj1
object. - Explicit Binding: Using
fn.call()
,fn.apply()
or ES5’sfn.bind()
. In the example above,foo.call(obj2)
setsthis
to theobj2
object. Some API functions have a parameter “context” to be bound in callbacks. new
Binding: When a function is called withnew
, a brand new object is created and set asthis
. Unless the function returns something, the new object is returned. In the example above,new foo()
setsthis
to a brand new empty object.
- Default Binding: Standalone function invocation. In the example above,
-
Binding exceptions:
- Passing
null
orundefined
tocall
,apply
orbind
. Use instead:var ø = Object.create(null); // a "DMZ" empty object fn.bind(ø);
- Indirection:
(p.foo = o.foo)();
- Passing
-
“Soft Binding”: Allows overriding with Implicit/Explicit Binding.
if (!Function.prototype.softBind) { Function.prototype.softBind = function(obj) { var fn = this, curried = [].slice.call( arguments, 1 ), bound = function bound() { return fn.apply( (!this || (typeof window !== "undefined" && this === window) || (typeof global !== "undefined" && this === global) ) ? obj : this, curried.concat.apply( curried, arguments ) ); }; bound.prototype = Object.create( fn.prototype ); return bound; }; }
-
It is possible to do lexical binding through arrow functions, but the author discourages it.
¶ Objects
- In objects, property names are always strings:
myObject[myObject] = 42; myObject["[object Object]"]; // 42
- ES6 computed property names:
var prefix = "foo"; var myObject = { [prefix + "bar"]: "hello", [prefix + "baz"]: "world" }; myObject["foobar"]; // hello myObject["foobaz"]; // world
- Methods “don’t exist” in JS, only references to functions.
- Duplicating objects.
- JSON-safe objects:
var newObj = JSON.parse(JSON.stringify(obj));
- Shallow copy (ES6):
var newObj = Object.assign({}, obj);
- JSON-safe objects:
- Property Descriptors:
Object.getOwnPropertyDescriptor
,Object.defineProperty
. The descriptors are:writable
,configurable
andenumberable
. Object.preventExtensions
,Object.seal
,Object.freeze
to make “immutable” objects.[[Get]]
and[[Set]]
characteristics of a property:var obj = { get a() { return this._a_; }, set a(val) { this._a_ = val * 2; } }; Object.defineProperty(obj, "b", { get: function(){ return this.a * 2 }, enumerable: true }); obj.a = 1; obj.a; // 2; obj.b; // 4;
- Test for existence:
("a" in obj);
(includes prototypes),obj.hasOwnProperty("a")
. - Array of keys:
Object.keys
(enumerable only),Object.getOwnPropertyNames
. - Iterate over keys:
for..in
loop (includes prototypes). Use in objects, not arrays. Also:obj.forEach
,obj.every
andobj.some
. - Iterate over values (ES6):
for..of
.- Works by accessing the built-in
@@iterator
:var it = myArray[Symbol.iterator](); it.next(). // {value: 42, done: false}
- It is possible to define custom iterators for objects:
Object.defineProperty( myObject, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function() { var o = this, i = 0, k = Object.keys(o); return { next: function() { return {; value: o[k[i++]], done: (i > k.length) }; } }; } });
- Works by accessing the built-in
¶ Mixins
- JS has no notion of classes/inheritance/polymorphism.
- Developers use mixings instead:
function mixin(sourceObj, targetObj) { for (var key in sourceObj) { if (!(key in targetObj)) { targetObj[key] = sourceObj[key]; } } return targetObj; }
- This leads to “explicit pseudo-polymorphism” (which the author discourages):
sourceObj.methodName.call(this, ...);
- Explicit mixings are not exactly the same as class copy.
- “Parasitic Inheritance”: calling
new Parent()
insidenew Child()
. - “Implicit Mixings”: “borrowing” methods through “implicit binding”.
¶ Prototypes
Object.create(obj)
(ES5) creates a object withobj
in[[Prototype]]
linkage.Object.prototype
is at the top of every “normal”[[Prototype]]
chain.- Shadowing only occurs if the property found in the
[[Prototype]]
chain is writable and it is not a setter. - A function’s
Foo.prototype
gets linked to the[[Prototype]]
chain of an object created withnew Foo();
:Object.getPrototypeOf(new Foo()) === Foo.prototype; // true
Foo.prototype
and the created object (through[[Prototype]]
) get a.constructor
property:Foo.prototype.constructor === Foo; // true (new Foo()).constructor === Foo; // true
- However,
.constructor
is unreliable and should be avoided where possible. - We’re are not actually copying but linking “classes”.
- How to simulate class-orientation then? Example from the book:
function Foo(name) { this.name = name; } Foo.prototype.myName = function() { return this.name; }; function Bar(name,label) { Foo.call(this, name); this.label = label; } // here, we make a new `Bar.prototype` // linked to `Foo.prototype` Bar.prototype = Object.create(Foo.prototype); // Beware! Now `Bar.prototype.constructor` is gone, // and might need to be manually "fixed" if you're // in the habit of relying on such properties! Bar.prototype.myLabel = function() { return this.label; }; var a = new Bar("a", "obj a"); a.myName(); // "a" a.myLabel(); // "obj a"
- Alternatives to
Bar.prototype = Object.create(Foo.prototype);
:Bar.prototype = Foo.prototype;
: not a copy, will modifyFoo.prototype
.Bar.prototype = new Foo();
: ifFoo()
has side-effects, they will happen.- ES6’s
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
: works well.
a instanceof Foo
tests whetherFoo.prototype
appears in the[[Prototype]]
chain ofa
.- Functions hard-bound with
.bind(..)
loose their.prototype
property. - A much cleaner approach is to use
Foo.prototype.isPrototypeOf(a)
. - Retrieve
[[Prototype]]
of an object:Object.getPrototypeOf(a);
(ES5) ora.__proto__
. - You should not change the
[[Prototype]]
of an existing object.
¶ Behavior Delegation/OLOO (objects-linked-to-other-objects)
- An alternative way of thinking/designing software in JS, which the author thinks is better than Object Orientation.
- Objects delegate common behaviour to a common prototype through
Object.create(..)
. - You want state to be on the delegators.
- We avoid if at all possible naming things the same at different levels of the
[[Prototype]]
chain. - Code gets much nicer when using
Object.setPrototypeOf(obj, prototypeObj)
. - We often can eliminate base classes, because behavior delegation suggests objects as peer of each other.
¶ ES6 class
- No need for
.prototype
references in the code. extends
keyword provides inheritance. It is also possible to extend built-in objects.constructor
method provides “instantiation”.super(..)
andsuper.fn(..)
provide “relative polmorphism”.- No need for commas.
- However, it works mostly the same way as the
[[Prototype]]
mechanism. - But it makes some things “static” where otherwise they would be dynamic.
¶ Types & Grammar
¶ Types
- JavaScript has only seven built-in types:
string
,number
,boolean
,null
,undefined
,object
andsymbol
(introduced in ES6). - All are called “primitives”, except for
object
. typeof
has a long-standing bug, but there is a workaround.typeof null // "object"` var a = null; (!a && typeof a === "object"); // true
function
is a (special) subtype ofobject
:typeof function foo() { return 42; } // "function" // but: typeof [1, 2, 3] // "object"
- “In JS, variables don’t have types – values have types.”
- We can use
typeof
to prevent errors with undeclared variables, without resorting to the globalwindow
object:if (typeof FLAG !== "undefined") {} // or: if (window.FLAG) {}
¶ Values
¶ Arrays
delete
removes slots but does not update.length
.- Adding named properties to arrays doesn’t affect the
length
property, except if the property “looks like a number”:myArray["42"] = "foo";
. - Empty slots are left in this case. They return
undefined
when accessed. - Build arrays from “array-likes”:
Array.prototype.slice.call([1, 2, 3]); // [1, 2, 3] // or (ES6): Array.from([1, 2, 3]); // [1, 2, 3]
¶ Strings
- JS
string
s are immutable. - There are some array-like methods, but other array methods are not available.
- It is possible to “borrow” some
array
methods:Array.prototype.join.call("foo", "-"); // "f-o-o" Array.prototype.map.call("foo", v => v.toUpperCase() + ".").join(""); // "F.O.O."
¶ Numbers
- JS
number
s use the “IEEE 754” standard (“double precision”), often called “floating-point.” - Comparisons can fail due to imprecision. Use the
Number.EPSILON
(ES6) or2^-52
to test equality. - Safe integers (no errors):
Number.MAX_SAFE_INTEGER
or2^53 -1
. - Force a
number
into a 32-bit signed integer:x | 0
. (bitwise operation). - Special numbers:
NaN
:var a = 42 / "foo"; a == NaN; // false a === NaN; // false -- comparisons with NaN always return false. // we can use isNaN(..), but it has gotchas: isNaN(a); // true isNaN("foo") // true // ES6: Number.isNaN(a); // true Number.isNan("foo"); // false
Infinity
and-Infinity
:1 / 0; // Infinity -1 / 0; // -Infinity 1e1000000; // Infinity 2 * Number.MAX_VALUE; // Infinity
-0
: happens as result of some operations, but behaves almost like0
:-0 === 0; // true (-0 === 0) && (1 / -0 === -Infinity); // true -- can be used to test for -0
¶ void
operator
- “voids” out any value:
void "foo"; // undefined
¶ Value vs. Reference
- Simple scalar primitives (
string
s,number
s, etc.) are assigned/passed by value-copy. - Compound values (
object
s, etc.) are assigned/passed by reference-copy. - References are not like references/pointers in other languages. They point to the underlying values and not to the variables themselves:
var a = [1, 2, 3]; var b = a; b.push(4); a; // [1, 2, 3, 4] b = [4, 5, 6]; a; // [1, 2, 3, 4] b = a.slice(); b.push(5); a; // [1, 2, 3, 4] b; // [1, 2, 3, 4, 5]
- Be careful with object wrappers:
Even though we’re passing the reference, the underlying value is unboxed from thefunction foo(x) { x++; console.log(x); } var a = Number(42); foo(a); // 43 a; // 42
Number
object in the addition operation.
¶ Natives
-
Object sub-types/built-in objects/natives:
String()
,Number()
,Boolean()
,Array()
,Object()
,Function()
,RegExp()
,Date()
,Error()
,Symbol()
(added in ES6). -
JavaScript automatically coerces primitives to their corresponding objects when accessing properties/methods:
var strPrimitive = "I am a string"; console.log(strPrimitive.length); // 13 console.log(strPrimitive.charAt(3)); // "m"
-
The following objects have corresponding primitives:
String
,Number
,Boolean
. -
null
andundefined
have no object wrapper form. -
Object
,Function
,Array
andRegExp
are object-only, though you should prefer their literal form. -
Date
andError
can only be created through their constructed object form. -
You should never wrap primitive values manually.
-
There are gotchas, like this one:
!(new Boolean(false)); // false
. -
Unboxing:
.valueOf()
. -
Never use empty-slot arrays. They are full ob bugs/inconsistencies
-
If you want to create an array with “empty” slots, use this:
var a = Array.apply(null, {length: 3});
It’ll create an array with 3 slots filled with
undefined
, which is more reliable then usingArray(3)
or setting the.length
property to 3. -
You probably should never use
Object(..)
,Function(..)
, andRegExp(..)
. -
Except if you want to pass flags to
RegExp(..)
, like this:var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );
¶ Coercion
- ToString:
a + ""
,String(..)
,.toString()
orJSON.stringify(..)
- ToNumber:
Number(..)
- ToBoolean:
Boolean(..)
- “Falsy” values:
undefined
null
false
+0
,-0
, andNaN
""
- Due to legacy reasons, all these coerce to
true
:var a = new Boolean(false); var b = new Number(0); var c = new String("");
¶ Explicit Coercion
- Strings <–> Numbers:
String(42)
andNumber("42")
.- Important: Don’t use the
new
keyword, to avoid creating an object. - Alternative ways:
(42).toString()
and+"42"
parseInt(..)
andparseFloat(..)
are tolerant to non-numeric characters.- Date --> Number: works the same way, the result is the unix timestamp.
- A explicit approach is better:
.getTime()
. orDate.now()
(ES5).
- A explicit approach is better:
- Important: Don’t use the
- –> Boolean:
!!x
orBoolean(..)
(former is preferred)
¶ Implicit Coercion
a + b
: If either is a string, the result is a string. Otherwise, it is always a numeric adition.a - 0
: Coercesa
to a number.if (..)
,for (; .. ;)
,while (..)
,? :
* --> Boolean.- Note about
||
and&&
: they don’t return a boolean, but select one of the two operands values:||
: returns the first if it’s true, the second otherwise.&&
: returns the first if it’s false, the second otherwise.
¶ Loose/Strict Equals
-
Use
==
and!=
when coercion is desired or makes no difference. -
Use
===
and!==
when type coercion is not desired. -
Implicit coercion also happens when using
>
,<
,>=
and<=
. -
Abstract Equality rules:
string
/number
: string is coerced tonumber
.- */
boolean
:boolean
is coerced tonumber
. null
/undefined
: returnstrue
.- */
null
or */undefined
: returnsfalse
. object
/non-object
: non-object
is toToPrimitive(object)
.
-
Never compare to
== false
. -
*Bad list`:
"" == 0
"" == []
0 == []
¶ Grammar
{a: 42}
isn’t an object, but a block with a labeled statement.- The author discourages using labeled statements.
- ES6 supports object destructuring:
{a, b} = {a: 42, b: "foo"}
. - “If the JS parser parses a line where a parser error would occur (a missing expected
;
), and it can reasonably insert one, it does so.” - Author: “use semicolons wherever you know they are ‘required’, and limit your assumptions about ASI to a minimum.”
¶ Async & Performance
¶ Async
- JavaScript executes asynchronous code in an “Event Loop”.
- Each iteration of the loop is a “tick”: user interaction, IO, timers… they all enqueue events.
- Only one event is processed at a time.
- Sometimes “processes” should “cooperate” by breaking themselves into smaller chunks to allow other “processes” interleaving. e.g. Through
setTimeout(.., 0)
. - JS uses callbacks as the “building block” of asynchrony.
- It is a nonlinear way of thinking.
- Leads to “trust issues”: someone else is continuing your program execution.
- Promises (ES6):
- They resolve those trust issues.
new Promise(fn(resolve, reject))
can be used to create to create Promises usingresolve(..)
andreject(..)
insidefn
.Promise.all([..])
can be used to handle multiple Promises together. Other variations of Promise patterns exist.Promise.race([..])
can be used to timeout Promises.Promise.resolve(..)
wraps values and thenables..then(..)
returns a promise: we can also chain Promises.- End chains with
.catch(..)
to handle errors.
- Generators (ES6):
- I haven’t read this part yet.
¶ Performance
- Ways to increase program performance:
- Web Workers
- SIMD
- asm.js
- Benchmarking:
- Benchmark.js is good for measuring.
- You should always consider the context.
- You should only test non-trivial snippets of code, not tiny optimizations.
- jsPerf measures snippets on multiple environments.
- Micro-optmizations or engine-specific details shouldn’t matter in JS.
- ES6 defines TCO (Tail Call Optimzation).
- Optimizes the stack when a function is called at the final
return
statement.
- Optimizes the stack when a function is called at the final
¶ ES6 & Beyond
I haven’t read this book yet.