- JavaScript: The Advanced Concepts Study Notes (Live)
- Table of Contents
- Section 2: JavaScript Foundation
- Javascript Engine
- Inside the Engine
- Interpreters and Compilers
- Babel + TypeScript
- Inside the V8 Engine
- Comparing Other Languages
- Writing Optimized Code
- WebAssembly
- Call Stack and Memory Heap
- Stack Overflow
- Garbage Collection
- Memory Leaks
- Single Threaded
- Javascript Runtime
- Node.js
- Advanced JavaScript Cheatsheet
- Section 3: Javascript Foundation II
- Section 4: Types in JavaScript
- Section 5: The 2 Pillars: Closures and Prototypal Inheritance
- Functions are callable Objects
- First Class Citizens
- Extra Bits: Functions
- Higher Order Functions
- Closures
- Closures and Memory
- Closures and Encapsulation
- Closures Exercises and Solutions
- Prototypal Inheritance
- Inherit the properties of parent object
- Check proprties
- Create our own prototypes
- Only functions has prototype property
- Exercise - extend the functionality of a built in object
- Prototypal Inheritance with this
- Section Review
- Section 6: Object Oriented Programming
- Section 7: Functional Programming
- Section 8: OOP vs FP
- Section 9: Asynchronous JavaScript
- Section 10: Modules In JavaScript
- Section 11: Error Handling
- List of ECMAScript engines
- V8 JavaScript engine
- Brendan Eich
- SpiderMonkey
- What’s the difference between JavaScript and ECMAScript?
- AST Explorer
- JavaScript engine fundamentals: Shapes and Inline Caches
- What happens inside JavaScript Engine ?
- How JavaScript works
const jsengine = code => code.split(/\s+/)
jsengine('var a = 5')
- Interpreter directly executes the instructions in the source programming language
- compiler translates instructions into efficient machine code
- take one language and convert into a different one
- Compilers and Interpreters
- A Deeper Inspection Into Compilation And Interpretation
- Inside the Javascript Engine: Compiler and Interpreter
Javascript compiler
JIT compiler = Interpreter + Compiler
- It all starts with the JavaScript code you write. The JavaScript engine parses the source code and turns it into an Abstract Syntax Tree (AST). Based on that AST, the interpreter can start to do its thing and produce bytecode. Great! At that point the engine is actually running the JavaScript code.
- To make it run faster, the bytecode can be sent to the optimizing compiler along with profiling data. The optimizing compiler makes certain assumptions based on the profiling data it has, and then produces highly-optimized machine code.
- If at some point one of the assumptions turns out to be incorrect, the optimizing compiler deoptimizes and goes back to the interpreter.
- Executable file: The product of the linking process. They are machine code which can be directly executed by the CPU.
- Machine code vs. Byte code vs. Object code vs. Source code vs. Assembly code vs. Executable code
Memoization is a way to cache a return value of a funcBon based on its parameters. This makes the funcBon that takes a long Bme run much faster aoer one execuBon. If the parameter changes, it will sBll have to reevaluate the funcBon.
Javascript Hidden Classes and Inline Caching in V8 Optimizing dynamic JavaScript with inline caches
Here are a few things you should avoid when writing your code if possible:
- eval()
- arguments
- for in
- with
- delete
There are a few main reasons these should be avoided.
- Hidden classes
- Inline caching
// Inline caching
const findUser = user => `Found ${user.firstName} ${user.lastName}`;
const userData = {
firstName: 'Johnson',
lastName: 'Junior'
}
// Code that executes the same method repeatedly will run faster than code that executes many different methods only once (due to inline caching).
findUser(userData)
// Hidden classes
function Animal(x, y) {
// Always instantiate your object properties in the same order so that hidden classes, and subsequently optimized code, can be shared.
this.x = x;
this.y = y;
}
const obj1 = new Animal(1,2);
const obj2 = new Animal(3,4);
// Adding properties to an object after instantiation will force a hidden class change and slow down any methods that were optimized for the previous hidden class. Instead, assign all of an object’s properties in its constructor.
obj1.a = 30;
obj1.b = 100;
obj2.b = 30;
obj2.a = 100;
Why not just use machine code from the beginning?
- Compile code ahead of time or even just compiling the code on the browser was not feasiblea at all because back in the day that was really really slow
- Impossible all the browsers agree on an executable format to run javascript
In the future
- WebAssembly
- Standard binary executable format
- Memory Heap: store and write information
- Call Stack: keep track of where we are in the code
// memory heap
const number = 610; // allocate memory for number
const string = 'some text'; // allocate memory for a string
const human = { // allocate memory for an object and its value
first: 'Chester',
last: 'Heng'
};
const subtractTwo = num => num - 2;
const calculate = () => subtractTwo(4 + 5)
// call stack - FILO
// subtractTwo
// calculate
// Global Execution Context
// When a function calls itself,
// it is called RECURSION
function inception() {
inception();
}
inception();
// returns Uncaught RangeError:
// Maximum call stack size exceeded
- Memory Management
- Garbage collection
- Understanding JavaScript Memory Management using Garbage Collection
var person = {
first: "Brittney",
last: "Postma"
};
person = "Brittney Postma";
// infinite loop
let array = []
for (let i = 5; i > 1; i++) {
array.push(i-1)
}
// global variable
var a = 1;
var b = 1;
var c = 1;
var person = {
first: "Brittney",
last: "Postma"
};
person = "Brittney Postma";
// event listeners
var element = document.getElementById('button')
element.addEventListener('click', onClick)
// setInterval
setInterval(() => {
// reference objec
})
- Only one thing can be executed at a time
- Javascript only has one call stack
- Javascript is a synchronous language
- Imagine an alert on the page
- Block the user from accessing any part of the page until the OK button is clicked
This is where concurrency and the event loop come in.
- The Javascript Runtime Environment
- JS Runtime Playground
- Web APIs
- see window global object for all Web APIs methods
- All synchronous code will be pushed to Call Stack and execute in sequence
- Asynchronous Web APIs code will be executed by browser in the background in sequence
- Browser will store all Callback functions in Callback Queue
- Event Loop check for empty Call Stack to make sure all synchronous code has completed execution
- Push Callback functions in Callback Queue to Call Stack and start execution
Summary
- I/O Intensive
- Non-blocking I/O
References
- Introduction to Node.js / A beginners guide to Node.js and NPM
- How Node.js Works
- How Node JS Works?
- Global Objects
- 10 Things I Regret About Node.js
Global Execution Context
Creation Phase
- Global object created: window
- Initializes this keyword to global
- window === this
Executing Phase
- Variable Environment created - memory space for var variables and functions created
- Initializes all variables to undefined (also known as hoistng) and places them with any functions into memory
this;
window;
this === window;
- lexical environment: the scope or environment the engine is currently reading code in
- a new lexical environment is created when curly brackets {} are used
- lexical scope: available data + variables where function was defined
- dynamic scope: where function is called
function one() {
var isValid = true; // local env
two(); // new execution context
}
function two() {
var isValid; // undefined
}
var isValid = false; // global
one();
// two() isValid = undefined
// one() isValid = true
// global() isValid = false
// -------------------------
// call stack
- Hoisting is the process of putting all variable and function declarations into memory during the compile phase
- functions are fully hoisted
- var variables are hoisted and initialized to undefined
- let and const variables are hoisted but not initialized a value
var favouriteFood = "grapes";
var foodThoughts = function () {
console.log("Original favourite food: " + favouriteFood);
var favouriteFood = "sushi";
console.log("New favourite food: " + favouriteFood);
};
foodThoughts()
// Global Execution Context
// Creation Phase
var favouriteFood = undefined
var foodThoughts = undefined
// Executing Phase
favouriteFood = "grapes"
foodThoughts = function () {
console.log("Original favourite food: " + favouriteFood);
var favouriteFood = "sushi";
console.log("New favourite food: " + favouriteFood);
}
foodThoughts()
// Functional Execution Context
// Creation Phase
var favouriteFood = undefined
// Executing Phase
"Original favourite food: undefined"
favouriteFood = "sushi"
"New favourite food: sushi"
Avoid hoisting when possible. It can cause memory leaks and hard to catch bugs in your code. Use let and const as your go to variables.
// Function Expression
var canada = () => console.log('cold')
// Function Declaration
function india() {
console.log('warm')
}
// Function Invocation / Call / Execution
canada()
india()
Functional Execution Context
- Only when a function is invoked, does a function execution context get created
Creation Phase
- Argument object created with any arguments
- Initializes this keyword to point called or to the global object if not specified
Executing Phase
- Variable Environment created - memory space for variable and functions created
- Initializes all variables to undefined and places them into memory with any new functions
function showArgs(arg1, arg2) {
console.log("arguments: ", arguments);
console.log(Array.from(arguments));
}
showArgs("hello", "world");
function showArgs2(...args) {
console.log("arguments: ", args);
console.log(args[0], args[1]);
}
showArgs2("hello", "world");
function noArgs() {
console.log("arguments: ", arguments);
}
noArgs();
function two() {
var isValid;
}
function one() {
var isValid = true;
two();
}
var isValid = false;
one()
// global execution context creation
// function two() { ... }
// function one() { ... }
// var isValid = undefined;
// global execution context execution
// var isValid = false;
// function one execution context creation
// var isValid = undefined;
// function one execution context execution
// var isValid = true;
// function two execution context creation
// var isValid = undefined;
// function two execution context execution
- Lexical Environment === [[scope]]
- outer scope === [[scope]]
// Scope defines the accessibility of variables and functions in the code
// Global Scope is the outer most scope
function sayMyName() {
var a = 'a';
return function findName() {
var b = 'b';
console.log(a);
return function printName() {
var c = 'c';
console.log(a);
console.log(b);
return 'Andrei Neagoie'
}
}
}
sayMyName()()()
function a() { }
// window: {
// a: f a() {
// [[Scopes]] : [
// Script { },
// Global { }
// ]
// }
// }
// Weird Javascript #1 - it asks global scope for height
// Global scope says: ummm... no but here I just created it for you.
// We call this leakage of global variables.
function weird() {
height = 50
}
weird()
// leakage of global variables is not allowed
'use strict'
function weird() {
height = 50
}
weird()
var heyhey = function doodle() {
return "heyhey"
}
heyhey();
doodle(); // Error! because it is enclosed in its own scope.
// Function Scope
function loop() {
for( var i = 0; i < 5; i++) {
console.log(i);
}
console.log(i)
}
// Block Scope
function loop2() {
for( let i = 0; i < 5; i++) {
console.log(i);
}
console.log(i)
}
// variable collisions
// all global variables are on the global execution context
// they overwrite each other
var z = 1;
var z = 2;
z
// place all library code inside local scope
// to avoid any namespace collisions
// function expression
// anonymous function
(function() {
})();
// cannot call function declaration immediately
function a(){}()
var script1 = (function() {
function a() {
return 5;
}
return {
a: a
}
})();
script1.a()
this is the object that the function is a property of
function a() {
console.log(this) // this refer to window object
}
const b = () => console.log(this) // this refer to window object
a()
b()
// gives methods access to their object
const obj = {
name: 'Billy',
sing: function() {
return 'llala ' + this.name + '!'
},
singAgain: function() {
return this.sing()
}
}
obj.singAgain()
// execute some code for multiple objects
function importantPerson() {
console.log(this.name)
}
importantPerson() // this refer to window object
const name = 'Sunny';
const obj1 = { name: 'Cassy', importantPerson }
const obj2 = { name: 'Jacob', importantPerson }
obj1.importantPerson() // Cassy
obj2.importantPerson() // Jacob
var b = {
name: 'jay',
say() {console.log(this)}
}
var c = {
name: 'jay',
say() {return function() {console.log(this)}}
}
var d = {
name: 'jay',
say() {return () => console.log(this)}
}
b.say() // this refer to b object
c.say()() // this refer to window object
d.say()() // this refer to b object
const character = {
name: 'Simon',
getCharacter() {
return this.name;
}
};
const giveMeTheCharacterNOW = character.getCharacter.bind(character);
giveMeTheCharacterNOW()
- lexical scope: available data + variables where function was defined
- dynamic scope: where function is called
- this keyword is dynamic scope
Lexical scope | Dynamic Scope |
---|---|
write-time | run-time |
where a function was declared | where a function was called from |
const a = function() {
console.log('a', this) // this refer to window object
const b = function() {
console.log('b', this) // this refer to window object
const c = {
hi: function() {
console.log('c', this) /// this refer to c object
}
}
c.hi()
}
b()
}
a()
const obj = {
name: 'Billy',
sing: function() {
console.log('a', this) // refer to obj object
const anotherFunc = function() {
console.log('b', this) // refer to window object
}
}
}
obj.sing()
const obj = {
name: 'Billy',
sing() {
console.log('a', this) // refer to obj object
// arrow function is lexical bound
const anotherFunc = () => console.log('b', this) // refer to obj object
anotherFunc()
}
}
obj.sing()
const obj = {
name: 'Billy',
sing() {
console.log('a', this) // refer to obj object
const anotherFunc = function() {
console.log('b', this) // refer to obj object
}
return anotherFunc.bind(this)
}
}
obj.sing()()
const obj = {
name: 'Billy',
sing() {
console.log('a', this) // refer to obj object
const self = this;
const anotherFunc = function() {
console.log('b', self) // refer to obj object
}
return anotherFunc
}
}
obj.sing()()
function a() {
console.log('hi')
}
a()
a.call()
a.apply()
const wizard = {
name: 'Merlin',
health: 100,
heal(num1, num2) {
this.health += num1 + num2;
}
}
const archer = {
name: 'Robin Hood',
health: 50
}
wizard.heal(10, 60)
// borrow heal method from wizard object
wizard.heal.call(archer, 50, 60)
wizard.heal.apply(archer, [20, 30])
// borrow heal method from wizard object
// call healArcher later on with archer object
const healArcher = wizard.heal.bind(archer, 50, 60);
healArcher()
const array = [1,2,3];
function getMaxNumber(arr) {
return Math.max.apply(null, arr);
}
const getMaxNumber = arr => Math.max.apply(null, arr)
getMaxNumber(array)
- translates a function from callable as f(a, b, c) into callable as f(a)(b)(c)
- Currying
- Currying: A Functional Alternative To fn.bind
function sum(a, b) {
return a + b;
}
// curry(f) does the currying transform
function curry(f) {
return function(a) {
return function(b) {
return f(a, b);
};
};
}
let curriedSum = curry(sum);
curriedSum(1)(2);
function multiply(a, b, c) {
return a * b * c;
}
const multipleByTwo = multiply.bind(this, 2, 4);
console.log(multipleByTwo(4));
const multipleByThree = multiply.bind(this, 3, 5);
console.log(multipleByThree(4));
- Understanding Scope and Context in JavaScript
- Scope is function based
- what is the variable access of a function when it is invoked?
- What is in the variable environment?
- scope refers to the visibility of variables
- Context is object based
- what is the value of this keyword?
- context is most often determined by how a function is invoked with the value of this keyword
// Primitive
typeof 5
typeof true
typeof 'To be or not to be'
typeof undefined // absent of definition
typeof null // absent of value
typeof Symbol('just me')
// Non-Primitive
// has a reference or pointer
typeof {}
typeof []
typeof function() {}
const obj1 = {
a: 'Tom'
}
// function and array are objects
function a() {
return 5;
}
a.hi = "hi"
// Object wrapper
true.toString()
Boolean(true).toString()
const array = ['1', '2', '3'];
const obj = {
0: '1',
1: '2',
2: '3'
}
Array.isArray(array)
Array.isArray(obj)
// pass by value
var a = 5;
var b = a;
b++;
// pass by reference
let obj1 = { name: 'Yao', password: '123' };
let obj2 = obj1;
obj2.password = 'easy'
var c = [1, 2, 3];
var d = c;
d.push(4)
var c = [1, 2, 3];
var d = [].concat(c); // clone array
d.push(4)
Compare objects with JavaScript
var user1 = {name : "nerd", org: "dev", c: { d: "d" } };
var user2 = {name : "nerd", org: "dev", c: { d: "d" } };
JSON.stringify(user1) === JSON.stringify(user2)
const number = 100
const string = "Jay"
let obj1 = {
value: "a"
}
let obj2 = {
value: "b"
}
let obj3 = obj2;
function change(number, string, obj1, obj2) {
number = number * 10;
string = "Pete";
obj1 = obj2;
obj2.value = "c";
}
change(number, string, obj1, obj2);
//Guess the outputs here before you run the code:
// number 100
// string 'Jay'
// obj1 { value: 'a' }
// obj2 { value: 'c' }
// obj3 { value: 'c' }
if(1) { // true
console.log(5);
}
if(0) { // false
console.log(5);
}
// Invoke function
function one() {
return 1
}
one()
one.call()
one.apply()
const obj = {
two: function() {
return 2;
}
}
obj.two()
const four = new Function('num', 'return num')
four(4)
Function
- code ()
- name (optional)
- properties: .call(), .apply(), .bind()
- pass function around like object
function woohooo() {
console.log('woohooo')
}
woohooo.yell = 'ahhhhhhh'
woohooo.name
// const specialObj = {
// yell: 'ahhhhhhh',
// name: 'woohooo',
// (): console.log('woohooo')
// }
Functions are first class citizens in JavaScript
- Function can be assigned to a variable
- Function can be passed as a paramter to another function
- Function can be returned as a value from other function
// can be assigned to a variable
var stuff = function(){}
// pass a function as paramter to another function
function a(fn) {
fn()
}
a(function() { console.log('hi there')})
// return function as a value from other function
function b() {
return function c() { { console.log('bye')} }
}
b()()
var d = b()
d()
- make sure function is initialize one time only
- use default parameters
for (let i = 0; i < 5; i++){
function a() { } // initialize 5 times
a()
}
function a() { } // initialize once
for (let i = 0; i < 5; i++){
a()
}
// default parameters
function b(param = 6) {
return param
}
b()
- Defining functions
- function() => function(a, b) => HOF
function() | function(a, b) | HOF | |
---|---|---|---|
what data to use ? | function definition | during invocation | during invocation |
what to do ? | function definition | function definition | during invocation |
// function()
function letAdamLogin() {
let array = [];
for (let i = 0; i < 50000; i++) {
array.push(i)
}
return 'Access Granted to Adam'
}
function letEvaLogin() {
let array = [];
for (let i = 0; i < 50000; i++) {
array.push(i)
}
return 'Access Granted to Eva'
}
letAdamLogin()
letEvaLogin()
// function(a, b)
// function with paramter
// what data to use when function is called ?
const giveAccessTo = (name) =>
'Access Granted to ' + name;
function letUserLogin(user) {
let array = [];
for (let i = 0; i < 50000; i++) {
array.push(i)
}
return giveAccessTo(user)
}
function letAdminLogin(admin) {
let array = [];
for (let i = 0; i < 1000000; i++) {
array.push(i)
}
return giveAccessTo(admin)
}
letUserLogin('Adam')
letUserLogin('Eva')
// HOF: function(a, b, fn)
// what data to use during invocation ?
// what to do during invocation ?
function authenticate(person) {
let array = [];
console.log(`Level: ${person.level}`)
for (let i = 0; i < 50000; i++) {
array.push(i)
}
return giveAccessTo(person.name)
}
function sing(person) {
return `lalalala ${person.name}`
}
function letPerson(person, fn) {
if (person.level === 'admin') {
return fn(person)
} else if (person.level === 'user') {
return fn(person)
}
}
letPerson({ level: 'user', name: 'Tim' }, authenticate)
letPerson({ level: 'admin', name: 'Sally' }, sing)
// multiplyBy is HOF
const multiplyBy = (num1) => {
return function (num2) {
return num1 * num2;
}
}
const multiplyByTwo = multiplyBy(2);
multiplyByTwo(4)
// multiplyBy1 is HOF
const multiplyBy1 = num1 => num2 => num1 * num2;
multiplyBy1(2)(4)
A closure is a function that has access to its outer scope that it is defined
- closure = function() + lexical scope (where we write the function)
function a() {
let grandpa = 'grandpa'
return function b() {
let father = 'father'
return function c() {
let son = 'son'
return `${grandpa} > ${father} > ${son}`
}
}
}
a()()()
//closures and higher order function
function boo(string) {
return function(name) {
return function(name2) {
console.log(`${string} ${name} ${name2}`)
}
}
}
const boo2 = (string) => (name) => (name2) => console.log(`${string} ${name} ${name2}`)
boo('hi')('john')('tanya');
boo2('hi')('john')('tanya');
const booString = boo2('sing');
const booStringName = booString('John');
const booStringNameName2 = booStringName('tanya')
function callMeMaybe() {
setTimeout(function() {
console.log(callMe);
}, 4000);
const callMe = 'Hi!';
}
callMeMaybe();
// create bigArray everytime heavyDuty is executed
function heavyDuty(item) {
const bigArray = new Array(7000).fill('😄')
console.log('created!');
return bigArray[item]
}
heavyDuty(699)
heavyDuty(699)
heavyDuty(699)
// Memory efficient
// create bigArray once with closure
function heavyDuty2() {
const bigArray = new Array(7000).fill('😄')
console.log('created Once!')
return function(item) {
return bigArray[item]
}
}
const getHeavyDuty = heavyDuty2();
getHeavyDuty(699)
getHeavyDuty(699)
getHeavyDuty(699)
// closure hide passTime and launch functions
const makeNuclearButton = () => {
let timeWithoutDestruction = 0;
const passTime = () => timeWithoutDestruction++;
const totalPeaceTime = () => timeWithoutDestruction;
const launch = () => {
timeWithoutDestruction = -1;
return '💥';
}
setInterval(passTime, 1000);
return { totalPeaceTime }
}
const ww3 = makeNuclearButton();
ww3.totalPeaceTime();
// Exercise
let view;
function initialize() {
view = '🏔';
console.log('view has been set!')
}
initialize();
initialize();
initialize();
view
// Solution
let view;
function initialize() {
let called = 0;
const startOnce = () => {
if (called > 0) {
return
} else {
view = '🏔';
called++;
console.log('view has been set!')
}
}
return { startOnce };
}
const app = initialize();
app.startOnce();
app.startOnce();
app.startOnce();
view
// Exercise
const array = [1,2,3,4];
for(var i=0; i < array.length; i++) {
setTimeout(function() {
console.log('I am at index ' + i)
}, 3000)
}
// Solution 1
const array = [1,2,3,4];
for(let i=0; i < array.length; i++) {
setTimeout(function() {
console.log('I am at index ' + i)
}, 3000)
}
// Solution 2
const array = [1,2,3,4];
for(var i=0; i < array.length; i++) {
(function(index) {
setTimeout(function() {
console.log('I am at index ' + index)
}, 3000)
})(i)
}
const array = []
array.__proto__ // Array []
array.__proto__.__proto__ // Object {}
function a() {}
a.__proto__ // Function ()
a.__proto__.__proto__ // Object {}
const obj1 = {}
obj1.__proto__ // Object {}
obj1.__proto__.__proto__ // null
let dragon = {
name: 'Tanya',
fire: true,
fight() {
return 5
},
sing() {
if (this.fire) {
return `I am ${this.name}, the breather of fire`
}
}
}
let lizard = {
name: 'Kiki',
fight() {
return 1
}
}
// lizard inherit the properties of dragon
// only need memory for one instance of sing() method
lizard.__proto__ = dragon;
lizard.sing()
lizard.fire
lizard.fight()
dragon.__proto__
dragon.isPrototypeOf(lizard);
// list all proprties of lizard
for(let prop in lizard) {
if(lizard.hasOwnProperty(prop)) {
console.log(`lizard: ${prop}`)
}
else console.log(`dragon: ${prop}`)
}
// __proto__: pointer to parent prototype object
const obj = { name: 'Sally' }
obj.hasOwnProperty('name')
obj.__proto__.hasOwnProperty('hasOwnProperty')
Object.prototype.hasOwnProperty('hasOwnProperty')
function a() {}
a.hasOwnProperty('name')
a.prototype
a.__proto__.hasOwnProperty('call')
a.__proto__.hasOwnProperty('apply')
a.__proto__.hasOwnProperty('bind')
Function.prototype.hasOwnProperty('call')
Function.prototype.hasOwnProperty('apply')
Function.prototype.hasOwnProperty('bind')
var human = { mortal: true }
var socrates = Object.create(human);
socrates.age = 70
human.isPrototypeOf(socrates);
socrates.hasOwnProperty('age')
socrates.__proto__.hasOwnProperty('mortal')
function multiply5(num) {
return num * 5;
}
multiply5.prototype
multiply5.__proto__ // Function
Function.prototype // Function
multiply5.__proto__.__proto__ // Object
Object.prototype // Object
multiply5.__proto__.__proto__.__proto__ // null
// Date object => to have new method .lastYear() which shows you last year 'YYYY' format.
Date.prototype.lastYear = function() {
return this.getFullYear() - 1;
}
new Date('1900-10-10').lastYear() //'1899'
// Mofify .map() to print '🗺' at the end of each item.
Array.prototype.map = function() {
let arr = [];
for(let i = 0; i < this.length; i++ ){
arr.push(`${this[i]}🗺`)
}
return arr;
}
console.log([1,2,3].map()) //1🗺, 2🗺, 3🗺
Function.prototype.bind = function(whoIsCallingMe) {
const self = this;
return function() {
return self.apply(whoIsCallingMe, arguments);
};
}
- The Scheme Programming Language
- Java
- Brendan Eich on Creating JavaScript in 10 Days, and What He’d Do Differently Today
Programming paradigms
- Clear + Understandable
- Easy to Extend
- Easy to Maintain
- Memory Efficient
- DRY
History of programming languages
- Procedure programming
- object oriented programming
- Functional programming
Data and behavior
Scheme | Java |
---|---|
Functions | Objects |
FP | OOP |
Object oriented programming is all about modeling real world objects and relationships.
// each elf object has a copy of attack method
function createElf(name, weapon) {
return {
name: name,
weapon: weapon,
attack() {
return `${name} attacks with ${weapon}`
}
}
}
const sam = createElf('Sam', 'stones');
const peter = createElf('Peter', 'fire');
sam.attack()
peter.attack()
// all elf object access to shared copy of elfFunctions via __proto__ pointer
const elfFunctions = {
attack: function() {
return `${this.name} attacks with ${this.weapon}`
}
}
function createElf(name, weapon) {
const newElf = Object.create(elfFunctions)
console.log(newElf.__proto__)
newElf.name = name;
newElf.weapon = weapon
return newElf
}
const sam = createElf('Sam', 'stones');
const peter = createElf('Peter', 'fire');
sam.attack()
peter.attack()
function Elf(name, weapon) {
this.name = name;
this.weapon = weapon;
}
Elf.prototype.attack = function() {
return `${this.name} attacks with ${this.weapon}`
}
const sam = new Elf('Sam', 'stones');
const peter = new Elf('Peter', 'fire');
sam.attack()
peter.attack()
class Elf {
// run constructor with new keyword
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
// shared attack method by all objects
attack() {
return `${this.name} attacks with ${this.weapon}`
}
}
const sam = new Elf('Sam', 'stones');
const peter = new Elf('Peter', 'fire');
sam.attack()
peter.attack()
// new binding
function Person(name, age) {
this.name = name;
this.age =age;
console.log(this);
}
const person1 = new Person('Xavier', 55)
//implicit binding
const person = {
name: 'Karen',
age: 40,
hi() {
console.log('hi' + this.name)
}
}
person.hi()
//explicit binding
const person3 = {
name: 'Karen',
age: 40,
hi: function() {
console.log('hi' + this.setTimeout)
}.bind(window)
}
person3.hi()
// arrow functions
const person4 = {
name: 'Karen',
age: 40,
hi: function() {
var inner = () => {
console.log('hi ' + this.name)
}
return inner()
}
}
person4.hi()
class Character {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return `${this.name} attacks with ${this.weapon}`
}
}
class Elf extends Character {
constructor(name, weapon, type) {
// console.log('what am i?', this); this gives an error
super(name, weapon)
console.log('what am i?', this);
this.type = type;
}
}
class Ogre extends Character {
constructor(name, weapon, color) {
super(name, weapon);
this.color = color;
}
makeFort() { // this is like extending our prototype.
return 'strongest fort in the world made'
}
}
const houseElf = new Elf('Dolby', 'cloth', 'house')
houseElf.makeFort() // error
const shrek = new Ogre('Shrek', 'club', 'green')
shrek.makeFort()
JavaScript’s New Private Class Fields, and How to Use Them
- Encapsulation: wrap data and code into objects
- Abstraction: hide the complexity in classes
- Inheritance
- Polymorphism: call the same methods on different objects with different responses
class Character {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'atack with ' + this.weapon
}
}
class Queen extends Character {
constructor(name, weapon, kind) {
super(name, weapon)
this.kind = kind;
}
attack() {
console.log(super.attack());
return `I am the ${this.name} of ${this.kind}, now bow down to me! `
}
}
const victoria = new Queen('Victoria', 'army', 'hearts');
victoria.attack()
- Functional Programming - Lambda Calculus
- Separate between data over a program and the behavior of a program
- All objects created in functional programming are immutable
- Pure functions
const user = {
name: 'Kim',
active: true,
cart: [],
purchases: []
}
//Implement a cart feature:
// 1. Add items to cart.
// 2. Add 3% tax to item in cart
// 3. Buy item: cart --> purchases
// 4. Empty cart
//Bonus:
// accept refunds.
// Track user history.
- no side effect
- no mutation
- same input -> same output
- map and concat methods can fix this issue of mutation
- Referential Transparency
// Side effects
// mutateArray modifies array outside of itself
// mutateArray2 modifies array outside of itself
const array = [1,2,3];
function mutateArray(arr) {
arr.pop()
}
function mutateArray2(arr) {
arr.forEach(item => arr.push(1
))
}
array
mutateArray(array)
mutateArray2(array)
array
// No side effects
const array = [1,2,3];
function mutateArray(arr) {
const newArray = [].concat(arr);
newArray.pop()
return newArray;
}
function mutateArray2(arr) {
const newArray = [].concat(arr);
newArray.forEach(item => newArray.push(1
))
return newArray;
}
function mutateArray3(arr) {
return arr.map(item => item * 2)
}
array
mutateArray(array)
mutateArray2(array)
mutateArray3(array)
array
Functional programming at the end of the day is just about making your code predictable
The Perfect Function
- Do 1 task only
- Return Statement
- Pure
- No shared state
- Immutable state
- Composable
- Predictable
- Idempotence is a property of some operations such that no matter how many times you execute them, you achieve the same result
function notGood(num) {
return Math.random(num)
}
// Idempotence
function good() {
return 5
}
Math.abs(Math.abs(10))
- Imperative versus declarative code… what’s the difference?
- Imperative vs. Declarative Programming (in 60 seconds)
- Imperative: How?
- Declarative: What?
// Imperative
for (let i = 0; i < 3, i++) {
console.log(i)
}
// Declarative
[1, 2, 3].forEach(item => console.log(item))
Immutable.js, persistent data structures and structural sharing
const obj = { name: 'Andrei' }
function clone(obj) {
return { ...obj }; // this is pure
}
function updateName(obj) {
const obj2 = clone(obj);
obj2.name = 'Nana'
return obj2
}
const updatedObj = updateName(obj)
//HOF
const hof = (fn) => fn(5);
hof(function a(x) { return x} )
//Closure and HOF
const closure = function() {
let count = 55;
return function getCounter() {
return count;
}
}
const getCounter = closure()
getCounter()
const multiply = (a, b) => a * b
const curriedMultiply = a => b => a * b
curriedMultiply(5)(3);
const curriedMultiplyBy5 = curriedMultiply(5);
curriedMultiplyBy5(3)
const multiply = (a, b, c) => a * b * c
const curriedMultiply = a => b => c => a * b * c
curriedMultiply(5)(4)(10)
const multiply = (a, b, c) => a * b * c
const partialMultiplyBy5 = multiply.bind(null, 5)
partialMultiplyBy5(4, 10)
// compose(fn1, fn2, fn3)(50)
// right to lext
// data -> fn3 -> data -> fn2 -> data -> fn1 -> data
const compose = (f, g) => a => f(g(a));
const multiplyBy3 = num => num * 3;
const Add10 = num => num + 10;
const multiplyBy3AndAbsolute = compose(multiplyBy3, Add10);
multiplyBy3AndAbsolute(-50)
// pipe(fn1, fn2, fn3)(50)
// left to right
// data -> fn1 -> data -> fn2 -> data -> fn3 -> data
const pipe = (f, g) => (a) => g(f(a));
const multiplyBy3AndAbsolute2 = pipe(multiplyBy3, Add10);
multiplyBy3AndAbsolute2(-50)
Arity of a function (or operation) describes the number of arguments that the function (or operation) takes
- Function arity
- arity: 1 to 2 is preferred
const user = {
name: 'Kim',
active: true,
cart: [],
purchases: []
}
const history1 = [];
const compose = (f, g) => (...args) => f(g(...args))
const pipe = (f, g) => (...args) => g(f(...args))
const purchaseItem = (...fns) => fns.reduce(compose);
const purchaseItem2 = (...fns) => fns.reduce(pipe);
function addItemToCart(user, item) {
history1.push(user)
const updatedCart = user.cart.concat(item)
return Object.assign({}, user, { cart: updatedCart });
}
function applyTaxToItems(user) {
history1.push(user)
const { cart } = user;
const taxRate = 1.3;
const updatedCart = cart.map(item => ({
name: item.name,
price: item.price * taxRate
}))
return Object.assign({}, user, { cart: updatedCart });
}
function buyItem(user) {
history1.push(user)
const itemsInCart = user.cart;
return Object.assign({}, user, { purchases: itemsInCart });
}
function emptyUserCart(user) {
history1.push(user)
return Object.assign({}, user, { cart: [] });
}
history1
purchaseItem2(
addItemToCart,
applyTaxToItems,
buyItem,
emptyUserCart,
)(user, { name: 'laptop', price: 60 })
history1
function refundItem() {
}
function getUserState() {
}
function goBack() {
}
function goForward() {
}
- Inheritance: a superclass that is extended to smaller pieces that add or overwrite things
- Composition: smaller pieces that are combined to create something bigger
FP | OOP |
---|---|
many operations on fixed data | few operations on common data |
stateless | stateful |
pure | side effect |
declarative | imperative |
- Job Queue has high priority than Callback Queue
- Understanding Event Loop, Call Stack, Event & Job Queue in Javascript
// Callback Queue - Task Queue
setTimeout(() => { console.log('1', 'is the loneliest number') }, 0)
setTimeout(() => { console.log('2', 'can be as bad as one') }, 10)
// Job Queue - Microtask Queue
Promise.resolve('hi').then( data => console.log('2', data))
console.log('3','is a crowd')
Promises for asynchronous programming
const promisify = (item, delay) =>
new Promise((resolve) =>
setTimeout(() =>
resolve(item), delay));
const a = () => promisify('a', 100);
const b = () => promisify('b', 5000);
const c = () => promisify('c', 3000);
async function parallel() {
const promises = [a(), b(), c()];
const [output1, output2, output3] = await Promise.all(promises);
return `prallel is done: ${output1} ${output2} ${output3}`
}
async function race() {
const promises = [a(), b(), c()];
const output1 = await Promise.race(promises);
return `race is done: ${output1}`;
}
async function sequence() {
const output1 = await a();
const output2 = await b();
const output3 = await c();
return `sequence is done ${output1} ${output2} ${output3}`
}
sequence().then(console.log)
parallel().then(console.log)
race().then(console.log)
- Concurrency vs Parallelism
- A gentle introduction to multithreading
- Using Web Workers
- Scaling Node.js Applications
JavaScript Module Pattern Basics
- global scope
- module scope
- function scope
- block scope - let and const
// IIFE
// Module Pattern
var fightModule = (function() {
var harry = 'potter'
var voldemort = 'He who must not be named'
function fight(char1, char2) {
var attack1 = Math.floor(Math.random() * char1.length);
var attack2 = Math.floor(Math.random() * char2.length);
return attack1 > attack2 ? `${char1} wins` : `${char2} wins`
}
return { fight }
})()
fightModule.fight('harry', 'voldemort')
Call Stack
-
ERROR!
-
Is there a catch?
-
Is there a catch?
-
Runtime catch: onerror() - browser
-
process.on('uncaughtException') - Node JS
throw 'Error2'; // String type
throw 42; // Number type
throw true; // Boolean type
throw Error
throw new Error // will create an instance of an Error in JavaScript and stop the execution of your script.
function a() {
const b = new Error('what?')
return b
}
a().stack
let error = new Error(message);
let error2 = new SyntaxError(message);
let error3 = new ReferenceError(message);
function fail() {
try {
console.log('this works');
throw new Error('oopsie');
} catch(e) {
console.log('error', e);
} finally {
console.log('still good');
return 'returning from fail';
}
console.log('never going to get here'); // not reachable
}
fail();
Promise.resolve('asyncfail')
.then(response => {
console.log(response)
throw new Error('#1 fail')
})
.then(response => {
console.log(response)
})
.catch(err => {
console.error('error', err.message)
})
.then(response => {
console.log('hi am I still needed?', response)
return 'done'
})
.catch(err => {
console.error(err)
return 'failed'
})
(async function() {
try {
await Promise.reject('oopsie')
} catch (err) {
console.error(err)
}
console.log('This is still good!')
})()
(function () {
try {
throw new Error();
} catch (err) {
var err = 5;
var boo = 10;
console.log(err);
}
//Guess what the output is here:
console.log(err);
console.log(boo);
})();
class authenticationError extends Error {
constructor(message) {
super(message)
this.name = 'ValidationError'
this.message = message
}
}
class PermissionError extends Error {
constructor(message) {
super(message)
this.name = 'PermissionError'
this.message = message
this.favouriteSnack = 'grapes'
}
}
class DatabaseError extends Error {
constructor(message) {
super(message)
this.name = 'DatabaseError'
this.message = message
}
}
throw new PermissionError('A permission error')