Skip to content

Instantly share code, notes, and snippets.

@florabtw
Created December 14, 2018 16:14
Show Gist options
  • Save florabtw/7d6a6abbfa585533ceb2c1e6320a4a31 to your computer and use it in GitHub Desktop.
Save florabtw/7d6a6abbfa585533ceb2c1e6320a4a31 to your computer and use it in GitHub Desktop.
Notes from You Don't Know Javascript

ALL of the falsy values:

  • ""
  • 0, -0, NaN
  • null, undefined
  • false

The empty array is truthy. However, when loosely compared, it is coerced into an empty string, which is falsy. So,

if ([]) { console.log('hey'); } // 'hey'

but

[] == false // true

Array.toString() will simply list contents comma-delimited:

[1,2,3].toString() == "1,2,3" // true

NaN will never be equal to another number.

NaN == 0 // false
NaN == NaN // false

For inequality comparisons (greater than, less than), if either side is not a string, then both sides are coerced into numbers (or 'NaN' if string cannot be cast to a Number).

'let' and 'const' are block scoped in ES6. Blocks are any set of curly braces. Including those for 'if' and 'while' statements:

if (true) { let a = 1; }
if (true) { var b = 2; }

console.log(a); // ReferenceError
console.log(b); // 2

Immediately Invoked Function Expression (IIFE) is the name for functions expressions (not declarations) wrapped in a set of parentheses, and then immediately executed:

(function IIFE() {
    var a = 'Hello!';
    console.log(a); // Hello!
})();

IIFE(); // ReferenceError

This type of expression prevents polluting the global scope.

The error parameter in the 'catch' of a try-catch block is block scoped and will not pollute the surrounding scope:

try {
    undefined();
} catch (err) {
    console.log( err ); // valid
}

console.log( err ); // ReferenceError

Variable declarations are hoisted, variable assignments are not:

console.log(a); // undefined (not ReferenceError)
var a = 2;
console.log(a); // 2

foo(); // TypeError (not ReferenceError)
var foo = function() { console.log( 5 ); }
foo(); // 5

Multiple 'var' declarations are ignored:

var a = 2;
var a;
console.log(a); // 2

Function definitions, however, override previous declarations (and variables):

foo(); // 3

function foo() { console.log( 1 ); }
var foo = function() { console.log( 2 ); };
function foo() { console.log( 3 ); }

Closure in a for loop:

// won't work
for (var i = 0; i < 6; i++) {
    setTimeout(function() { console.log( i ); }, i * 1000);
} // 6, 6, 6, 6, 6

// will work (IIFE)
for (var i = 0; i < 6; i++) {
    (function(j) {
        setTimeout(function() { console.log( j ); }, j * 1000);
    })(i);
}

// will also work (in ES6)
for (let i = 0; i < 6; i++) {
    setTimeout(function() { console.log( i ); }, i * 1000);
}

Singleton pattern:

function Singleton() {
    if (Singleton._instance) {
        return Singleton._instance;
    }
    
    Singleton._instance = this;
}
Singleton.getInstance = function() {
    return Singleton._instance || new Singleton();
}

The new arrow functions in JavaScript are not just for letting developers type less. It actually changes how 'this' works in the function. It changes 'this' to use lexical scope rather than the normal rules:

var MyFunc = function() {
  this.myNum = 123;
  var callMe = function() {
    console.log( this.myNum );
  }
  return { callMe: callMe };
}

var ArrowFunc = function() {
  this.myNum = 123;
  var callMe = () => {
    console.log( this.myNum );
  }
  return { callMe: callMe };
}

var myFunc = new MyFunc();
myFunc.callMe(); // undefined

var arrowFunc = new ArrowFunc();
arrowFunc.callMe(); // 123

Variables declared on the global scope are placed on the global object as a property.

var a = 2;
console.log( a === this.a ); // true

Default 'this' binding for a function call comes from the call site of the function. This would be any function called without any context.

function foo() { console.log( this.a ); }

var a = 2;
foo(); // 2 - this won't work in NodeJS

