JavaScript Roadmap Journey Introduction What is JavaScript It is a scripting language that is the core of websites, along with HTML and CSS, by learning scripting language you give interactivity to your web pages! Sliders, alerts, click interactions, popups, etc... Are all made possible with JavaScript! NodeJs and Chrome Browser Today, JavaScript also execute outside of browsers! JavaScript can be used with Nodejs to write server-sided code! In fact, any device that has a JavaScript engine is able to execute JavaScript. i.e. V8 in Chrome, SpiderMonkey in Firefox. So browser and the like provide an environment that the JavaScript can be executed by the JavaScript engine underneath. Nodejs is also an environment that can execute JavaScript! What in-browser JavaScript can/can't do It is able to do the following Add new HTML to the current web page, change its content that's within the tag Handle user events, such as mouse clicks, mouse movements, and key presses Send request over the network to other servers and get the result back, this is AJAX/XMLHTTPRequest object Get and set cookies, and use client-side storages However, because in-browser is meant to be safe it's ability is also limited in order to protect the user's safety It cannot read any arbitrary files on hard disk, has no direct access to OS functions, can't read or write the arbitrary files Different tabs are isolated via "Same Origin Policy", the JavaScript of one webpage cannot sniff and peak into the webpage of another, i.e. retrieving and stealing cookies Languages on top of JavaScript Since JavaScript is a scripting language, the syntax might not be for everyone since everyone's need is different. Therefore, a bunch new language is created, that is ultimately transpired (converted) back to JavaScript before they run. The transpilation process is fast which make them usable. Coffeescript: A syntactic sugar for JavaScript that have bunch of shorter syntax for writing shorter and cleaner JavaScript code TypeScript: Adds strict data typing and typing into JavaScript Flow: Also adds data typing but in a different way compared to TypeScript Dart: Is a standalone language that has its own engine and can also be transpiled into JavaScript Kotlin: Can be run in browser or Node Many example of languages that are developed on top of JavaScript Code structure and data types Script tag You can insert JavaScript program into HTML document using the If the src attribute is set, the script content is ignored. Code Structure Statement Code in JavaScript can be ended with semicolons, or without semicolons if the statements are on line break alert("Hello World"); alert("This is hello!"); or alert("Hello World") alert("This is hello!") Having semicolons for the end of a statement is good practice because the JavaScript might do wrong assumption of when to actually insert or not insert the semicolon take a look at the example below: alert("Hello"); [1, 2].forEach(alert); // The two statement above will print out "Hello" then 1 and 2, which is what we expected The two statement above will print out "Hello" then 1 and 2, which is what we expected. However, if we change up the code to be the following: alert("Hello") [1, 2].forEach(alert); The code will only print out "Hello", and then it will display an error. This is because JavaScript doesn't insert semicolon before square brackets, therefore the two lines are treated as if they are one line alert("Hello")[1, 2].forEach(alert); Comments // This is a comment /* This is a multi-line comment :). */ Not nesting /* */ comments just lie in C. Data Types A value in scripting language is always a certain type, and there are eight of the basic data types in JavaScript A variable isn't associated with a type, but rather is associated with a value. The type of a variable can change per use. i.e. storing a string into a variable then later storing a number into it later. Number Represents both integer and floating point numbers. Infinity, -Infinity, and NaN are speical numeric values. When you are dividing undefined math operations you will get NaN, such as dividing a string with a number. Math in JavaScript is "safe", meaning that your script will never result in a runtime error, at worse your result that you get will just be NaN . BigInt In JavaScript there is a maximum size integer limit, you cannot represent integer values larger than (2^53 - 1) to do that you have to use the BigInt data type. To create a BigInt you append a n to the end of the number const bigInt = 12931293129312931923912391n; String A string must be surrounded by quotes, and there are three way of quoting your string. Double quotes: "Hello" Single quotes: 'Hello' Backticks: `Hello` Double and single quotes are basically the same thing, they have no differences between them. However, with backticks you are allowed to do variable interpolation, meaning you can incorporate variables into the string let name = "John"; console.log(`Hello, ${name}`); Besides incorporating variables, you can also call functions and the return value will be used in place for the string. Boolean A boolean only has two values, either true or false . Any number besides 0 are also considered to be true . Null value The special null value don't belong to any type. It is basically a reference to a non-existing object, a "null pointer". "undefined" value Just like null it doesn't belong to any type, it is a type of its own. The meaning of undefined is that the value isn't assigned yet. let x; console.log("x"); // prints out undefined, because we haven't assigned it a value Objects and Symbols All the data types we have discussed so far are referred to as "primitives" meaning it is only storing one value of the corresponding type. Objects on the other hand you can used to store a collection of data to make up a more complex data type. Symbol is used to refer to objects. typeof operator The typeof operator is used to return the type of the operand in String . Used to quickly check the actual type of the variable. You can call it using typeof x or typeof(x) works as well. Variables Variables A named storage for storing data. There are couple way of creating a variable var keyword This is the relic of the past, back when JavaScript first came out it was the only way of declaring variables with the var keyword. Variable declared with var is either globally scoped or function scoped. A var variable defined in function can be used only the function and is functionally scope. A var variable defined outside of a function is global scoped, can be used in any function. var greeter = "hey hi"; function newFunction() { var hello = "hello"; } greeter is globally scoped, hello is function scoped, hence if you try to access hello outside of the newFunction it will be a error since it is undefined. funky var keyword You are able to re-declare and updated var variables. var greeter = "hello"; var greeter = "hello world!"; This is perfectly valid JavaScript code. var greeter = "hi"; greeter = "haha xd"; This is of course allowed. Hoisting of var A mechanism where variables and function declarations are moved to the top of their scope before code execution. console.log(greeter); var greeter = "say hello"; So the code above is interpreted as if it was like this var greeter; console.log(greeter); // greeter is undefined greeter = "say hello"; Problem with var The major problem with using var is that if you are going to redefine a variable that is already been defined, it will be hard to tell. To make this point more clear, here is an example: var greeter = "hey hi"; var times = 4; if (times > 3) { var greeter = "say Hello instead"; } console.log(greeter) // "say Hello instead" Here, you can see that if times is greater than 3 which it is in this case, it will redefine greeter to be "say Hello instead" . Now in this short snippet maybe you can tell by yourself that greeter has been redefined, but what if it is many lines down. You won't know you be overwriting the original greeter variable since they are so far apart. let keyword The let keyword is the de facto standard for declaring a variable instead of var . Variable declare with let are block-scoped. You can update variable that's declared as let , but you cannot redeclare it. Redeclaring the same variable in different scope { }, is fine because those two greeting are treated as different variables since they are in different scope. let greeting = "say Hi"; if (true) { let greeting = "say Hello instead"; console.log(greeting); // "say Hello instead" } console.log(greeting); // "say Hi" It also solves the problem with using var . No redeclaring With let you cannot redeclare a variable like var . var a = 5; var a = 3; // This is fine let a = 5; let a = 3; // This is error No hoisting var keyword will basically move the declaration of a variable even if you assigned it on the same line to the top of the program. console.log(a); var a; // undefined (not an error) However, variable declared with let have no hoisting. console.log(a); let a; // Uncaught ReferenceError: a is not defined const keyword Variable that's declared with const have const values. They are block scoped just like let . However, they cannot be updated or redeclared once they are declared. const greeting = "Hi"; greeting = "Hello"; // error, you cannot assign to a const variable In addition, they must have an initialization value. Object declared with const can be updated, but cannot be reassigned. const greeting = { message: "say Hi", times: 4 } greeting.message = "Oh hi!"; // This is allowed! const greeting = { message: "say Hi", times: 4 } greeting = { another: "message", xd: "please" } // This is not allowed! Reassigning is not allowed! No hoisting Just like let the const keyword also doesn't allow hoisting. Type Conversion String Conversion You can call the String() function to explicit convert a value to a string String(false) -> "false" String(null) -> "null" Numeric Conversion Occurs when you use math functions and expressions console.log("6" / "2"); // Print out 3! Or you can also do it more explicitly by calling the  Number() function. However, note that any non-digit character that shows up in String will result in the conversion value NaN console.log(Number("lol")); // Results in NaN Here is the table of conversion for some common values passed into Number() Value Becomes undefined NaN null 0 true and false 1 and 0 string Empty string is 0, then convert the string to number, if any non-digit character are present results in NaN Boolean Conversion Like mentioned before, any number that's besides 0 are considered to be true. You can call the Boolean() function to explicitly convert a value to be either true/false Non-empty strings are also considered to be true so even "0" is considered to be true. Basic Operators and Comparsion Math Operators +, addition -, subtraction *, multiplication /, division %, remainder **, exponentiation String Operators The plus symbol if used with Strings will be for concatenation, you join two String together. If any of the operand is a String, the other one is also converted to a String too Assignments Assignment statement will do the assignment and return the value that was assigned, just like in Ruby console.log(x = 5); This will print out 5 because after 5 is being assigned into the variable x, it will return 5 for the console.log() function to print. Bitwise Operators AND = & OR = | XOR = ^ NOT = ~ LEFT SHIFT = << RIGHT SHIFT = >> ZERO-FILL RIGHT SHIFT = >>> == vs === The problem with regular equality check is that it does type conversion by default. So you cannot distinguish between say 0 and false since 0 == false result in true. This is because anything besides 0 are considered to be false. In order to do equality check without type conversion you would use the triple equality operator, which does checks without type conversion. a === b , will result in false if a and b are different type. There is also the !== variant compare to != Comparison using == Comparison using === null and undefined When comparing null and undefined using non-strict check you will see them to be equal to equal other null == undefined // true null === undefined // false, to be expected When null is used together with numbers, null is type converted to 0. However, this is only for comparison operator, not equality check! null > 0 // false null == 0 // false null >= 0 // true Hence, you see that null == 0 is false, because the type conversion from null to 0 isn't carried out. The equality check for undefined and null is defined such that without any conversion, they are equal to each other only and equal to nothing else! When undefined is used together with numbers for comparison operator, it will always be false. This is because undefined gets converted to NaN . And equality check don't work because like mentioned previously undefined only equal to null and nothing else. Takeaway If you are going to compare a variable that might be undefined/null treat it with care Don't use >=, >, <, <= if the variable might be undefined/null have a separate check to deal with those values. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness A nice read up on how the comparison is actually done, if needed for further clarification. Conditional and Logical Operator Ternary Operator let accessAllowed; if (age > 18) { accessedAllowed = true; } else { accessedAllowed = false; } // Can be simplified into just accessedAllowed = age > 18 ? true : false; || (OR) Returns true if one of the boolean value is true. There is an "extra" feature of JavaScript for the OR operator. result = value1 || value2 || value3; Given the above code snippet, the OR operator will go from left to right evaluating each expressions, and for each value it will convert it into a boolean, if the result is true it will stop and return the original value of that expression. if all of the expressions has been evaluated, and all of them are false then return the last value. Return the first truthy value or the last one if none were found. let firstName = ""; let lastName = ""; let nickName = "SuperCoder"; alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder && (AND) Return true only if both operand are true. Just like OR there is also this "extra" feature from JavaScript for AND operator. result = value1 && value2 && value3; It will evaluate left to right as well. It will convert each value to a boolean, if the result is false then it will stop and return the original value. If all of the values are evaluated and are all true , then return the last value. Return the first falsey value or the last one if none were found. alert( 1 && 0 ); // 0 alert( 1 && 5 ); // 5 ! (NOT) It first convert the operand to a boolean type, then return the inverse of that value. result = !value; Double NOT can be used to convert value to boolean conversion. Nullish coalescing ?? A value is defined when it's neither null or undefined . The result of a ?? b is if a is defined, then a if a isn't defined, then b Basically the ?? operator will return the first argument if it's not null/undefined , otherwise, the second one. This is just a shorter syntax for writing result = (a !== null && a !== undefined) ? a : b; // Is the same as result = a ?? b; You can also use ?? to pick the first value that isn't null/undefined . As opposed to || , it returns the first truthy value! This results in a subtle difference: let height = 0; alert(height || 100); // 100 alert(height ?? 100); // 0 We might only want to use a default value (in this case is 100), when the variable is null/undefined . However, if you use || you will be picking the first truthy value, and since 0 is considered falsey you will be skipping the default value, as opposed to the ?? operator, which does what we want. Only use the 100 default value, if it is null/undefined . Loops and switch statement While and for loop They are the same as in Python, C, and Java while (condition) { // Repeat certain code } for (begin; condition; step) { // Repeat certain code } break and continue both works the same way as in any other language. Switch statement switch(x) { case 'value1': // Do something if x is === 'value1' break; case 'value2': // Do something if x is === 'value2' break; default: // Do something if x isn't equal to any value above break; } The equality for the case is checked using strict equality (===).   Functions, function expression, arrow function Function Declaration To declare a function follow the syntax: function function_name(parameter1, parameter2, ..., parameterN) { // Body of the function } The function can have access to outer variable, as well as modifying it. The outer variable is only used if there is no local one, if there is a same-named variable that is declared inside the function then it shadows the outer one. The outer one is ignored. JavaScript's function parameter are passed by value, meaning a copy of the argument is copied into the parameter. Default values If you call a function without providing the arguments required, then the corresponding value for those parameters becomes undefined . You can provide default values in the function declaration if the argument is not passed for that parameter then it will use the default values. It is also used if the argument is specified but is equal to undefined . function foo(from, text="hello world") { console.log(from + ": " + text); } Call foo("Ann") will result in "Ann: hello world" Call foo("Ann", undefined) will also result in "Ann: hello world" Return Value A function without return or without returning a explicit value returns undefined , just like how Python returns None . Function expressions Another way of creating a function is via function expressions. It let you create a new function in the middle of any expression: let foo = function() { console.log("foo!"); }; foo(); // Print out foo! The function creation occurs in the middle of a assignment expression thus it is a function expression! Function expression should have a semicolon at the end because it is an assignment statement, which is good practice. Functions in JavaScript are higher order, meaning that you can pass a function as an argument and return a function as a return value. Anonymous functions If you just write a function expression without storing it into a variable, then that is an anonymous function. It is only accessible to the context that it is passed into, and not anywhere else. function run_callback(callback) { callback(); } run_callback(function() {console.log("hi im callback!")}); Function declaration vs function expression A function expression is created when the execution reaches it and is usable only from that moment and onward. On the other hand, a function declaration can be called earlier than it is defined, and it will still work. foo(); // fooing! function foo() { console.log("fooing!"); } Global function declaration is visible in the entire script, no matter where it is. foo(); // error let foo = function() { console.log("fooing!"); } However, function expression are created when execution reaches them, which means line number 1 wouldn't know what foo is at that point. Arrow function A much more concise way of creating a function compared to function expression is via arrow functions. Here is how one looks in function expression vs arrow function: let func = (arg1, arg2, ..., argN) => expression; This arrow function accepts n arguments and then evaluates the expression on the right side and then return the result. let func = function(arg1, arg2) { return arg1 + arg2; } let func = (arg1, arg2) => arg1 + arg2; In this case, both func does the same thing, but the arrow function on line 5 is much more concise in that it will evaluate arg1 + arg2 and then returns it without you having to specify a return. One argument If the arrow function you are writing only have one argument then you can skip the parentheses around the parameter. But having it makes it much more readable. let double = n => n * 2; No argument If the function takes no argument, the empty parentheses must be present. let foo = () => console.log("fooing!"); Function expression & arrow function You can use them the same way, for example, to create anonymous callback functions: function do_callback(callback) { callback(); } do_callback(() => { console.log("doing callback") }); // Using arrow function do_callback(function() { console.log("doing callback") }); // using function expression Multi-line arrow functions If your arrow function's logic is longer than one line, then you can use multi-line arrow functions: let sum = (a, b) => { let result = a + b; return result; } This is still a arrow function but you can do multiple lines now. However, by adding brackets to denote multi-line arrow function you now must provide an explicit return statement. Otherwise, the arrow function will just return undefined just like a regular function. Unless you don't need the multi-line arrow function to return a value, then you wouldn't need the return statement. Objects and object references Objects In JavaScript objects are used to store key to value pair collection of data. You can think of them as dictionary in Python. You can create an object using brackets { }, and a list of optional properties that the object will have. For example: let exampleObj = { age: 30, name: "John" }; Access/modify the property using dot notation: console.log(exampleObj.age); exampleObj.age = 50; To delete a property you can use the delete operator delete exampleObj.age; You can also create property that is multi-worded, but they must be quoted: let exampleObj = { name: "John", age: 30, "have computer": true }; Accessing multi-worded must use the square bracket notation, since dot notation require key to be a valid variable identifier (which excludes  space, digit or special characters). Single word property you can either use square bracket notation or dot notation, is up to you. console.log(exampleObj["have computer"]); Empty Object You can create empty object using either of these syntax: let user = new Object(); let user = {}; Accessing object property With the square bracket notation, you can use a variable to query the property. However, you cannot do the same with dot notation, it doesn't work. let key = "name"; console.log(exampleObj[key]); // Print out John console.log(example.key); // Undefined, because it tries to find property named key Computed properties To insert a property name that is from a variable you can use the square bracket. let fruit = prompt("What fruit do you want?"); let bag = { [fruit]: 5 } console.log(bag.apple); In this case, if the user entered "apple", then the apple property will be inserted into the bag object with value of 5. Property value shorthand If the variable you are assigning a property attribute to is the same as the property name, like below: function makeUser(name, age) { return { name: name, age: age } } You can just ignore writing the property assignment part and just put the variable name like below: function makeUser(name, age) { return { name, age } } They are both equivalent, but it is a shorthand way of writing the other. If the property name is the same as the variable, then you can just use the variable name as a shorthand. You can mix and match property shorthand with normal property assignment. Property name limitations Variables cannot have keyword names. However it is not true for object property, the name can be whatever even keywords! let obj = { for: 1, let: 2, return: 3 } If you use other types as property name such as 0 , they are automatically converted into String so "0" . let obj = { 0: "test" }; console.log(obj["0"]); console.log(obj[0]); // Print out same thing. The 0 in both the property and the square bracket are converted to String. Property existence test You can test if an object has a property via two ways: console.log(user.noSuchProperty === undefined); // If this is true, then it has no "noSuchProperty" "key" in object // If this is true, it exists, false it doesn't The in operator is more preferred, because there are cases where comparing to undefined will fail, for example, if the property's value is undefined . Looping over keys of object for (let key in object) { // Executes the body for each "key" // Access the value via object[key] } Object.keys, values, entries These method provide a generic way of looping over an object. They are called on the Object class because it is meant to be generic, the each individual object can write their own while still having this generic way. Object.keys(obj) : Returns an array of keys Object.values(obj) : Returns an array of values Object.entries(obj) : Returns an array of [key, value] pairs Since Object lack map , filter , and other functions that array supports you can simulate it using Object.entries follow by Object.fromEntries let prices = { banana: 1, orange: 2, meat: 4, }; let doublePrice = Object.fromEntries( Object.entries(prices).map(entry => [entry[0], entry[1] * 2]) ); Object references When you assign a primitive data type to another variable, it makes a new copy of the original value. However, if you assign another variable an existing object you are making an alias, it is a reference to the original object. It does not make a new copy. So if you change the attribute of the alias, it will also change the original object! Reference comparsion Two objects are equal if they are the same object let a = {}; let b = a; // Making a reference of a a == b; // This is true, because they are referencing the same object a === b; // Also true. Duplicating object The function Object.assign(dest, ...sources) will take in a target object, in which to copy all the property to. One or more list of source object whose's property to copy into dest . let user = { name: "John" }; let perm1 = { canView: true }; let perm2 = { canEdit: true }; Object.assign(user, perm1, perm2); console.log(user); // Print out "John", true, true However, Object.assign() doesn't support deep cloning, if the object we are copying contain another object, then it will be copying the references to the destination object. In order to do deep cloning you will have to use structuredClone(object) function to clone all nested objects. let user = { name: "John", sizes: { height: 182, width: 50 } }; let clone = structuredClone(user); user.sizes == clone.sizes // false, the size object within user object is cloned as well. Method and "this" keyword You are able to add methods (functions that is a property of an object) to the object. let user = { name: "John", age: 30 }; // First way of adding method user.sayHi = function() { console.log(`Hi my name is ${this.name}`); }; // Second way of adding method let ricky = { name: "Ricky", age: 22, sayHi: function() { console.log(`Hi my name is ${this.name}`); } }; A shorthand way of writing method for an object is you can skip out the property name: // Second way of adding method let ricky = { name: "Ricky", age: 22, sayHi() { console.log(`Hi my name is ${this.name}`); } }; The sayHi() function in both ricky object are kind of similar but not fully identical, but the shorter syntax is preferred. The this keyword is used to access the object that the method is invoked upon. Using this keyword allow you to access the object's property that it is invoked upon. In the previous example it is used to accessed ricky 's name property. "this" is not bound Unlike other languages like Java or Python, the this keyword can be used in any function actually and doesn't have to be for a method. You can directly write the following function: function sayHi() { console.log(this.name); } Then you can assign it to be an object's method: let user = {name: "John"}; let admin = {name: "Admin"}; function sayHi() { console.log("Hi I am " + this.name); } user.f = sayHi; admin.f = sayHi; user.f(); // Will say "Hi I am John". this == user admin.f(); // Will say "Hi I am Admin". this == admin this will determine which object it is invoked upon at call-time, which object is before the dot basically. Calling the same function without an object will make this == undefined . If you call sayHi() directly, in the previous example then this will be undefined . If you do this in browser, then this will be assigned the global object window . In Nodejs it will also be a global object. It is expected that you call the function in an object context if it is using this . Arrow function have no "this" If you reference this in arrow function, then it inherit the this from the outer "normal" function. let ricky = { name: "Ricky", age: 22, sayHi: function() { console.log(`Hi my name is ${this.name}`); }, foo() { let bar = (x, y) => { console.log(this); } bar(); } }; In this case if you invoke ricky.foo() then the  this that is being used in the  bar arrow function will be referring to ricky object, since it is inheriting it from the foo normal function. In addition, if you invoke an arrow function that uses "this" directly without it being nested inside any function at all, "this" will be an empty object. Object to primitive conversion JavaScript doesn't allow you to customize how operator work on objects. Languages like Ruby, C++, or Python allows you but not JavaScript! When you are using operator with +, -, * with objects, those objects are first converted into primitive before carrying out the operations. So the rules for converting object to primitive is as follows Treating object as a boolean is always true. All objects are true in boolean context Using object in numerical context, the behavior can be implemented by overwriting the special method For object in string context, the behavior can also be implemented by overwriting the special method Hints To decide which conversion that JavaScript apply to the object, hints are provided. There are total of three hints. "string" This hint is provided when doing object to string conversion. "number" This hint is provided when doing object to number conversion. "default" This hint is provided when operator is unsure what type to expect, for example the binary plus operator can work both on string and number, so if a binary plus operator encounters an object, the "default" hint is provided. In addition, the == operator also uses the "default" hint. JavaScript conversion If obj[Symbol.toPrimitive](hint) method exists with the symbol key  Symbol.toPrimitive (system symbol), then it is called Otherwise, if the method doesn't exist and the hint is "string"   obj.toString() or obj.valueOf() is called whichever exists Otherwise, if the method doesn't exist and the hint is "number"   obj.valueOf() or obj.toString() is called whichever exists Symbol.toPrimitive If the object have this key to function property then this method is used and rest of the conversion method are ignored. let obj = { name: "John" }; obj[Symbol.toPrimitive] = function(hint) { // Here the code to convert this object to a primitive // It MUST return a primitive value! // hint can be either "string", "number", "default" } toString/valueOf If there is no Symbol.toPrimitive then JavaScript will call toString and valueOf . For "string" hint toString method is called, if it doesnt' exist or it returns an object instead of primitive, then valueOf is called For other hints valueOf method is called, and again if it doesn't exist or it returns an object instead of primitive, then toString is called By default, toString returns a String "[object Object]" By default, valueOf returns the object itself let user = {name: "John"}; "hello".concat(user) // hello[object Object] user.valueOf() === user // True If you only implement toString() then it is sort of like a catch all case to handle all primitive conversion. You can't just implement valueof() to handle all primitive conversion since toString() return a primitive String by default already. Garbage collection Reachability In JavaScript garbage collection is implemented through something called reachability. Variable that are reachable are kept in memory and not deleted by the garbage collector. A value is considered to be reachable if it's reachable from a root by a reference or by a chain of references. However, if an object is not reachable anymore, then it will be deleted by the garbage collector. Example let user = { name: "John" }; Let's say we have this global variable user referencing to the object {name: "John"} . If user = null; then the reference to "John" is deleted, thus the object "John" becomes unreachable. It will be garbage collected. Example let user = { name: "John" }; let admin = user; Here, we have two references that points to the "John" object. If user = null; the garbage collector will not delete "John" because it is still reachable via admin .   Constructor and "new" operator Constructor function Function that is meant to be a constructor are named with capital letter first and be executed with the new operator. We use a constructor because it creates a shorter and simpler syntax compared to creating object with {...} every time. function User(name) { this.name = name; this.isAdmin = false; } // Invoking constructor let user = new User("Ricky"); console.log(user.name); // Ricky console.log(user.isAdmin); // false When a function is executed with the new operator here it what happens A new empty object is created and assigned to this implicitly The function body executes, usually modifying this by adding new properties or method to it Then the value of this is returned implicitly So step 1 and step 3 is done implicitly for you already, you just need to worry about modifying this . new function() {...} Using this syntax you can create a single complex object that you don't aim to create again in the future. The constructor isn't saved anywhere. let user = new function() { this.name = "John" this.isAdmin = false; }; The constructor function is created and immediately called with the new operator. Returning from constructor Normally, a constructor function don't have a return statement. But if there is one the rule are as follows: If return is called with an object, then the object is returned instead of this If return is called with a primitive, it's ignored Methods in constructor You can add method in a constructor function as well, the same way you would add a property! function User(name) { this.name = name; this.foo = function() { console.log(`My name is ` + this.name); }; } You are able to write arrow functions and refer to this in constructor function as the this is bounded to the constructor's this . function User(name) { this.name = name; this.foo = () => { console.log(this.name); }; this.bar = function() { console.log(this.name); } } let u1 = new User("Ricky"); u1.foo(); // Ricky u1.bar(); // Ricky       Optional chaining Optional chaining There is this problem that if the object that you are accessing might or might not contain an object attribute say address and you can trying to access address 's attribute further say user.address.street , then your JavaScript will crash because it is trying to access an undefined 's attribute. // The problem let user = {}; user.address.street // Error To solve this we have an operator called optional chaining , the ?. operator. So essentially, you replace the dot notation with ?. and it will stop the evaluation if the value before ?. is undefined/null and finally returns undefined . value?.prop // Will be equal to value.prop if value exists // Otherwise if value is undefined/null it will return undefined. This is a much safer way of accessing attributes that you know will be undefined. Don't overuse optional chaining You should only use the optional chaining operator only when it's ok that something doesn't exist In addition, the variable that you call optional chaining on must be declared otherwise, it will be an error. user?.address // If there is not let user; then it will be an error Symbol Type Symbols In JavaScript there is two primitive types that can be used as object property key String Symbol type Don't confuse the symbol type in Ruby with JavaScript they are different albeit similar in that they are used to create unique identifier. A symbol can be created using Symbol() let id = Symbol(); You can give a symbol a description as it's parameter, mostly useful for debugging purposes. // id is a symbol with description "this is id" let id = Symbol("this is id"); Symbols are guaranteed to be unique, even if they have the same description, they are considered to be different values. let id1 = Symbol("id"); let id2 = Symbol("id"); id1 == id2 // False Property of symbols Symbols don't support implicit conversion to a String. Hidden properties Symbols allows you to create hidden properties on an object that no other part of code can accidentally access or overwrite let user = { name: "John" }; let id = Symbol("id"); user[id] = 1; console.log(user[id]); Here we are creating a hidden property using the id symbol that we have created. We can access it using the same symbol as the key. However, if we tried to access it using String for example user["id"] we would get undefined . Using symbol is to avoid conflict between say a third party code that wants to inject some of their own property into the object. If they are just using String key then it will likely overwrite some of the existing property for the object. However, if they use Symbols then there will be no conflicts between the identifiers. Symbols... more You can use symbols in object literal, just use the square bracket when you are using symbols as key, like how you would use an variable as key Symbols are skipped in for ... in loops Global symbols If you want different named symbols to be referring to the same entity you can do that using global symbol registry. Create symbol in it and access them later, repeated access by the same name will return the same symbol. The function Symbol.for(key) will look in the global registry for a key named key and return the Symbol that the key is mapped to. If it doesn't exist it will create one. Subsequent access to the same key will guaranteed to return the same Symbol. let id = Symbol.for("id"); // read from global registry, create if it doesn't exist let idAgain = Symbol.for("id"); // read it again in another part of code id == idAgain // true This symbol is the same symbol in Ruby. Symbol.keyFor If you want to find the key for a particular symbol using the Symbol object, you can do that with this function. let globalSym1 = Symbol.for("name"); let globalSym2 = Symbol.for("name"); Symbol.keyFor(globalSym1); // "name" Symbol.keyFor(globalSym2); // "name" Arrays and methods Array Two ways of creating empty array let arr = new Array(); let arr = []; Create an array with elements in them let arr = ["Apple", "Orange", "Plum"]; Array in JavaScript is heterogeneous, you can store element of any type in them. arr.length property to get the number of element in array if you used it properly. Otherwise, if you insert item into an array with a huge gap like below let arr = []; arr[999] = 59; console.log(arr.length); // 1000, it is the index of the last element + 1 arr.at() Normally, if you index an array you can only use positive indexing, i.e. [0..length - 1] . You can use negative index with the arr.at method. pop/push Use pop to remove element from the end of the array Use push to insert element to the end of the array. You can insert multiple items by providing them in the parameter. Treat array as a stack, first in last out data structure. shift/unshift Use shift to dequeue the first element from the head of the array Use unshift to add element to the head of the array. These two operations are slow takes O(n) because it requires shifting the array after removing or adding the element. Queue To use an array as a queue, use the push and shift function to dequeue and enqueue element into the queue. Looping over array You can use a traditional index loop let arr = ["Apple", "Orange", "Pear"]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } But if you only need to loop over the element without the indices then there is a shorter syntax let fruits = ["Apple", "Orange", "Plum"]; // iterates over array elements for (let fruit of fruits) { console.log(fruit); } Remember although you can use for ... in loop for array, it is not optimized for looping over array, so it will be slower. In addition, you will be looping over extra properties that is in array. So it is not recommended to use for ... in for array iterations, only for object property iterations. toString() Array object implements the toString() method to return a comma separated value of String let arr = [1, 2, 3]; arr.toString() // returns "1,2,3" Array object doesn't have Symbol.toPrimitive nor a valid valueOf , so they only implement toString as a catch all object to primitive conversion. [] will become "" [1] will become "1" [1, 2] will become "1,2" Comparison of array using == Remember that == comparison does type conversion if the type are different, and with objects it will be converted to a primitive, and in this case because array only implemented toString() it will be converted to a String. Which results in some really interesting comparison. With == if you are comparing two object, it will only be true if they are referencing the same object. The only exception is null == undefined is true and nothing else. [] == [] // false, because they are different object when you created an empty array [0] == [0] // false, again different object 0 == [] // true, because the empty array gets converted to '', and '' is converted to 0 which is true '0' == [] // false, since empty array converts to '', those two strings are different So how do we actually compare array? Use a loop to compare item-by-item Array methods arr.splice arr.splice(start[, deleteCount, ele1, ..., elemN]) The splice method will modify arr starting from index start , it removes deleteCount elements and then inserts elem1, ..., elemN at their place. Returns the array of removed elements. let arr = ["Dusk blade", "Eclipse", "Radiant virtue", "Moonstone"]; console.log(arr.splice(1, 2)); // Prints out ["Eclipse", "Radiant virtue"] they are removed console.log(arr); // Prints out ["Dusk blade", "Moonstone"] arr.slice arr.slice([start], [end]) The slice method will return a new array copying all the item from index start to end not including end . arr.concat arr.concat(arg1, arg2...) Create a new array that includes values from other arrays and additional items. args can be either an array, in which it will include every items, or it can be elements themselves which will also be appended into the array indexOf, includes arr.indexOf(item, from) : Looks for  item starting from index from , and return the index where it is found, if it cannot find it return -1 arr.includes(item, from) : Looks for item starting from index from , and return true if found The comparison for indexOf and includes uses === strict equality checking. So if you are looking for let's say false , you won't be accidentally finding 0 , since == equality checking does type conversion. arr.find If we are looking for an object with specific condition, we can use arr.find . This is very similar to find in Ruby as well. We will pass in an anonymous function into find , and it will find the first element that satisfy the condition, i.e. that returns true . When you are writing the anonymous function, if you don't need all of the arguments, it is okay to write your anonymous function without it. Since if the find will pass all three of those parameter into the anonymous function, but if your function doesn't take it it is okay. function foo(a, b) { console.log(a, b); } foo(1, 2, 3); // Calling it with more argument won't matter. Extra arg are ignored foo(1); // Calling it with less arg is fine too, rest will just be undefined Example of using arr.find : let result = arr.find(function(item, index, array) { // if true is returned, item is returned and iteration is stopped // if no true is return, undefined is returned }); There are other variant to find arr.findIndex if you are interested in finding the index of the element you are looking for instead of the element. arr.findLastIndex to search right to left. arr.filter Very similar syntax to arr.find , but instead of searching for only one element, this method will look for all element that satisfies the condition, i.e. that returns true and return the result in an array. arr.map Again very similar syntax as well to find and map , this time you are transforming each element of the array into something else. The function will be responsible for collecting all of the element. You just need to return the element after it is transformed let nums = [1, 2, 3, 4, 5];// Using multi-line arrow function, to practice returning.console.log(nums.map((ele) => { let result = ele * 2; return result;})); arr.sort Sorts the array in place. However, by default if you don't provide a comparer function it will be sorting it lexicographically, even if the elements are other type. It will attempt to convert the type into String and then sort it accordingly. let numArr = [1, 2, 15]; numArr.sort(); // This will modify numArr to be [1, 15, 2]! Because lexicographically 2 comes after 15! In order to fix this we will have to write our own comparer function. function compareNumeric(a, b) { if (a > b) return 1; // a comes after b if (a == b) return 0; // they are equal return 0 return -1; // a comes before b } let numArr = [1, 2, 15]; numArr.sort(compareNumeric); // now this will modify numArr to be [1, 2, 15]; Actually, you can even simplify this even further by just writing arr.sort((a, b) => a - b); // Ascending arr.sort((a, b) => -(a - b)); // Descending arr.reverse Just reverse the order of elements in place. let arr = [1, 10, 4, 2]; arr.reverse(); // arr is now [2, 4, 10, 1]; split and join Works just like how it work in Python, but with a minor differences. let names = "Ricky Xin Rek'sai" let arr = names.split(' '); // arr is ["Ricky", "Xin", "Rek'sai"] split() need you to specify a space ' ' if your delimiter separating the different words by a space. In addition, if you provide in an empty String '' as delimiter, it will split the String into array of single letters. join() requires you to call it on the array that you are joining together, and you provide the String to join all the element together by as a parameter let strs = ["Ricky", "Xin", "Rek'sai"]; let out = strs.join("-"); // Becomes "Ricky-Xin-Rek'sai" reduce/reduceRight The basic syntax for using reduce/reduceRight is as follows: let value = arr.reduce(function(accumulator, item, index, array), initial); accumulator : The result of previous function call, on the first element it is equal to initial if it is provided item : The current array item index : The current array item's index array: The array that you call reduce on initial : if the initial is provided then accumulator will be assigned that value and start the iteration at the first element. Otherwise, if initial is not provided, then accumulator will take the value of the first element of the array and start the iteration at the 2nd element How reduce work is that the function will be applied to every element, the function will return a result, and that result is passed to the next element as accumulator . The first parameter basically becomes the storage area for storing the overall results. The easiest example is to use this to sum up all the elements in a number array: let nums = [1, 2, 3, 4, 5]; console.log(nums.reduce((sum, ele) => sum + ele), 0); // Will return 15 You can do more complicated logic with multi-line arrow function. You just need to return the result as the accumulator for the next function call. Array.isArray You can check if an object is an array by using this function to check whether the parameter passed is an array or not. This is needed because typeof doesn't distinguish between object and array . Iterables Iterables A generalization of arrays, it allows us to make any object iterable in the context of for ... of loop. Making an object iterable just requires you to add the Symbol.iterator hidden property which is a function that returns the iterator. Really doubt this is ever need to be done, but the option is there if it is needed. The code for it isn't that bad. String is iterable You can iterate over each letter of a String using for ... of loop since the String type probably have Symbol.iterator implemented. for (let char of "Testing") { console.log(char); // T, e, s, t, i, n, g } Map and set, weakmap and weakset Map Very similar to an object, however, with object the only key that is allowed is a String. Map on the other hand allows keys of any type. Any type as it ANY type, even an object can be used as a key new Map() : Creates a new empty map map.set(key, value) : Sets the key-value pair into the map map.get(key) : Returns the value by the key , returns undefined if key doesn't exist in map map.has(key) : Returns true if the key exists, false otherwise map.delete(key) : Removes the key-value pair by the key map.clear() : Removes everything from the map map.size : Returns the total number of key-value pair The way that Map compares keys is roughly the same as === , but NaN is considered to be equal to NaN hence, even NaN can be used as key as well! Iteration over Map Three ways of iterating over a map map.keys() : Returns an iterable for keys map.values() : Returns an iterable for values map.entries() : Returns an iterable for both key and value, this is the default for for ... of let recipeMap = new Map(); recipeMap.set('cucumber', 500) .set('tomatoes', 350) .set('onion', 50); for (let vegs of recipeMap.keys()) { console.log(vegs); // cucumber, tomatoes, onion } for (let amount of recipeMap.values()) { console.log(amount); //500, 350, 50 } for (let entry of recipeMap) { console.log(entry) // [cucumber, 500], [tomatoes, 350], [onion, 50] } The iteration follows the insertion order, unlike object which doesn't preserve the insertion order in iteration. Map from array You can create a map from a 2D array like below: let map = new Map([ ['1', 'str1'], [1, 'num1'], [true, true] ]) Map from object You can create a map from an object like below: let obj = { name: "john", age: 30 } let map = new Map(Object.entries(obj)); Object.entries(obj) will return a 2D array where the 1D array will be the two key-value properties. Since all of the key in object are String the key will always be a String. Object from Map Object.fromEntries does the opposite, it will create an object from a map. All of the key from the map will be converted to a String, because keep in mind that object can only take String as it's key, nothing else. The values will be kept as the same. let map3 = new Map(); map3.set(1, "50"); map3.set("name", "Ricky"); map3.set("2", "Ricky"); let obj = Object.fromEntries(map3); // {"1": "50", "name": "Ricky", "2": "Ricky}. Set A set of unique values. There is no key-value pair mapping, Set only contains the values, and the same value may only occur once. new Set([iterable]) : To create a set, if the iterable object is provided, it creates a set from those values set.add(value) : Add value to the set, and return the set itself set.delete(value) : Remove the value, returns true if value existed otherwise false set.has(value) : true if it contains the value false otherwise. set.clear() : Removes everything from the set set.size : Returns the number of elements in the set Set iteration You can iterate over a set using same for ... of loop. set.keys() : Return the iterable object for values set.values() : This is the same as set.keys() set.entries() : Return iterable object with entries [value, value] These method exists in order to be compatible with Map if you decide to switch from one to the other. WeakMap With normal Map the object that is mapped as the value will be kept in memory and so long as the Map exists, the object will exist as well. WeakMap on the other hand is different in handling how the garbage collection work. It doesn't prevent garbage-collection of key objects like  Map does, hence their use cases are completely different. Here is how WeakMap works. let weakmap = new WeakMap(); let obj = {}; weakmap.set(obj, "ok"); First of all, WeakMap can only take object as the key, if you try to use a primitive like String or integer it will result in an error. Then, if the object we used as the key have no other references to that object (excluding the one from WeakMap ) it will be removed from memory automatically. let john = {name: "John"}; let weakmap = new WeakMap(); weakmap.set(john, "..."); john = null; // the object now lost the reference! // john is now lost from memory, because WeakMap doesn't prevent garbage collection of key objects. In addition, there are limited functionality to WeakMap , there is no iteration, no way to get all keys or values from it. It only supports set, get, delete, has method like a normal map. Use cases One of the use cases for WeakMap is for caching. The result from a function call associated with an object can be stored in a WeakMap , future calls on the same object can reuse the same result. Then when you want to clean up the cache , you can just delete the reference to the object and that result in WeakMap cache will be automatically removed from memory since it gets garbage collected. WeakSet Behaves similarly, but you can only ad objects to WeakSet no primitives. Again only supports add, has, delete no way of doing iterations. Used for a yes/no fact, say if an object is still being used somewhere else or not. The object will also be removed from the WeakSet once the object becomes inaccessible/unreachable.     Destructuring assignment Destructuring assignment A special syntax to let us unpack either array or object into different variables Array destructuring let arr = ["Ricky", "Lu"] let [firstName, lastName] = arr; console.log(firstName); // "Ricky" console.log(lastName); // "Lu" The way the destructuring works is that it will assign the first element to the first variable that you gave, second element to the second variable that you gave, and so on... You can ignore elements that you don't want by not putting a variable for that particular element. For example, if you don't want to assign the second element and the fourth element of the array: let arr = ["Ricky", "Lu", "Xin", "Wang"]; let [first, , third] = arr; console.log(first); // Ricky console.log(third); // Xin This kind of assignment works with any iterable, you can use it on Set , and Map since destructuring assignment is actually a syntax sugar for calling for ... of loops. You can actually use any assignable on the left side, even object properties! let user = {}; [user.name, user.surname] = ["Ricky", "Lu]; Swapping variable You can use destructuring assignment to swap two variable's value without using an intermediate variable let a = 5; let b = 100; [b, a] = [a, b]; // Swaps a's value with b's value The rest '...' Normally, if you are destructuring say only one value out of the entire array, the rest of the values are just ignored. You can actually gather the rest of the values into a variable as well using the ... syntax: let [name1, name2, ...rest] = ["Ricky", "Xin", "Rek'sai", "Kai'sa"]; name1 // "Ricky" name2 // "Xin" rest // ["Rek'sai", "Kai'sa"] It will fill in the named variable first, then anything that's left over will be stored into the rest variable as an array. Even if there are no items left, the rest variable will remain an empty array. The rest assignment must also be the last assignment in a destructuring statement! Default values When doing destructuring assignment if you use more variable names than the number of elements in the array, the variables that aren't able to receive a value will become undefined . However, you can assign a default value for them if they didn't receive a value from the array. let [firstname="Anonymous", lastname="Anonymous"] = ["Julius"]; firstname // "Julius" because firstname was able to receive a value lastname // "Anonymous" because there is no more value left for lastname You can also use function calls for default values. They will only be called if the variable didn't receive a value. Object destructuring Just like you can do array destructuring you can also destruct object, the syntax is a little bit different. let {var1, var2} = {var1: ~, var2: ~}; If the variable that you are assigning the property of the object into have the same name as the property name then you can just write the property name on the left. However, if you decide to assign the property to a different variable name say name property into just nm , then you would have to do a little bit more: let {name: nm, height} = {name: "Ricky", height: "5.9"}; You put the variable name that you are actually assigning to on the right side of the : , left side is the property name. You can also use default values as well! let {name: nm, height=500} = {name: "Ricky", age: 50}; // name -> nm // height -> 500 The order that you put the property assigning does not matter Rest pattern with object destructuring Again you can also use the rest variable to assign the rest of the property that you are not assigning directly to a variable into the rest variable. The rest variable becomes another object with those left over properties that you didn't assign directly. let options = { title: "Menu", height: 200, width: 100 }; let {title, ...rest} = options; // titles -> "Menu" // rest -> {height: 200, width: 100} Gotcha if there's no let With array destructuring assignment you can declare the variable that you are using for the assigning before you actually do the assigning: let item1, item2; [item1, item2] = [1]; console.log(item1, item2); // item1 -> 1, item2 -> undefined However, with object destructuring you have to do a little bit more, using the same syntax it would not work! let title, width, height; // error in this line {title, width, height} = {title: "Menu", width: 200, height: 100}; This is because JavaScript treats {...} as a code block, thus it will try to execute it. To fix this you have to wrap the expression in parentheses: let title, width, height; // error in this line ({title, width, height} = {title: "Menu", width: 200, height: 100}); Nested destructuring If the object that you are destructuring have nested object, then you can extract the nested object's property out as well. You just need to do another layer of destructuring with the same syntax: let nested = { another: { name: "Inner me", age: 30, address: "6601231" }, name: "Outer me", age: 10 } let {another: {name: innerName, ...innerRest}, name: outerName, age: outerAge} = nested; // innerName -> Inner me // innerRest -> {age: 30, address: "6601231"} // outerName -> Outer me // outerAge -> 10 Smart function parameters If you are going to write a function with many optional parameters, it might looks something like this: function showMenu(title = "Untitled", width = 200, height = 100, items = []) { // ... } Which doesn't look very nice, and when you are calling the function you will have to remember what is what. To help resolve this we can use destructuring assignment in the function parameter function showMenu({title="Untitled", width=200, height=100, items=[]}) { // Body of the function // title, width, height, items all have default vlaues if you didn't specify it } // Calling showMenu showMenu({}); // With no parameter showMenu({width: 6969}); // With one optional parameter Now when you are going to call the function, you provide in an object of parameters. If you don't want to provide any then you can just pass in an empty object. You can make empty object call even better by assigning a default value to the object destructuring assignment. function showMenu({title="Untitled", width=200, height=100, items=[]} = {}) { // Body of the function // title, width, height, items all have default vlaues if you didn't specify it } // Calling showMenu showMenu(); // With no parameter showMenu({width: 6969}); // With one optional parameter Keep in mind that you are still allowed to have other parameter besides the object destructuring assignment JSON JSON file When you want to transport say a complicated object or an array of object to another compute using network. You cannot transport a complicated object just directly as "it is". You must serialize it which essentially turns the object into a stream of bytes, a string (since string can be represented using binary easily). Then send the stream of bytes over the wires, and the receiver can then reconstruct the object using the stream of bytes that you have sent. JSON file syntax JSON file can either be an array in which it starts with hard bracket, and it defines an array of objects in it JSON file can also be an object directly in which it starts with soft brackets and it defines the property of the object Each key of the object in JSON must be in quotation marks! No question! Each key can map to another string, a float, boolean, an array, or another dictioanry Each element in either object or array must be separated by a comma JSON.stringify This is the function that will translate a given object into a JSON. The resulted string is called JSON-encoded or serialized object or marshalled object that is ready to be send over the wire since it is a plain string of bytes. JSON.stringify is able to support the conversion of Objects Arrays strings, numbers, boolean values, and even null Into JSON However, here are some of the excluded items that are excluded for the conversion Object methods Symbolic keys and values Properties that are mapped to undefined let user = { sayHi() { // ignored alert("Hello"); }, [Symbol("id")]: 123, // ignored something: undefined // ignored }; alert( JSON.stringify(user) ); // {} (empty object) Nested objects Nested objects are handled automatically by JSON.stringify so we don't have to worry about that! Circular references JSON.stringify does not support circular references! It will be an error during conversion! Extra parameter JSON.stringify(value, replacer, space) value : Is the object that we are encoding replacer : It can either be an array of property that we specifically ask to encode and ignore the rest, or it can be a mapping function. The function has to take in function(key, value) . The function will be called on for every  (key, value) pair of the object and return the replace value that will be used for doing the encoding instead of the original. Return undefined if you don't want to encode a specific property. The reason why this is done to give us a finer control on how we want the stringify to be carried out space : Specifies how many spaces to use for the output encoded string Custom "toJSON" If an object implements it's own toJSON method for string conversion JSON.stringify will automatically call it if it is available to do the encoding. JSON.parse On the other hand, once you receive the JSON encoded string you would want to decode it back into an object so you can process it. You can do that with the JSON.parse method let value = JSON.parse(str, [receiver]); str : The JSON-string to parse receiver : An optional function(key, value) that will be called for each (key, value) pair and can be used to transform the value after it has been parse There is some peculiarity with the receiver function, mainly in that it will be calling on each key and value pair, but at the last final iteration it will also called on the entire object where the key is an empty string and the value is the original object/array itself.                   Miscellaneous function topics Rest parameters and spread syntax How do we make a function take in an arbitrary number of arguments? Simple we use the ... rest operator in the function header. function sum(...args) { let sum = 0; for (let arg of args) sum += arg; return sum; } When you prefix a parameter with ... you can pass in an arbitrary number of arguments into the function and it will all be collected into an array that's stored into the parameter args . You can also mix rest parameters with normal parameters like so function showName(firstName, lastName, ...extra) { console.log(firstName, lastName); console.log(extra); } showName("Ricky", "Lu", 30, 40, 50); In this case, the first two parameter will be stored into firstName and lastName respectively, and any further parameter that you pass in will be stored into extra as an array of parameters. Keep in mind that the rest parameter must be at the end when you use it. You cannot have a function like so function f(arg1, ...rest, arg2) this will be a syntax error Spread Syntax On the other hand, you can also unpack the values from an array or any iterable into a function. For example: // instead of writing let arr = [3, 5, 1]; console.log(Math.max(arr[0], arr[1], arr[2])); // Too long, and if there are hundreds of values, we are not gonna do this console.log(Math.max(...arr)); // Much better, this unpacks the arr This is much like the opposite of doing the reverse of rest parameter. We want to spread the values from an array into the parameter of a function. When ... is used in a function call it expands the iterable object into the list of arguments. You can spread multiple iterable into a function let arr1 = [1]; let arr2 = [2, 3, 4, 5, 6]; function foo(a, ...rest) { console.log(a, rest); } foo(...arr1, ...ar2); // prints out "1 [2, 3, 4, 5, 6]" Merging array You can also use the spread syntax to merge arrays together, instead of using arr.concat let arr = [3, 5, 1]; let arr2 = [8, 9, 15]; let merged = [...arr, ...arr2]; // becomes [3, 5, 1, 8, 9, 15]; Lexical environment Okay this is gonna just be a brief summary of what lexical environment is. Every JavaScript script have something called a lexical environment object that is internal. It consists of: Environment record (all of the local variables and methods), and a reference to the outer lexical environment. When you declare a global variable or global function it is stored in the global lexical environment that is associated with the whole script. Here in this example above, phrase is a global variable and hence its record is stored in the global lexical environment. The global lexical environment does not have reference to a outer lexical environment because it is the most outer one, hence it is just null . Variable declaration and function declaration When you declare a variable, it is available in the lexical environment immediately but the value it has is uninitialized from the beginning, and as the script execute to the point where it is initialize, that value is updated. On the other hand, for function declaration (not function expression or arrow functions), they are available immediately become ready-to-use functions. It doesn't have to wait until the line the function becomes defined to be initialized. This is why we are able to call the function before the function declaration! Inner and outer lexical environment When you invoke a function it creates a new lexical environment to store the local variables and parameter of the function call. Every new function invocation will create a new lexical environment! Here in this example, invoking say creates a new lexical environment and has the parameter information in it. It has the reference to the outer lexical environment, which in this case is the global lexical environment. When the function wants to access a variable, the inner lexical environment is searched first, then the outer one, and recursively back up until the global lexical environment. If the variable is not found anywhere, then it is an error in strict mode , without strict mode then assignment to a non-existing variable will be automatically added to the global lexical environment. Returning a function If you wrote a function that returns another function say: function makeCounter() { let count = 0; return function() { return ++count; }; } let counter = makeCounter(); counter(); // count becomes 1 counter(); // count becomes 2 In this case makeCounter() creates a lexical environment that holds the variable count , then it returned a function that will increment the outer count . How does it do that? When counter is invoked later on, it will again create a new lexical environment with nothing in it because it has no variables but rather referring to the outer one. Since it is created in the lexical environment in makeCounter the reference that the inner function points to will be makeCounter 's lexical environment and it has the count . Then when it tries to increment count it will be incrementing the count under makecounter 's lexical environment and it works out! Closure Now this is where closure comes in. A closure is a function that is able to remember it's environment context that it was created in. It remembers the outer variable and is able to access them. Some languages don't support closure, and if it doesn't then what we have just talked about isn't possible. All JavaScript functions are closure, is able to resolve those outer variables that it used, and when those variables goes out of scope it is still able to remember them, have closure per say. The only exception is the new Function syntax, it is not closure. Global object The global object provides variables and functions that are available anywhere, by default it stores the ones that are built into the language or the runtime environment. For browsers it is named window , for Node.js it is global , but it is recently been renamed into globalThis You can access the property of global object directly. In addition, all var variables are becomes the property of the global object. Variables without let or var are implicitly var hence they become the property of the global object as well! (Without strict mode that is. With strict mode, it is an error) Usage of global variable It is generally discouraged, there should be as few global variables as possible, with access via the global object that is. The new Function syntax You can create a new function via a string: let func = new Function([arg1, arg2, ...argN], functionBody); // Both the args and functions should be strings For example: let sum = new Function('a', 'b', 'return a + b'); let sum = new Function('a, b', 'return a + b'); // Both are equivalent. sum(1, 2) // will be 3 Now using this way to create function it is not closure. Meaning that it cannot access any outer variables! This is the only exception that functions aren't closure, in all other cases functions in JavaScript are closure. Named function expression When you are writing a function expression you don't normally give it a name, but the thing is you can, so writing this is perfectly valid: let sayHi() = function func(who) { console.log(`Hi ${who}`); }; sayHi("John"); // Hi John But what does this achieve? By adding a name to the function expression it did not become a function declaration, it is still a function expression! You can still call the function as it is using sayHi . However, by adding func name we are able to let the function calls itself internally, and it is not visible outside of the function. let sayHi = function func(who) { if (!who) { func("Anonymous"); } else { console.log(`Hi ${who}`); } } sayHi(); // Hello, guest! We use func internally instead of sayHi is because the value of sayHi could be changed down the line, and if it is changed, to say a number, then the reference inside would not be valid anymore. let sayHi = function(who) { if (who) { alert(`Hello, ${who}`); } else { sayHi("Guest"); // Error: sayHi is not a function } }; let welcome = sayHi; sayHi = null; welcome(); // Error, the nested sayHi call doesn't work any more! Scheduling setTimeout/setInterval Both follows the function header: setTimeout/setInterval(func | code, [delay], [arg1], [arg2], ...) func : Refers to the function to execute delay : The number of milliseconds to wait before the code specified is executed, by default is 0 so execute immediately. arg1, arg2,... : The arguments for the function that you specified function waveHello() { console.log("I am waving hello!") } setTimeout(waveHello, 1000); // I am waving hello! After one second The difference between setTimeout and setInterval is that setTimeout will only execute the function once after the specified delay, while setInterval will execute that function regularly after every specified delay . So if you write setInterval(waveHello, 1000) this will wave hello after every second. clearTimeout/clearInterval Use these two functions to delete the function that is going to be called after you do setTimeout/setInterval . You will have to pass the "timer identifier" that is returned from calling setTimeout/setinterval , in order to cancel the timer handler. Nested setTimeout A better way of doing interval code execution is via nested setTimeout setTimeout(function tick() { console.log("Doing work every regularly"); setTimeout(tick, 2000); }, 2000); Recalling from named function expression that an function expression can be named and itself can refer to it internally. Now the first function execution will occur after 2 seconds, then it will run the body of the tick function, it will do the work and schedule itself to run 2 seconds again later. Why is this better? It gives us a finer control on when to schedule, instead doing it every 2 seconds, we can control the time of the interval inside the tick function based on say CPU-usage, or how much work is given. We can do it every 10 seconds, 20 seconds, or 60 seconds so a variable interval is what this is trying to emulate. In addition, nested setTimeout guarantees the fixed delay. Since setInterval the function execution can take up the delay, thus making the interval inaccurate. Zero delay setTimeout There is actual a usage for setTimeout(func, 0) . This schedules the execution of the function as soon as possible, but the scheduler is invoked only after the current executing script is complete. Hence the function is scheduled to run right after the current script is finished. setTimeout(() => console.log("World"))console.log("hello!");// Prints out hello! World Function binding If you decide to somehow pass an object's method as a callback into say setTimeout , you will lose the this keyword in the method let user = { firstName: "John", sayHi() { console.log("Hi I am " + this.firstName); } }; setTimeout(user.sayHi, 1000); // This will print Hi I am undefined The method that was passed into setTimeout didn't have a receiver when it was invoked. So again if the method that you invoked doesn't have a receiver, in browser this will be binded to window object, and for Node.js it will be the timer object, but not that relevant. Solution 1: Wrapper We can solve this by wrapping the method that we actually want to invoke in another function call like so let user = { firstName: "John", sayHi() { console.log("Hi I am " + this.firstName); } }; setTimeout(function() { user.sayHi() }, 1000); // This will print Hi I am undefined Now because of closure, it is able to resolve this to be the appropriate user object and this will be fine. However, this solution will fail if the user object somehow changed before the callback is executed, then it will be invoked on the changed value, not the old one anymore. Solution 2: bind To solve the issue that was discussed previously where if the object is somehow changed before the callback is executed, it will be executing callback with the updated object, we can use the bind function to fix this. let boundFunc = func.bind(context); The result of calling bind on a function is another function that has the same body but with this=context fixed. For example: let user = { firstName: "John" }; function func() { console.log(this.firstName); } let funcUser = func.bind(user); funcUser(); // John, because this is set to be user. The returned function will have the same spec as the original function the only thing that changed is that this is fixed to whatever object that you have provided. Using bind we can solve the problem we have just discussed by fixing the this to be that original object, it won't matter if the object changed down the line: let user = { firstName: "John", sayHi() { console.log(`Hello, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); // (*) // You can run it without an explicit receiver sayHi(); // Hello, John setTimeout(sayHi, 1000); // Hello, John // Even if user is changed to something else, it will still do Hello, John user = {}; After you have bind an object, it cannot be changed again! Meaning you cannot do .bind again on the function that was returned. Partial functions With the bind method you can also fulfill the partial parameter, here is an example function mul(a, b) { return a * b; } let double = mul.bind(null, 2); double(3) // 6 double(8) // 16 We can partially fill out the function that we are binding with some predetermined parameter, in this case a=2 , then the user only need to fill out one more parameter b in this case and the result will be returned. You can also make methods that are partial as well if you so to choose, this is done by using the func.call method which invokes the method with the option to provide the this context. Then you can just return a function that will call the method with the predetermined parameters, and let the user provide additional parameter. function partial(func, ...argsBound) { return function(...args) { // Returns a function that is partially filled. Let user put in additional args if needed return func.call(this, ...argsBound, ...args); } } let user = { firstName: "John", say(time, phrase) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // add a partial method with fixed time // takes in the function to partially fill, and the args to prefill with user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); // user.sayNow is a prefilled method, it this is binded to the same user object still // because func.call(this...) // now you can call the function with any additional method that was needed after prefilled user.sayNow("Hello"); Getters & setters Virtual property In addition to the normal property that we have for objects, we can also set up virtual properties. They appears to the outside code as if they were the object's property but they are actually implemented as functions underneath. let user = { name: "John", last: "Smith", get fullName() { return this.name + " " + this.last; } set fullName(value) { [this.name, this.last] = value.split(" "); } }; console.log(user.fullName); // John Smith user.fullName = "Ricky Lu" console.log(user.fullName); // Ricky Lu Like mentioned before to the outside, fullName looks like a property but underneath it is implemented as an method. There is getter that uses the get keyword before the function name to provide a virtual attribute that you can read from, and there is set keyword to provide a setter that you can assign the virtual attribute. For an object there is two type of property, an accessor property which is a virtual property that has either get/set (or both) method, or a data property that is just the normal property which we have been dealing with. Property flags and descriptors For each property that an object has besides the value it contains it also have some additional metadata. Namely, they are called property flags writable : If true , the value can be changed, otherwise it's read-only. If you attempt to assign to non-writable property then error will only show up in strict mode enumerable : If true , the value will be listed in loops, otherwise it is not listed configurable : If true , the property can be deleted and these attributes can be modified, otherwise, it cannot be deleted or modified anymore. If this flag is set the only flag changing operation that is permitted afterward is to turn writable to be true -> false to add another layer of protection value : Describes the value of the property Object.getOwnPropertyDescriptor(obj, propertyName) You can use this method to get the property descriptor of a specific property to see which property flag is set and which one isn't Object.defineProperty(obj, propertyName, descriptor) You can use this method to set a specific property's property flag, if it is defined in the object then it will just update the flag, otherwise, if the property doesn't exist then it will create the property with the given value and flags. If the flags aren't supplied it is assumed to be all false . Object.defineProperties You can define many properties at once instead of just one at a time All about prototypes Prototype inheritance Every object have a special hidden property [[Prototype]] it is either null or a reference to another object. Whenever you read a property from object and if it is missing, JavaScript will automatically take it from the prototype. Setting prototype One of the way to set the prototype is  to use the special name __proto__ : let animal = { eat: true }; let rabbit = { jump: true }; rabbit.__proto__ = animal; console.log(rabbit.eat); // true, taken from the prototype animal console.log(rabbit.jump); // true If animal has any useful method, then since it is a prototype to the rabbit it is able to call it directly as well! let animal = { eats: true, walk() { console.log("Walking"); } }; let rabbit = { jumps: true, __proto__: animal // Another way of setting the prototype }; // walk is taken from the prototype rabbit.walk(); // Walking Limitation The only rules on assigning prototype is that there should be no circular references, and the value of __proto__ should be either an object or null all other types are simply ignored and have no effect. Modifying prototype state What if we did something like this? let user = { name: "Ricky" last: "Lu" get fullName() { return this.name + " " + this.last; } set fullName(value) { [this.name, this.last] = value.split(" "); } }; let admin = { __proto__: user, isAdmin: true }; console.log(admin.fullName); // Ricky Lu, this works as expected admin.fullName = "Another person"; // Now what happens to user.name and user.last? admin.fullName // Another perons user.fullName // Ricky Lu, it stays! The state of user is protected As you can see if you tried to modify the admin's name by calling the prototype's setter function the state change will be enforced on admin and not user ! Even though you are calling  user 's setter method. This is because this always binds to the object that it is called on, and in this case, it is assigning admin.name and admin.last to be "Another" and "person" respectively. user is unchanged. Even if you do something like this: let user = { name: "Ricky" last: "Lu" }; let admin = {}; admin.name = "Another"; admin.last = "person"; user -> Will still be "Ricky Lu" admin -> Will be "Another person", because it is assigning name and last property to admin This behavior is needed because you wouldn't want to modify the prototype's state if multiple object has it as it's prototype, the changes will be untraceable and should be on the object itself not prototype! for...in loop Using for...in it will iterate over the inherited properties as well if it is enumerable, however, Object.keys(obj) will only return the keys this object has itself, not inherited ones: let animal = { eats: true }; let rabbit = { jumps: true, __proto__: animal }; // Object.keys only returns own keys console.log(Object.keys(rabbit)); // jumps // for..in loops over both own and inherited keys for(let prop in rabbit) console.log(prop); // jumps, then eats Native prototypes These are the built-in prototypes that all of the objects inherits from. Object.prototype This is the default prototype that any object created inherits from. And itself's prototype is just null because there is no one above it. This object has some default method that all object contains toString, hasOwnProperty,... hasOwnProperty(prop) Return true/false depending on whether or not the object has that particular property itself, and not inherited. Array.prototype/Function.prototype/Number.prototype These are the other prototypes that some of the objects that you create inherits from such as Arrays, Functions, and Numbers. You can verify that they indeed are those prototype by comparing say [1, 2].__proto__ == Array.prototype it will be equal to true . Primitives Primitives aren't objects, but somehow we are able to access methods on them, how does that work? Well when you try to access their methods a temporary wrapper objects are created using the built-in constructors String, Number, Boolean . They provide the methods and then disappear. The process of creation are invisible to us and engines optimize them out mostly. null and undefined have no object wrapper, so they have no methods or properties. One of the interesting thing that you can do with primitive prototypes is that you can add some custom properties or method that can be used by all primitives. String.prototype.foo = 5; will allow all strings to access a foo property.  let x = 5; you can do x.foo to get 5. Interesting aside If you set a property to an object and marked it as enumerable: false , and you console.log the object, that property will not show up because it is not iterated over using for...in loop! F.prototype When writing constructor functions, i.e. functions that are called with new F() , where F is a constructor function we can take advantage and use F.prototype to do prototype inheritance as well. If we assigned an object to F.prototype then when new F() is invoked, the object that is created will have its __prototype__ pointed to the object that we have assigned. So it is another way that we can do prototype inheritance, instead of setting it manually after the object is created using {...} . let animal = { eat: true }; function Rabbit(name) { this.name = name; } Rabbit.prototype = animal; let rabbit = new Rabbit("White"); // rabbit.__proto__ == animal console.log(rabbit.eat); // true Keep in mind that F.prototype is only used when new F() is called, so if you decide to change it prototype after you created the object, future object created using the constructor function will have the newer prototype, but existing object keep the old one. This only concerns with constructor methods, nothing else. Modern way of setting [[prototype]] .__proto__ is an old way of letting you access and set [[prototype]] and its usage is generally discourage. The modern way of setting [[prototype]] is to use the methods: Object.getPrototypeOf(obj) : This returns the prototype of the object, same as doing  .__proto__ getter Object.setPrototypeOf(obj, proto) : This sets the prototype of the obj to be proto , same as doing .__proto__ setter Object.create(proto, [descriptors]) : Allows you create an object with the specified prototype and optionally descriptors, this is the same as doing {__proto__: ... } True dictionary The bad usage of .__proto__ becomes apparent if we decides to keep a key-value pair mapping using object. If we allow the user to enter any kind of key-value pair mapping and if they decides to enter __proto__ and map to say 5 , it would be invalidated, because using __proto__ you can only assign it another object or null . This isn't the behavior that a dictionary would want right? To fix this we can use  Map or a real empty object, an object with no inherited prototype to begin with to inherit the setter and getter for __proto__ via Object.create(null) . Now we are able to do: let empty = Object.create(null); // No prototype inherited empty.__proto__ = 5; console.log(empty.__proto__); // 5 Classes in JavaScript Class basic syntax Besides using a constructor function there is a more advance object oriented programming construct called class in JavaScript. The basic syntax is as follows: class MyClass { constructor() {} method1() {} method2() {} } To utilize this class that you have just created you would use the same syntax as constructor functions: let x = new MyClass() to create a new object with all of the methods listed. The constructor() method is automatically called by new and you would just do the same thing as a constructor function. What happens underneath When you write class User {...} what it happens underneath is that Creates a function named User , and that is the result of the class declaration. The function code is taken from the constructor method, assume empty you didn't write such method Stores class method that you wrote inside the class in User.prototype just like all the other native prototypes, i.e. String.prototype, Array.prototype, Number.prototype So all the method that you call on the instantiated object will be on the taken from YourClass.prototype. Not just a syntactic sugar Many people say that the class declaration is a syntaxsyntactic sugar on top of constructor function. Yes, but there are still some differences: The constructor created by class declaration must be invoked using new unlike constructor method where you can invoke it directly, even though it won't work properly Class methods are non-enumerable, compared to constructor function's methods Code inside class declaration are always use strict Class expression Like function expression you can also store class declaration into a variable or as something you would returned from a function, basically be part of another expression. let User = class { sayHi() { console.log("Hi"); } }; Getters/setters You can also provide getters and setters as well Class fields You can also add properties to the instances of class that you created. These class fields are set on individual objects not under Class.prototype , so if you change one on one object, the other's class fields aren't affected. class User { name = "John"; sayHi() { console.log(`Hi I am ${this.name}`) } } new User().sayHi(); // Hello, John! Try...catch Syntax try { // Code } catch (err) { // Error handling } finally { // Excuted always, regardless if there is any error. And is always last // It is also executed right before you return if you decide to return/throw from try and catch } Keep in mind that try...catch works synchronously, so if you have asynchronous code and it throws an error after the execution of the try...catch block, then the error will not be caught. In order to catch some exception in an asynchronous code, the try...catch must be inside the asynchronous function. Error object After an error has occurred inside the try...catch object, JavaScript generates an object containing the details about it name : Name of the error that has occurred message : Textual message about error details stack : The current call stack By default, if you print the err object, it will print all three of these information, name, message, and stack . Throwing our own errors We can use the throw operator to generate our own error. Technically we can throw anything as the error object, like primitives, numbers or strings, but is better if you throw an object. let error = new Error(message); try { throw error; } catch (err) { console.log(err); } There are many built-in constructors for standard errors: Error , SyntaxError , ReferenceError Writing custom errors To write your own custom error classes you can extend the Error with your own class class CustomError extends Error { constructor(message) { super(message); // must be first line this.name = "CustomError"; } } function test() { throw new CustomError("Custom test error thrown"); } try { test(); } catch (err) { console.log(err); // CustomError: Custom test error thrown + stack trace. } You can further extend your CustomError to be more specific errors and you can check if those extended errors are part of the CustomError by doing if (err instanceof CustomError) , if it is an error extended from CustomError then the result will be true otherwise, false . Wrapping exceptions Lower level exceptions can be re-raised as a more general exception under one category of exception and the real cause of the exception will be stored in it as a property err.cause . This technique is widespread is commonly used.       Promises and promise chaining Time before promise, callbacks There are entities in JavaScript that let you schedule asynchronous tasks, tasks that you initiate first, then they will finish later. Task like disk reading, fetching resources from network that will take arbitrary amount of time and you don't want your CPU to sit at that line of code waiting until it finishes. You would like to schedule it and then come back when the task finishes. Functions such as setTimeout , setInterval , or asynchronous file reading let you do that, let you schedule an asynchronous task, they start, but finishes later, when the resources that they are waiting on are finished. Often time, after the task is finished, we want to use the result of the task immediately how would we do that? We cannot just add a function call right after the task because it will be scheduled later, and we don't know if it will finishes right after it starts. Say loadScript will take 10 seconds to load, then calling newFunction() immediately after you started loading will be an error. loadScript('myscript.js'); // contains newFunction(), but takes 10 seconds to load newFunction(); // no such function Introducing callbacks A callback function is a function that will be run after the asynchronous task is finished. We can add a callback parameter to the loadScript function function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); // Scripts are loaded asynchronously by the browser. // We have to provide an empty arrow function because the // event handler for onload calls a function with no parameter // inside the body we will invoke the callback, when script is loaded document.head.append(script); } loadScript('myscript.js', (script) => console.load(`My script ${script} is loaded`)); Okay, then this guarantees that "My script ${script} is loaded" message to show up after myscript.js is loaded into the HTML. Now here comes the interesting part, what if I want to load a second script myscript2.js right after myscript.js is finished? Easy, we can just add another loadScript function call inside the callback of the first script load. loadScript('myscript.js', function(script) { console.log("First script loaded"); loadScript('myscript2.js', function(script) { console.log("Second script loaded"); }); }); And what if there is a third script that I want to load only after myscript2.js finishes? We would just continue on nesting into the callbacks, and this is what is called the Pyramid of Doom or callback hell. It gets worse if you added error handling into loadScript itself, what if the script that you are loading was unable to complete? It adds another layer or nesting like so: loadScript('1.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', function(error, script) { if (error) { handleError(error); } else { // ...continue after all scripts are loaded (*) } }); } }); } }); Solution This can be partially alleviated by storing each step of script loading into a function on it's own, but then it creates a function that will only be called once. The better solution is to use Promise Promise An enhancement to callback functions! The idea with Promise is "I promise a result to you at a later time". There are two components to a Promise object, the "Producing code" which is the code that can take some time. Then there is the "Consuming code" which is the code that is waiting for the producing code's result. The "Producing code" is meant to take some long, it will be executed as an asynchronous function and the flow of the execution will move on to the next line of code. When the Promise is settled by either calling resolve/reject with a value, then "Consuming code" kicks in after it is settled and the callback function will be ran. Producer code/Promise object creation To create a new Promise object: let promise = new Promise(function(res, rej) { // the producing code that will take some take // usually will be waiting for some work to be completed before resolving/rejecting }); The executor or the "Producing code" after finishing with it's work will call either resolve if it was successful or reject if there was an error. The Promise object returned by new Promise constructor have two internal properties state : Initially is pending then changes either to fulfilled when resolve is called or rejected when reject is called. result : Initially is undefined , then changes to value when resolve(value) is called or error when reject(error) is called. You usually reject with an Error even though you can technically reject with anything. How executor is ran let promise = new Promise((res, rej) => { // the function is executed automatically when promise is constructed setTimeout(() => resolve("done"), 1000); }); The executor is called automatically and immediately by new Promise i.e. the producing work is immediately started The executor will receive two arguments resolve and reject . These functions are pre-defined by the JavaScript engine, so you just need to use them. After a second resolve is called with the value "done" and the state of  promise is changed to  "fulfilled" with result "done" . What does (1) imply? That means that if you have synchronous code for example a big for loop meant to print out 1 - 100,000,000 inside the Promise it will be executed synchronously before the promise moves onto the line after Promise . Unless you have await or waiting for other promises to resolve within this promise, the code will be executed synchronously. Only one result You can only call resolve or reject once. State change is final, any further calls to resolve/resolve are ignored. Consumers Now we talked about how "Producing code" work, we will look into how consumers uses the Promise . These are handlers that when the Promise resolves either a result or error, will be executed. You register these handlers using .then and .catch . then promise.then( function(result) { /* handles successful result. Pass the value of the promise */ }; function(error) { /* handles reject. Pass the error of the promise */ }; ); The first argument is the handler that will be run when Promise is resolved. The second argument is the handler that will be run when Promise is rejected. In the case that you only interested in the successful result, you can ignore writing the second function for handling the reject. catch If we are only interested in errors, then you can use null as the first argument: .then(null, function(err) { /* error handling code */ }); Or the second way that you can do it is: .catch(function(err) { /* error handling code */ }); They are equivalent. Promise chaining First you have to understand that the method .then will return a new Promise object that you can call .then again, and so on, and this is called Promise chaining. Regardless of what you return, even if you return a primitive it will be wrapped in a Promise object with it's state resolved immediately. If you return a Promise object, then the next .then that you chained will only execute when that Promise you have returned is resolved. In addition, even if you return nothing, .then will create a Promise object with undefined as it's result. Wrong way of chaining let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); promise.then(function(result) { alert(result); // 1 return result * 2; }); This is the wrong way of chaining promises, these are registering three separate handlers listening on the same promise object that will all be executed simultaneously when promise is resolved. new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); // (*) }).then(function(result) { // (**) alert(result); // 1 return result * 2; }).then(function(result) { // (***) alert(result); // 2 return result * 2; }).then(function(result) { alert(result); // 4 return result * 2; }); This is the correct way of chaining the promise handlers, the first .then will only execute when the promise object is resolved after one second. The second .then will execute after the first .then has returned a resolved promise. Since the first .then returns a primitive it will execute immediately right after the first .then is executed. Then so on... Returning promises Inside .then like it was mentioned you can create and return a promise. In that case further handlers will wait until it settles, and then it will execute new Promise(function(res, rej) { setTimeout(() => res(1), 1000); }).then(function(val) { console.log(val); return new Promise((res, rej) => { setTimeout(() => res(val * 2), 1000); }); }).then(function(val) { console.log(val); }); Here, the first .then will execute after one second, then the second .then will execute after another one second, because the first .then returns a new promise, the second handler must wait until that promise resolves before it can execute! .catch for promise chaining You can write one .catch to handle the entire promise chaining if error has occurred anywhere along the chain. This works because if one of the promises' state becomes rejected, then the .then will not execute and it will continue down the chain until it finds .catch or a .then with an error handler. Promise handler have a implicit try...catch , if an exception happens it will get caught and treat as a rejection promise object. new Promise((resolve, reject) => { throw new Error("Whoops"); }).catch(console.log); // Equivalent to new Promise((resolve, reject) => { reject(new Error("Whoops")); }).catch(console.log); It also happens in .then chaining. Good summary picture for promise chaining Solving loadScript with promise Now let's revisit the pyramid of doom that we had with loadScript , how do we make it so that one script loads one after the other without callback hell? Simple! We make loadScript return a promise like so: function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error(`Script load error for ${src}`)); document.head.append(script); }); } The promise object will resolve when the script is loaded and reject when the script failed to load. Now we can just chain loadScript right after one another because it returns a promise. loadScript("myscript1.js") .then(val => { return loadScript("myscript2.js"); // remember loadScript returns a promise }) .then(val => { return loadScript("myscript3.js"); // this will only run when the previous .then's promise resolves, i.e. when myscript2.js is loaded }) .then(val => { one(); two(); three(); // Now you can call the functions that were loaded from each script files }); And it looks way nicer to the eye without callback hell. How are code executed inside Promise? If you write synchronous code inside the executor then it will be executed synchronously immediately, however, the handler will still be executed asynchronously. However, if you write asynchronous code, meaning it will say wait for some task to be completed, then the flow of the code will move onto the next line of the code, and when the task is done then handler will be executed asynchronously as well. More promise API Promise.all This API will allow us to take in an iterable, an array of promises and return a new promise. That returned promise will only be resolved when all listed promises are resolved, the array of their results will become the result of this returned promise. If any of the promises is rejected, the promise returned by Promise.all immediately rejects with that error let promise = Promise.all(iterable); // Example Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(console.log); Promise.allSettled A less stricter version of Promise.all , it waits for all promises to settle, and regardless of the result. let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => { // (*) results.forEach((result, num) => { if (result.status == "fulfilled") { alert(`${urls[num]}: ${result.value.status}`); } if (result.status == "rejected") { alert(`${urls[num]}: ${result.reason}`); } }); }); Promise.race Similar to Promise.all but waits only for the first settled promise and get its result or error The first promise that is resolved will become the value. Promise.any Similar to Promise.any but waits for the first fulfilled promise. If all of the promises are rejected then the returned promise is rejected with Aggregateerror with all the promise error in its errors property. Async and await async keyword async keyword is placed before a function like so: async function f() { return 1; } It basically make your function always returns a promise. If you didn't return a promise explicitly in the function they will be wrapped in a resolved promise automatically. The above async function is equivalent to the one below: function f() { return Promise.resolve(1); } Async function are thenable because they return a promise object: async function f() { return 1; } f().then(console.log); // prints out 1 It just ensures that the returned value is a promise 100% and enables await in the function body, that is all. Function is still executed as a normal function! await keyword You can only use await inside an async function, using it outside of an async function will result in a syntax error The keyword await will make JavaScript wait until that promise settles and return the result of the resolved promise object as the value: async function f() { let promise = new Promise((res, rej) => { setTimeout(() => res("done"), 1000) }); let result = await promise; console.log(result); // prints out done! } f(); await basically makes the function wait at the promise that you used it on and wait until the promise settles before moving on. It will wait for another promise to resolve before moving on with its own execution. The waiting doesn't cost any extra CPU time because since the function is marked asynchronous it can move onto executing other part of the code before resuming the execution after the promise you are awaiting is resolved. Using await is just a more elegant syntax of executing .then , you wait until the promise is resolved before moving on. It is easier to read and write. But you can only use await in a async function! Error handling If the promise resolves normally, await promise returns the result. But in the case of rejection, it will throw the error: async function f() { await Promise.reject(new Error("oops")); } // Is equivalent to async function f(){ throw new Error("oops"); } You can handle that error using try...catch async function f() { try { let response = await fetch('http://no-such-url'); } catch(err) { console.log(err); // TypeError: failed to fetch } } f(); If you don't handle it using try...catch inside the body of the function, then the async function itself will become rejected, and you can handle it by adding .catch to handle it. If you forget .catch then you will get an unhandled promise error. Why they are needed If you are going to use async/await then you will rarely need to write promise.then/catch explicitly. async/await are based on promises. Keep in mind that await doesn't work outside of non-async functions. Example of call async from non-async async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // How do we call wait() and print out the result 10? // simple wait().then(val => console.log(val)); } You would just have to treat wait as a promise object, remember async functions always return a promise object, and then just have to .then it to use the result after it is resolved.       All about modules Two different standards In the browser JavaScript ecosystem, JavaScript modules depends on import and export statements to load and export ES modules. In addition, ES module is the official standard format to package JavaScript code for reuse. On the other hand, Node.js, supports the CommonJS module format by default. CommonJS module load using require() and the variables and functions export from a CommonJS module with module.exports Why the two different standards? Good question. CommonJS module is built into Node.js before ES module were introduced. Node.js support ES module There are two ways you can enable ES modules in Node.js. First way You can simply change the file extension from .js to .mjs to use ES module syntax to load and export modules: // util.mjd export function add(a, b) { return a + b; } // app.mjs import {add} from './util.mjs' console.log(add(1, 2)); // 3 Second way (better way) You can add a "type": "module" field inside the nearest package.json file. By including that field, Node.js will treat all files inside the package as ES modules instead, so you wouldn't have to change to .mjs extensions. How frameworks deal with this You will be using import/export for frameworks like React and Vue.js, the framework themselves will use a transpiler to compile the import/export syntax down to require anyway. Module What is a module? A module is just a file, one script is one module. Simple. Modules can load each other using export and import directives to give and exchange functionality between different files. export : This keyword labels variables and functions that should be accessible from outside import : This keyword allows you to import functionality that are exported by other modules. Example // sayHi.js export function sayHi(user) { console.log("Hello " + user) } Another file can import it and use it // main.js import {sayHi} from './sayHi.js' console.log(sayHi); // function sayHi("Ricky"); // Hello Ricky Base modules Any import statement must get either a relative or absolute URL. Modules without any path are called bare modules: import {sayHi} from 'sayaHi' They are not allowed in browser, but Node.js or bundle tools allow such bare modules Build tools Using a build tool like Webpack will allow you to use bare modules. It also does code optimization and remove unreachable code. Export/import Export and import directives have many syntax variants and we will go over them. Export before declaration Here is how you export along with the declaration of variables and functions: export let months = [1, 2, 3, 4]; // exporting an array export const MODULE_CONST = 69; // exporting a const // exporting a class export class User { constructor(name) { this.name = name; } } // exporting a function export function foo() { console.log("fooing around"); } Export after declaration Here is how you export if you already have the declaration of variables and functions already: let x = 69; function sayHi() { console.log("Hi"); } function sayBye() { console.log("Bye"); } export {x, sayHi, sayBye}; // you pass in a list of exported variables or functions Export as When you export a variable or function you can also choose a different name to export under, so that the modules that will be importing will use the name that you choose // say.js export { sayHi as hi, sayBye as bye}; // main.js import {hi, bye} from './say.js' hi(); bye(); Export default Typically there is two types of modules Module that contain a library like bunch of functions Or modules that declare a single entity which exports say only a class for others to use The second approach is mostly done. And to do it there is the syntax export default to export a default export, and there is only one default export per file, one thing that you can defaulty export per module. // user.js export default class User { constructor(name) { this.name = name; } } // or equivalent class User { constructor(name) { this.name = name; } } export {User as default}; import User from './user.js' // not {User} just User import {default as User} from './user.js' // this is the second way new User("ricky"); When you are importing the default export you do not need any curly braces and it looks nicer. On the other hand, named exports needs curly braces, and default export do not need them When you are using default exports, you always choose the name when you are importing, it doesn't matter what the name is. However, naming it different things might end up confusing some team members, so the best practice is to name the default export the same as the file names. import User from './user.js' import LogicForm from './logicForm.js' import func from '/path/to/func.js' Import Usually you can put a list of what you want to import in curly braces import { sayHi, sayBye, x} from './say.js' Import * But if there is a lot to import you can import everything as an object using import * as import * as say from './say.js' say.sayHi(); say.sayBye(); say.default; // to access the default export if you export everything * Import <> as You can also pick an alias for the functions or variables that you have imported using as import {sayHi as hi, sayBye as bye} from './say.js' hi(); bye(); Import default export along with name export The default keyword is used to reference to the default export import {default as User, sayHi} from './user.js' Re-exporting Re-exporting syntax export ... from allows you to import things and immediately export them like so: export {sayHi} from './say.js' // re-export sayHi export {default as User} from './user.js' // re-export the default export under User name Why would you do this? Well imagine you are writing a package: folder with lots of modules and some under a different folder because they are just helper functions. So your file structure could be like so: auth/ index.js user.js helpers.js tests/ login.js providers/ github.js facebook.js ... You would like to expose the package functionality via just a single entry point, i.e. if someone wants to use your package they can just import only from auth/index.js . import {login, logout} from 'auth/index.js' Instead of doing it from the exact file that it was exported. We can just let the main file to export all the functionality that we want to provide in the package. Other programmer who want to use the package shouldn't need to look into the internal structure, search for files inside the package folder for the exact one to import. They can just look at one main file to import from, while keeping the rest hidden. This is where re-exporting can be used to do this // auth/index.js // import login/logout and export them import {login, logout} from './helpers.js' export {login, logout} // import default as User and then export it import User from './user.js' export {User} Now user can just do import {login} from 'auth/index.js' The syntax export ... from ... is just a shorter notation for such import-export: import {login, logout} from './helpers.js' export {login, logout} import User from './user.js' export {User} // Equivalent to export {login, logout} from './helpers'.js' export {default as User} from './user.js' // This is default re-exporting If you are re-exporting a default export using the shorter notation, you must do it via export {default} from './file.js' and if you are planning to export other named exports from the same file you would have to do export * from './file.js' . Basically, for default exports you would have to handle it separately, you cannot just export * , it will only handle named exports and not default exports.