In strict mode, if a function is called from global scope, then 'this' will be bound to 'undefined'. In other words, the global scope is not eligible for default binding in strict mode.

'use strict';
function foo() { console.log( this.a ); }

var a = 2;
foo(); // TypeError

Implicit 'this' binding for a function call includes when a function call has context:

function foo() { console.log( this.a ); }

var obj = {
    a: 2,
    b: 3,
    foo: foo,
    bar: function() { console.log( this.b ); }
};

obj.foo(); // 2
obj.bar(); // 3

Function calls can also lose their implicit binding:

var obj = {
    a: 2,
    foo: function() { console.log( this.a ); }
}

var bar = obj.foo;
var a = "whoops"

bar(); // whoops

Explicit 'this' binding for a function includes calling the function using the 'call()' or 'apply()' functions:

function foo() { console.log( this.a ); }

var obj = { a: 2 };

foo.call( obj ); // 2

Hard 'this' binding is explicitly setting the context for which a function can be called. Hard binding is just a variation of explicit binding:

function foo() { console.log( this.a ); }

var obj = { a: 2 };

var bar = foo.bind( obj );
bar(); // 2

'new' 'this' binding is when a new object is constructed and 'this' is bound to the newly created object:

function foo(a) {
    this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

Explicit binding takes precedence over implicit binding:

var obj = {
    a: 2,
    foo: function() {
        console.log( this.a );
    }
}

obj.foo(); // 2
obj.foo.call({ a: 3 }); // 3

'new' 'this' binding takes precedence over explicit 'this' binding:

var foo = function(a) { this.a = a; };

var obj = {};
var bar = foo.bind( obj );

bar( 2 );
var baz = new bar( 3 );

console.log( bar.a ); // 2
console.log( baz.a ); // 3

In summary, the precedence and rules of 'this' binding can be stated:

  1. Is the function called with 'new'? If so, 'this' is the newly constructed object.
  2. Is the function called with 'call()', 'apply()', or 'bind()' ? If so, this is the explicitly specified object.
  3. Is the function called as a 'child' to an object? If so, 'this' is bound to the parent/owner object.
  4. Otherwise, 'this' is bound to 'undefined' in strict mode or the global object if not.

Of course, there are some exceptions to these rules...

If 'null' or 'undefined' is passed into 'call', 'apply', or 'bind', then the parameter is ignored and default binding is applied:

function foo() { console.log( this.a ); }

var a = 2;
foo.call( null ); // 2

Passing in 'null' is typical for currying which can be done via 'bind':

function foo(a, b) { console.log( 'a: ', a, ', b: ', b ); }

var bar = foo.bind( null, 2 );
bar( 3 ); // a: 2, b: 3

However, passing in 'null' isn't always the safest. To avoid unwanted error messages, it might be best to pass in an object that is intentionally empty:

function foo(a, b) { console.log( 'a: ', a, ', b: ', b ); }

var empty = Object.create( null ); // alternatively '{}' works
var bar = foo.bind( empty, 2 );
bar( 3 ); // a: 2, b: 3

JavaScript has seven primary types:

  • string
  • number
  • boolean
  • null
  • undefined
  • object
  • symbol - added in ES6!

All types except object are primitive types, however some also have Object pairs.

JavaScript has these built-in objects:

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

Primitive types are not objects themselves:

var strPrimitive = "I am a string";
typeof strPrimitive;            // "string"
strPrimitive instanceof String; // false

var strObject = new String( "I am a string" );
typeof strObject;               // "object"
strObject instanceof String;    // true

and cannot be treated as objects:

var a = 5;
a.foo = 'bar';
console.log( a.foo ); // undefined

but are automatically coerced when performing operations:

var strPrimitive = "I am a string";
console.log( strPrimitive.length() ); // 13

ES6 adds computed property names, where you can specify an expression as a key for an object (surrounded with brackets):

var prefix = "foo";
var obj = {
    [prefix + 'bar']: 'hello',
    [prefix + 'baz']: 'world'
}

console.log( obj['foobar'], obj['foobaz'] ); // hello world

All properties on an object are stored as strings:

var obj = {};
obj[true] = 'foo';
obj[1] = 'bar';
obj[{}] = 'baz';

obj['true']; // foo
obj['1']; // bar
obj['[object Object]']; // baz

Named properties on an array will not change the length of the array, but numeric properties of any value will:

var arr = ['foo', 'bar', 'baz'];
console.log( arr.length );    // 3

arr.something = 5;
console.log( arr.something ); // 5
console.log( arr.length );    // 3

arr['20'] = true;
console.log( arr[20] );       // true
console.log( arr.length );    // 21

JavaScript object properties have four descriptors: value, writable, enumerable, and configurable. These can be explicitly set, and read:

var obj = {};

Object.defineProperty( obj, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
} );

Object.getOwnPropertyDescriptor( obj, "a" );
// {
//   value: 2,
//   writable: true,
//   configurable: true,
//   enumerable: true
// }

To prevent any new properties from being added to an object, call Object.preventExtensions( obj ).

To prevent any new properties and to set all current properties to non-configurable, call Object.seal( obj ).

To prevent any new properties and to set all current properties as non-configurable and non-writable, call Object.freeze( obj ).

Objects can have getters and setters, declared during initialization or by defineProperty:

var obj = {
    get a() {
        return this._a_ + 2;
    },
    
    set a(a) {
        this._a_ = a + 2;
    }
};

Object.defineProperty( obj, 'b', {
    get: function() { return this._b_ + 3; },
    set: function(b) { this._b_ = b + 3; }
});

obj.a = 2;
obj.b = 3;

console.log( obj.a ); // 6
console.log( obj.b ); // 9

To check existence of a property on an object, use in or hasOwnProperty. Using in will consult the prototype chain as well, but hasOwnProperty will only check the referenced object. Example:

var obj = { a: 2 };

('a' in obj); // true
('b' in obj); // false

obj.hasOwnProperty( 'a' ); // true
obj.hasOwnProperty( 'b' ); // false

Enumerability also affects the keys function. keys will only return the properties of an object that are enumerable. To get all of the properties of an object, call getOwnPropertyNames. However, this doesn't seem that useful since properties are enumerable by default.

ES6 adds a way to iterate through arrays without the need for an index or accidentally iterating over property names by using the of operator in a for loop:

var arr = [ 1, 2, 3 ];

arr.prop = 'uh oh!';
for (var prop in arr) {
    console.log( arr[prop] ); // 1, 2, 3, uh oh!
}

for (var val of arr) {
    console.log( val ); // 1, 2, 3
}

Objects do not come with iterators by default but can be manually defined. Note that the Symbol.iterator property isn't enumerable by default like most properties (magic!):

var obj = {
    a: 2,
    b: 3,
    [Symbol.iterator]: function() {
        var o = this;
        var idx = 0;
        var keys = Object.keys( o );
        return {
            next: function() {
                return {
                    value: o[ keys[ idx++ ] ],
                    done: (idx > keys.length)
                }
            }
        }
    }
}

var it = obj[Symbol.iterator]();
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }

for (var v of obj) {
    console.log ( v ); // 2 3
}

This & Object Prototypes

Chapter 5

Property assignment on an object with a prototype is not as simple as it originally appears. There are rules about property assignment if the object has a prototype with a property of the same name. Consider obj.foo = 'bar' when foo is not already on obj directly, but is at a higher level on the prototype chain:

  1. If foo is found on the prototype chain and not marked as read-only (writable: true), then foo will be added to obj and will shadow the prototype foo.
  2. If foo is found on the prototype chain and marked as read-only (writable: false), then the assignment will silently fail in default mode or throw an error in strict mode.
  3. If foo is found on the prototype chain and it's a setter function, then the setter will always be called. No foo will be added to obj nor will the setter be redefined.

If you wanted to redefine foo on obj in scenario #2 and #3 then you would have to call Object.defineProperty(..).

There is no inheritance in JavaScript, only linking. The prototype object that exists on objects is simply a link between other objects - not a copy of behavior. If you want to create a new object with similar behavior to another object, use Object.create(..) instead of creating a 'class function' and adding functions to its prototype.

You can get the prototype of an object with Object.getPrototypeOf(..). You can also check if an object is the prototype of another with isPrototypeOf():

var Foo = function() {};
var bar = new Foo();

console.log( Foo.prototype.isPrototypeOf( bar ) ); // true

Object.create(..) also takes a second parameter which specifies property names to add to the new object:

var other = { a: 2 };

var obj = Object.create( other, {
    b: { value: 3 },
    c: { value: 4 }
});

obj.hasOwnProperty( 'a' ); // false
obj.hasOwnProperty( 'b' ); // true
obj.hasOwnProperty( 'c' ); // true

obj.a; // 2
obj.b; // 3
obj.c; // 4

Chapter 6: Behavior Delegation

The author argues that behavior delegation is a more appropriate paradigm for JavaScript than inheritance. The author seems to be correct since trying to force inheritance does require a little hackery.

OLOO (objects-linked-to-other-objects) is the alternative to OOP and looks a little something like this:

var Task = {
    setId: function(id) { this.id = id; },
    outputId: function() { console.log( this.id ); }
};

var LabeledTask = Object.create( Task );

LabeledTask.prepareTask = function(id, label) {
    this.setId( id );
    this.label = label;
};

LabeledTask.outputTaskDetails = function() {
    this.outputId();
    console.log( this.label );
};

Another example of using OLOO:

var Foo = {
    init: function(who) {
        this.me = who;
    },
    identify: function() {
        return "I am " + this.me;
    }
};

var Bar = Object.create( Foo );

Bar.speak = function() {
    alert( "Hello, " + this.identify() + "." );
}

var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );

b1.speak();
b2.speak();

Types & Grammar

Chapter 1

To safely check if a variable has been declared or not, use the typeof operator. This is useful for creating a JS library:

var a = 5;

typeof a !== "undefined"; // true
typeof b !== "undefined"; // false

a ? true : false; // true
b ? true : false; // ReferenceError: b is not defined

Chapter 2

undefined can be reassigned globally in non-strict mode and locally in strict mode:

undefined = 2;

console.log( undefined ); // 2
'use strict';

function foo() {
    var undefined = 2;
    console.log( undefined ); // 2
}();

undefined = 2; // TypeError

However, doing this is a terrible idea. Don't do it. Ever.

Use the void operator to void any expression of its value:

void 0; // undefined
void true; // undefined
void 'hello!'; // undefined
function () { return void someFunc(); }(); // undefined

This knowledge is also not incredibly useful.

NaN is the only value in javascript that is not reflexive: NaN !== NaN. Also, the type of NaN is number. So, the way to check for NaN is using the global utility function isNaN(..). However, this function doesn't just check for NaN, it will return true for anything that isn't a number (string, boolean, etc).

Luckily, ES6 fixes this problem with Number.isNaN(..). It will only return true for the NaN value.

Overflowing a numeric value in JavaScript will result in Infinity, aka Number.POSITIVE_INFINITY:

var a = Number.MAX_VALUE;
a + a; // Infinity

Other rules:

  • Infinity / Infinity is equal to NaN
  • A finite number divided by Infinity is 0
  • Any positive number divided by 0 is Infinity
  • Any negative number divided by 0 is -Infinity

JavaScript has the concept of negative zero (-0) and it's weird and confusing. Just read the book chapter if you want to study it.

Rules for passing/assigning by value/reference:

  • Primitives (null, undefined, string, number, boolean, and symbol) are always by value.
  • Compound values (object, array, and function) are always by reference.

Example:

var a = 2;
var b = a;
b++;
a; // 2
b; // 3

var c = [1, 2, 3];
var d = c;
d.push( 4 );
c; // [1, 2, 3, 4]
d; // [1, 2, 3, 4]

To do a shallow copy of an array (in order to pass by value), use the slice(..) function with no parameters:

var numHolder {
    _nums_: [1, 2, 3],
    get nums() {
        return this._nums_.slice();
    }
}

numHolder.nums.push(4);
numHolder.nums; // [1, 2, 3]

Chapter 4

Putting a toString function on any object will allow that method to be automatically called when coerced to a string.

var obj = {
    name: 'Nick',
    occupation: 'Developer',
    toString: function() {
        return 'Hi my name is ' + this.name + ' and I am a ' + this.occupation;
    }
}

console.log( String( obj ) ); // Hi my name is Nick and I am a Developer

Putting a toJSONfunction on any object will allow that method to be automatically called when JSON.stringify is used on the object:

var obj = {
    secret: 'no one can know!',
    data: 42,
    toJSON: function() {
        return { data: this.data }; // exclude secret!
    }
}

console.log( JSON.stringify( obj ) ); // {"data":42}

This is also useful for removing circular references since toJSON will throw an error if any circular references exist.

When coercing to a number, values behave in the following way:

  1. boolean - true becomes 1 and false becomes 0
  2. undefined - becomes NaN
  3. null - becomes 0
  4. string - an attempt at parsing the string into a number is made. So Number( "42" ) === 42 but any invalid string will produce NaN: Number( "42c" ); // NaN
  5. object (array and function) - An attempt will be made to convert the object into a primitive, first using valueOf and then using toString. Then an attempt will be made to coerce that primitive into a number using the other rules.

Values can be coerced to a number by using the Number(..) function.

When coercing to a boolean, there is a finite list of items that return false. All other values return true. The falsy list:

  • undefined
  • null
  • false
  • 0
  • NaN
  • ""

Values can be explicitly coerced into a Boolean by using the Boolean(..) function. However, a more idiomatic approach might be using double negatation: !!.

An idiom for coercing a -1 sentinel value into false is using the bitwise NOT operator ~:

var a = 'Hello World!';

if (a.indexOf( "lo" ) >= 0) {
    console.log( 'Found it!' ); // Found it!
}

if (~a.indexOf( "lo" )) {
    console.log( 'Found it!' ); // Found it!
}

The function parseInt (and parseFloat) should only be called with a string. Once called, it will parse the value until it reaches a non-numeric character and return the result:

parseInt( '42px' ); // 42

parseInt and (parseFloat) can also take a second parameter which specifies which numeric base to use.

If parseInt (or parseFloat) is called with a non-number then that value will be coerced into a string and then parsed. This is a common source of confusion (and comedy):

parseInt( 1/0, 19 ); // 18

parseInt( 0.000008 );       // 0   ("0" from "0.000008")
parseInt( 0.0000008 );      // 8   ("8" from "8e-7")
parseInt( false, 16 );      // 250 ("fa" from "false")
parseInt( parseInt, 16 );   // 15  ("f" from "function..")

parseInt( "0x10" );         // 16
parseInt( "103", 2 );       // 2

The difference between == (loose equality) and === (strict equality) is that loose equality allows coercion and strict equality does not. A little known fact is that both operators perform the same for object types, returning true only if both values have the same reference.

For equality checks, null and undefined are equal with loose equality.

var a = null;
var b;

a == b; // true
b == a; // true

This is useful if you don't want to distinguish between null and undefined for a logical branch.

There are seven gotchas to implicit coercion comparison:

"0"   == false; // true
false == 0;     // true
false == "";    // true
false == [];    // true
""    == 0;     // true
""    == [];    // true
0     == [];    // true

Safely using implicit coercion (in comparison):

  1. If either side of the comparison can have true or false, don't ever use ==.
  2. If either side of the comparison can have [ ], "", or 0 values, seriously consider not using ==.

JavaScript has labeled blocks which can be used with break and continue, however they are highly discouraged:

outer: for (var i = 0; i < 2; i++) {
    for (var j = 0; j < 2; j++) {
        if (i == j) {
            continue outer;
        }
        console.log(i, j);
    }
}
// 1 0
// 2 0
// 2 1

block: {
    console.log( 'Hello, ' ); // Hello,
    break block;
    console.log( 'never runs..');
}
console.log( 'World!' ); // World!

Object destructuring is going to be a thing in ES6:

function getData() {
    return {
        a: 42,
        b: 'foo'
    };
}

// shorthand for `var { a: a, b: b } = getData();`
var { a, b } = getData();

console.log( a, b ); // 42 foo    

Object destructuring will also work in function arguments:

function foo({ a, b, c }) {
    console.log( a, b, c) // foo bar baz
}

foo( {
    a: 'foo',
    b: 'bar',
    c: 'baz'
} );

HTML DOM elements with an id attribute create global javascript variables with the same name.

...
<div id="whoa"></div>
...
console.log( whoa ); // <div></div>

Rules around extending native objects:

  1. Don't extend natives
  2. If you do extend natives, at least guard against future additions:
if (!Array.prototype.push) {
    // Limited version of 'push'
    Array.prototype.push = function(item) {
        this[this.length] = item;
    }
}

Async & Performance

Chapter 4

Generators are pretty cool:

function *generator() {
    for (var i = 0; true; i++) {
        yield i;
    }
}

var it = generator();
it.next().value; // 0
it.next().value; // 1
it.next().value; // 2

Generators can be used with promises to create async code that looks sychronous:

var http = require( 'http' );

function getJoke() {
  var promise = new Promise(function ( resolve, reject ) {
    http.get({
      host: 'api.icndb.com',
      path: '/jokes/random'
    }, function( res ) {
      res.on( 'data', function( chunk ) {
        resolve( JSON.parse( chunk.toString() ) );
      } );
    } );
  } );

  return promise;
}

/* It looks so synchronous! Just get the joke, then print it */
function* main() {
  try {
    var res = yield getJoke();
    console.log( res.value.joke );
  } catch (e) {
    console.log( e );
  }
}

var it = main();
var promise = it.next().value;

promise.then( function resolved( data ) {
    it.next( data );
} );

Generators can also delegate to other generators:

function* foo() {
    var b = yield "B";
    console.log( b ); // 2
    
    return "C";
}


function* bar() {
    var a = yield "A";
    console.log( a );     // 1
    
    var b = yield* foo(); // notice * syntax
    console.log( b );     // C
    
    return "D";
}

var it = bar();
it.next().value;    // A
it.next( 1 ).value; // B
it.next( 2 ).value; // D

Chapter 6

Tail call optimization is when a function call is the last expression in a function. This can be optimized because the engine can use the current stack (because it's empty) instead of creating a new stack for the function. This eliminates the 'stack overflow' problems.

function factorial(n) {
    function fact(n, res) {
        if (n < 2) return res;
        
        return fact( n - 1, n * res );
    }
    
    return fact( n, 1 );
}

factorial( 5 ); // 120

ES6 & Beyond

Chapter 2 - Syntax

ES6 introduces

default function parameters

function foo( x = 5, y = 10 ) {
    console.log( x + y );
}

foo( 7 ); // 17

object destructuring (pattern matching) with default values

var [ a, b ] = [ 1, 2 ];
var { x, y, z: Z = 5 } = { x: 3, y: 4 };

console.log( a, b, x, y, Z ); // 1 2 3 4 5

smart strings (backtics)

var name = "Nick";
console.log( `${name} works ${6 + 2} hours` );

Note: Smart strings finally allow multiline strings in JavaScript. However, the newlines will appear in the final string, unless you use a tagged literal:

function multiline(strings, ...values) {
  return strings.reduce( function( s, v, idx ) {
    if (idx > 0) {
      s += values[ idx - 1 ];
    }
    
    return s + v.replace( /\n/g, ' ' );
  }, '');
}

multiline`Look at how cool
this thing is! it is amazingly cool
and I can even include interpreted
${ 'values' } in this string`;

// "Look at how cool this thing is! it is amazingly cool and I can even include interpreted values in this string"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment