Skip to content

Instantly share code, notes, and snippets.

@shivam1283
Last active December 21, 2022 08:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save shivam1283/f58fc0b3cde0311416827f3509cf04c2 to your computer and use it in GitHub Desktop.
Save shivam1283/f58fc0b3cde0311416827f3509cf04c2 to your computer and use it in GitHub Desktop.
JS concepts

Callback

function loadScript(src) {
  // creates a <script> tag and append it to the page
  // this causes the script with given src to start loading and run when complete
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...

Let’s say we need to use the new script as soon as it loads. It declares new functions, and we want to run them.

loadScript('/my/script.js'); // the script has "function newFunction() {…}"

newFunction(); // no such function!

Let’s add a callback function as a second argument to loadScript that should execute when the script loads:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

Now if we want to load multiple scripts synchronously, we can use loadscript in this fashion.

loadscript('script1.js', function(){
  loadscript('script2.js', function(){
   loadscript('script3,js', function(){
   }) 
  })
})

Handling errors

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onerror = callback(new Error('failed to load script'))
  script.onload = () => callback(null, script);

  document.head.append(script);
}

loadScript('errorscript',function(error, script){
  if(error){
    console.log('error')
  } else {
    //success 
  }
})

It’s called the “error-first callback” style.

The convention is:

  1. The first argument of the callback is reserved for an error if it occurs. Then callback(err) is called.
  2. The second argument (and the next ones if needed) are for the successful result. Then callback(null, result1, result2…) is called.

Pyramid of Doom

At first glance, it looks like a viable approach to asynchronous coding. And indeed it is. For one or maybe two nested calls it looks fine.

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 (*)
          }
        });

      }
    });
  }
});

In the code above:

  1. We load 1.js, then if there’s no error…
  2. We load 2.js, then if there’s no error…
  3. We load 3.js, then if there’s no error – do something else (*).

As calls become more nested, the code becomes deeper and increasingly more difficult to manage, especially if we have real code instead of ... that may include more loops, conditional statements and so on.

That’s sometimes called “callback hell” or “pyramid of doom.”

Image

The “pyramid” of nested calls grows to the right with every asynchronous action. Soon it spirals out of control.

So this way of coding isn’t very good.

We can try to alleviate the problem by making every action a standalone function, like this:

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...continue after all scripts are loaded (*)
  }
}

It works, but the code looks like a torn apart spreadsheet. It’s difficult to read, and you probably noticed that one needs to eye-jump between pieces while reading it. That’s inconvenient, especially if the reader is not familiar with the code and doesn’t know where to eye-jump.

Also, the functions named step* are all of single use, they are created only to avoid the “pyramid of doom.” No one is going to reuse them outside of the action chain. So there’s a bit of namespace cluttering here.

Promise

A “producing code” that does something and takes time. For instance, some code that loads the data over a network. That’s a “singer”.
A “consuming code” that wants the result of the “producing code” once it’s ready. Many functions may need that result. These are the “fans”.
A promise is a special JavaScript object that links the “producing code” and the “consuming code” together. In terms of our analogy: this is the “subscription list”. The “producing code” takes whatever time it needs to produce the promised result, and the “promise” makes that result available to all of the subscribed code when it’s ready.

Syntax

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});

The function passed to new Promise is called the executor. When new Promise is created, the executor runs automatically. It contains the producing code which should eventually produce the result.

When the executor obtains the result, be it soon or late, doesn’t matter, it should call one of these callbacks:

  • resolve(value) — if the job is finished successfully, with result value.
  • reject(error) — if an error has occurred, error is the error object.

The promise object returned by the new Promise constructor has these internal properties:

  • state — initially "pending", then changes to either "fulfilled" when resolve is called or "rejected" when reject is called.
  • result — initially undefined, then changes to value when resolve(value) called or error when reject(error) is called.

Image

let p = new Promise(function(resolve, reject){
 setTimeout(() => {
 	resolve('done)
 },1000)
})

We can see two things by running the code above:

  1. The executor is called automatically and immediately (by new Promise).

  2. The executor receives two arguments: resolve and reject. These functions are pre-defined by the JavaScript engine, so we don’t need to create them. We should only call one of them when ready.

After one second of “processing” the executor calls resolve("done") to produce the result. This changes the state of the promise object:

Image

Now with error

let p = new Promise(function(resolve, reject){
 setTimeout(() => {
 	reject(new Error('Oops!'))
 },1000)
})

Consumers: then, catch, finally

A Promise object serves as a link between the executor (the “producing code” or “singer”) and the consuming functions (the “fans”), which will receive the result or error. Consuming functions can be registered (subscribed) using methods .then, .catch and .finally.

then

promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

catch

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second

The call .catch(f) is a complete analog of .then(null, f), it’s just a shorthand.

finally

The call .finally(f) is similar to .then(f, f) in the sense that f always runs when the promise is settled: be it resolve or reject.

finally is a good handler for performing cleanup, e.g. stopping our loading indicators, as they are not needed anymore, no matter what the outcome is.

new Promise((resolve, reject) => {
  /* do something that takes time, and then call resolve/reject */
})
  // runs when the promise is settled, doesn't matter successfully or not
  .finally(() => stop loading indicator)
  // so the loading indicator is always stopped before we process the result/error
  .then(result => show result, err => show error)

But finally(f) isn’t exactly an alias of then(f,f) though. There are few subtle differences:

  1. A finally handler has no arguments. In finally we don’t know whether the promise is successful or not. That’s all right, as our task is usually to perform “general” finalizing procedures.

  2. A finally handler passes through results and errors to the next handler.

For eg:

new Promise((resolve, reject) => {
  setTimeout(() => resolve("result"), 2000)
})
  .finally(() => alert("Promise ready"))
  .then(result => alert(result)); // <-- .then handles the result

Similarly for error

new Promise((resolve, reject) => {
  throw new Error("error");
})
  .finally(() => alert("Promise ready"))
  .catch(err => alert(err));  // <-- .catch handles the error object

Example

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);
  });
}

Usage:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
  script => alert(`${script.src} is loaded!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('Another handler...'));

Differences between promises and callback

Promises Callback
Promises allow us to do things in the natural order. First, we run loadScript(script), and .then we write what to do with the result. We must have a callback function at our disposal when calling loadScript(script, callback). In other words, we must know what to do with the result before loadScript is called.
We can call .then on a Promise as many times as we want. Each time, we’re adding a new “fan”, a new subscribing function, to the “subscription list” There can be only one callback.

Promises chaining

Difference between Promise and async/await

Link

Declaration

let arr = new Array();
let arr = [];

Get last elements with “at”

arr.at(i):

  1. is exactly the same as arr[i], if i >= 0.
  2. for negative values of i, it steps back from the end of the array.

Methods pop/push, shift/unshift

Queue operations

  1. arr.push()
  2. arr.shift()

Stack operations

  1. arr.push()
  2. arr.pop()

Methods that work with the end of the array

  1. pop
  2. push

Methods that work with the beginning of the array

  1. shift
  2. unshift

toString

Gives comma separated values as string

let arr = [1, 2, 3];

alert( arr ); // 1,2,3

alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"
  • Arrays do not have Symbol.toPrimitive, neither a viable valueOf, they implement only toString conversion, so here [] becomes an empty string, [1] becomes "1" and [1,2] becomes "1,2".

Array methods

Add/remove items

We already know methods that add and remove items from the beginning or the end:

arr.push(...items) – adds items to the end, arr.pop() – extracts an item from the end, arr.shift() – extracts an item from the beginning, arr.unshift(...items) – adds items to the beginning.

splice

  • Modifies array in-place
  • Returns deleted elements

arr.splice(start[, deleteCount, elem1, ..., elemN])

  • Allows negative elements as well
  • If element are to be inserted in the array, they are inserted before start index
let arr = [1, 2, 5];

// from index -1 (one step from the end)
// delete 0 elements,
// then insert 3 and 4
arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

slice

arr.slice([start], [end])

  • It returns a new array copying to it all items from index start to end (not including end). Both start and end can be negative, in that case position from array end is assumed.

concat

arr.concat(arg1, arg2...)

  • The method arr.concat creates a new array that includes values from other arrays and additional items.
  • It accepts any number of arguments – either arrays or values.
  • The result is a new array containing items from arr, then arg1, arg2 etc.
  • If an argument argN is an array, then all its elements are copied. Otherwise, the argument itself is copied.

Iterate: forEach

The arr.forEach method allows to run a function for every element of the array.

arr.forEach(function(item, index, array) { // ... do something with item });

Searching in array

  1. arr.indexOf arr.indexOf(item, from)
  2. arr.lastIndexOf arr.lastIndexOf(item, from)
  3. arr.includes arr.includes(item, from)
  4. arr.find(fn) arr.findIndex(fn) Imagine we have an array of objects. How do we find an object with the specific condition? and findIndex
let result = arr.find(function(item, index, array) {
  // if true is returned, item is returned and iteration is stopped
  // for falsy scenario returns undefined
});

The arr.findIndex method is essentially the same, but it returns the index where the element was found instead of the element itself and -1 is returned when nothing is found.

  1. filter let results = arr.filter(function(item, index, array) { // if true item is pushed to results and the iteration continues // returns empty array if nothing found });

Transform an array

  1. map
let result = arr.map(function(item, index, array) {
  // returns the new value instead of item
});
  1. sort

    -The call to arr.sort() sorts the array in place, changing its element order. -The items are sorted as strings by default. -Literally, all elements are converted to strings for comparisons. For strings, lexicographic ordering is applied and indeed "2" > "15".

  2. reduce/reduceRight

let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]);

Map

Map is a collection of keyed data items, just like an Object. But the main difference is that Map allows keys of any type.

Methods

  1. new Map() – creates the map.
  2. map.set(key, value) – stores the value by the key.
  3. map.get(key) – returns the value by the key, undefined if key doesn’t exist in map.
  4. map.has(key) – returns true if the key exists, false otherwise.
  5. map.delete(key) – removes the value by the key.
  6. map.clear() – removes everything from the map.
  7. map.size – returns the current element count.
let map = new Map();

map.set('1', 'str1');   // a string key
map.set(1, 'num1');     // a numeric key
map.set(true, 'bool1'); // a boolean key

// remember the regular Object? it would convert keys to string
// Map keeps the type, so these two are different:
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

Chaining in map

map.set('1', 'str1')
  .set(1, 'num1')
  .set(true, 'bool1');

Iteration over Map

  1. map.keys() – returns an iterable for keys,
  2. map.values() – returns an iterable for values,
  3. map.entries() – returns an iterable for entries [key, value], it’s used by default in for..of.
let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// iterate over keys (vegetables)
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// iterate over values (amounts)
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// iterate over [key, value] entries
for (let entry of recipeMap) { // the same as of recipeMap.entries()
  alert(entry); // cucumber,500 (and so on)
}
  • The iteration goes in the same order as the values were inserted. Map preserves this order, unlike a regular Object.

Map from Object: Object.entries

let obj = {
  name: "John",
  age: 30
};

let map = new Map(Object.entries(obj));

Object from Map: Object.fromEntries

let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

let obj = Object.fromEntries(map.entries());

This also works, map without entries

let obj = Object.fromEntries(map); // omit .entries()

Set

A Set is a special type collection – “set of values” (without keys), where each value may occur only once.

Methods

  1. new Set(iterable) – creates the set, and if an iterable object is provided (usually an array), copies values from it into the set.
  2. set.add(value) – adds a value, returns the set itself.
  3. set.delete(value) – removes the value, returns true if value existed at the moment of the call, otherwise false.
  4. set.has(value) – returns true if the value exists in the set, otherwise false.
  5. set.clear() – removes everything from the set.
  6. set.size – is the elements count.

Iteration over Set

let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) 
  alert(value);

// the same with forEach:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

Weak Map

  • If we use an object as the key in a regular Map, then while the Map exists, that object exists as well. It occupies memory and may not be garbage collected.
  • WeakMap is fundamentally different in this aspect. It doesn’t prevent garbage-collection of key objects.
let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // overwrite the reference

// john is stored inside the map,
// we can get it by using map.keys()
  • The first difference between Map and WeakMap is that keys must be objects, not primitive values:
let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // works fine (object key)

// can't use a string as the key
weakMap.set("test", "Whoops"); // Error, because "test" is not an object

Methods

  1. weakMap.get(key)
  2. weakMap.set(key, value)
  3. weakMap.delete(key)
  4. weakMap.has(key)

Iteration

  • WeakMap does not support iteration and methods keys(), values(), entries(), so there’s no way to get all keys or values from it.
  • If an object has lost all other references (like john in the code above), then it is to be garbage-collected automatically. But technically it’s not exactly specified when the cleanup happens.

Usecase

  1. The main area of application for WeakMap is an additional data storage.
    • If we’re working with an object that “belongs” to another code, maybe even a third-party library, and would like to store some data associated with it, that should only exist while the object is alive – then WeakMap is exactly what’s needed.
  2. Caching
    • We can store (“cache") results from a function, so that future calls on the same object can reuse it.
// 📁 cache.js
let cache = new Map();

// calculate and remember the result
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculations of the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// Now we use process() in another file:

// 📁 main.js
let obj = {/* let's say we have an object */};

let result1 = process(obj); // calculated

// ...later, from another place of the code...
let result2 = process(obj); // remembered result taken from cache

// ...later, when the object is not needed any more:
obj = null;

alert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!)

For multiple calls of process(obj) with the same object, it only calculates the result the first time, and then just takes it from cache. The downside is that we need to clean cache when the object is not needed any more.
Replace Map with WeakMap in the code above.

// 📁 cache.js
let cache = new WeakMap();

// calculate and remember the result
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculate the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 📁 main.js
let obj = {/* some object */};

let result1 = process(obj);
let result2 = process(obj);

// ...later, when the object is not needed any more:
obj = null;

// Can't get cache.size, as it's a WeakMap,
// but it's 0 or soon be 0
// When obj gets garbage collected, cached data will be removed as well

WeakSet

  • WeakSet behaves similarly to Weakmap
  • It is analogous to Set, but we may only add objects to WeakSet (not primitives).
  • An object exists in the set while it is reachable from somewhere else.
  • Like Set, it supports add, has and delete, but not size, keys() and no iterations.

Destructuring assignment

Destructuring assignment is a special syntax that allows us to “unpack” arrays or objects into a bunch of variables, as sometimes that’s more convenient.
Destructuring also works great with complex functions that have a lot of parameters, default values, and so on. Soon we’ll see that.

Array destructuring

// we have an array with the name and surname
let arr = ["John", "Smith"]

// destructuring assignment
// sets firstName = arr[0]
// and surname = arr[1]
let [firstName, surname] = arr;

alert(firstName); // John
alert(surname);  // Smith

Works with any iterable on the right-side

let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);

...rest syntax

  • Usually, if the array is longer than the list at the left, the “extra” items are omitted.
// default values
let [name = "Guest", surname = "Anonymous"] = ["Julius"];

alert(name);    // Julius (from array)
alert(surname); // Anonymous (default used)

Object destructuring

let {var1, var2} = {var1:…, var2:…}

// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;

{“what : goes where”} = obj Default Value

let {width = 100, height = 200, title} = options;

Combine : and default values

let {width: w = 100, height: h = 200, title} = options;

...rest syntax

let options = {
  title: "Menu",
  height: 200,
  width: 100
};

// title = property named title
// rest = object with the rest of properties
let {title, ...rest} = options;

// now title="Menu", rest={height: 200, width: 100}
alert(rest.height);  // 200
alert(rest.width);   // 100

Nested destructuring

let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true
};

// destructuring assignment split in multiple lines for clarity
let {
  size: { // put size here
    width,
    height
  },
  items: [item1, item2], // assign items here
  title = "Menu" // not present in the object (default value is used)
} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
alert(item1);  // Cake
alert(item2);  // Donut

Smart function parameters

// we pass object to function
let options = {
  title: "My menu",
  items: ["Item1", "Item2"]
};

// ...and it immediately expands it to variables
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
  // title, items – taken from options,
  // width, height – defaults used
  alert( `${title} ${width} ${height}` ); // My Menu 200 100
  alert( items ); // Item1, Item2
}

showMenu(options);

Syntax function({ incomingProperty: varName = defaultValue ... })

Difference between regular functions and arrow functions

  1. Arrow functions don't have their own bindings to this, arguments or super, and should not be used as methods.
  2. Arrow functions can’t run with new - Not having this naturally means another limitation: arrow functions can’t be used as constructors. They can’t be called with new.
  3. Arrow functions don't have access to the new.target keyword.
  4. Arrow functions cannot be used as constructors.
  5. Arrow functions cannot use yield, within its body.

Arrow functions have no “this”

let group = {
  title: "Our Group",
  students: ["John", "Pete", "Alice"],

  showList() {
    this.students.forEach(
      student => alert(this.title + ': ' + student) // No error: Work as expected
    );
  }
};

group.showList();
  • Not having this naturally means another limitation: arrow functions can’t be used as constructors. They can’t be called with new.

Arrow functions VS bind

There’s a subtle difference between an arrow function => and a regular function called with .bind(this):

  1. .bind(this) creates a “bound version” of the function.
  2. The arrow => doesn’t create any binding. The function simply doesn’t have this. The lookup of this is made exactly the same way as a regular variable search: in the outer lexical environment.

Arrows have no “arguments”

  • That’s great for decorators, when we need to forward a call with the current this and arguments.
  • For instance, defer(f, ms) gets a function and returns a wrapper around it that delays the call by ms milliseconds:
function defer(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(who) {
  alert('Hello, ' + who);
}

let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred("John"); // Hello, John after 2 seconds

Currying is the concept of breaking a function(that expects multiple arguments) that takes multiple argument into a series of functions that takes one argument. Source

Applications *Source

  • The curried function will go through the appliation and different other parts of the application will sprinkle their ingredients into it and eventually we will get our final output.
  • A curriable function takes an argument and adds this dependency to function untill all the dependencies have been fulfilled.
  • Currying can be used to implement async job. The curried function can be kept and passed around while other I/O operations are being carried out.

Problems

Futures

Encapsulates the IO operation and allows to obtain the result later.

Decorators and forwarding, call/apply

Transparent caching

If the function is called often, we may want to cache (remember) the results to avoid spending extra-time on recalculations.

function cachingDecorator(fn){
  let m = new Map()
  
  return function(x){
    if(m.has(x)){
      return m.get(x)
    } else {
      let result = fn(x)
      m.set(x, result)
      return result
    }
  }
}

In the code above cachingDecorator is a decorator: a special function that takes another function and alters its behavior.

Benefits

  1. The cachingDecorator is reusable. We can apply it to another function.
  2. The caching logic is separate, it did not increase the complexity of slow itself (if there was any).
  3. We can combine multiple decorators if needed (other decorators will follow).

Using “func.call” for the context

The caching decorator mentioned above is not suited to work with object methods.

// we'll make worker.slow caching
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // scary CPU-heavy task here
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// same code as before
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // the original method works

worker.slow = cachingDecorator(worker.slow); // now make it caching

alert( worker.slow(2) ); 

func.call(context, …args)

Syntax

func.call(context, arg1, arg2, ...) Example

function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// use call to pass different objects as "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

cachingDecorator with call

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this,x); // (**)
    cache.set(x, result);
    return result;
  };
}

func.apply(context, args)

Syntax: func.apply(context, args)

  • It runs the func setting this=context and using an array-like object args as the list of arguments.

  • Passing all arguments along with the context to another function is called call forwarding.

function wrapper(func){
  return func.apply(this,arguments)
}

Borrowing a method

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);
  • We take (borrow) a join method from a regular array ([].join) and use [].join.call to run it in the context of arguments.

Debounce

  • Debounce is like a secretary that accepts “phone calls”, and waits until there’s ms milliseconds of being quiet. And only then it transfers the latest call information to “the boss” (calls the actual f).

Image

Implementation

  1. Set a timer in the outer context and clear it out whenever the call is made to fn.
  2. call the fn.
function debounce(fn, delay){
      	let timer 

	return function(){
        clearTimeout(timer)

      timer = setTimeout(() => {
          return fn.apply(this, arguments) 
      }, delay)
    }
	
}

Throttle

throttle(f, ms)

  • When it’s called multiple times, it passes the call to f at maximum once per ms milliseconds.

  • The difference with debounce is that it’s completely different decorator:

    • debounce runs the function once after the “cooldown” period. Good for processing the final result.
    • throttle runs it not more often than given ms time. Good for regular updates that shouldn’t be very often.

Implementation

  1. Store 3 states in outer function
    1. isThrottled flag
    2. savedThis
    3. savedArgs
  2. When the wrapper is called initially, call the fn with current arguments and this. Set the flag isThrottled flag as true. setTimeout is send a callback to be triggered after ms milliseconds.
  3. If the wrapper is called again within ms millisecond, the this and arguments are saved in savedThis and savedArgs respectively and the wrapper returns.
  4. The callback sent to setTimeout is now triggered. If there are any savedArgs, call the wrapper and set the flag isthrottled to false.
function throttle(fn, ms){
	let isThrottled = false,
    savedThis, savedArgs
    
	return function wrapper(){
    	if(isThrottled){
       		savedThis= this
          	savedArgs = arguments
          	return
        }
      	fn.apply(this, arguments)
      	setTimeout(() => {
          isThrottled = false
          if(savedArgs){
           wrapper.apply(savedThis, savedArgs)
           savedThis = savedArgs = null
          }
        },ms)
    }
}

function binding

Losing this

let user = {
 name: 'shivam',
 sayHello: function(){
   console.log(`Hello ${this.name`);
 }
}

setTimeout(user.sayHello, 0) // Hello undefined

Solution 1 : wrapper

let user = {
 name: 'shivam',
 sayHello: function(){
   console.log(`Hello ${this.name`);
 }
}

setTimeout(() => user.sayHello(), 0) // Hello undefined

But there is a pitfall here. user can modify before the setTimeout triggers.

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// ...the value of user changes within 1 second
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

// Another user in setTimeout!

Solution 2 : bind

Syntax:
let boundFunc = func.bind(context);

The result of func.bind(context) is a special function-like “exotic object”, that is callable as function and transparently passes the call to func setting this=context.

Example 1

const user = {
  name: 'shivam'
}

function sayHello(phrase){
  console.log(`${phrase} ${this.name}`)
}

const wrapper = sayHello.bind(user)

console.log(wrapper.sayHello('Hello'))// Hello shivam

Example 2

let user = {
  name: 'shivam',
  sayHello: function(){
    console.log(`Hello ${this.name}`)
  }
}

let sayHi = user.sayHello.bind(user)

console.log(sayHi())// Hello shivam

setTimeout(sayHi,100) // Hello shivam

// even if the value of user changes within 1 second
// sayHi uses the pre-bound value which is reference to the old user object
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};
  • In the line (*) we take the method user.sayHi and bind it to user. The sayHi is a “bound” function, that can be called alone or passed to setTimeout – doesn’t matter, the context will be right.

Partial function

We can bind not only this, but also arguments. That’s rarely done, but sometimes can be handy.

Syntax let bound = func.bind(context, [arg1], [arg2], ...);

Here the mul function is created with another context

function mul(a, b) {
  return a * b;
}

let double = mul.bind(null, 2);

alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

implementation

function partial(fn, ...args){
  return function(...argvar){
  	return fn.call(this,...args, ...argvar)
  }
}

Excercise

  1. Second bind

Error handling, "try...catch"

Flow

Image

try...catch only works for runtime errors

try {
  {{{{{{{{{{{{
} catch (err) {
  alert("The engine can't understand this code, it's invalid");
}

try...catch works synchronously

try {
  setTimeout(function() {
    noSuchVariable; // script will die here
  }, 1000);
} catch (err) {
  alert( "won't work" );
}

Instead put the catch in callback itself

setTimeout(function() {
  try {
    noSuchVariable; // try...catch handles the error!
  } catch {
    alert( "error is caught here!" );
  }
}, 1000);

Error object

try {
  // ...
} catch (err) { // <-- the "error object", could use another word instead of err
  // ...
}

For all built-in errors, the error object has two main properties:

  1. name Error name. For instance, for an undefined variable that’s "ReferenceError".

  2. message Textual message about error details.

  3. stack (non-standard property) Current call stack: a string with information about the sequence of nested calls that led to the error. Used for debugging purposes.

try {
  lalala; // error, variable is not defined!
} catch (err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)

  // Can also show an error as a whole
  // The error is converted to string as "name: message"
  alert(err); // ReferenceError: lalala is not defined
}

Throwing our own errors

“Throw” operator

throw <error object>

Technically, we can use anything as an error object. That may be even a primitive, like a number or a string, but it’s better to use objects, preferably with name and message properties (to stay somewhat compatible with built-in errors).

JavaScript has many built-in constructors for standard errors: Error, SyntaxError, ReferenceError, TypeError and others. We can use them to create error objects as well.

For built-in errors (not for any objects, just for errors), the name property is exactly the name of the constructor. And message is taken from the argument.

Rethrowing

let json = '{ "age": 30 }'; // incomplete data

try {
  user = JSON.parse(json); // <-- forgot to put "let" before user

  // ...
} catch (err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (no JSON Error actually)
}

In our case, try...catch is placed to catch “incorrect data” errors. But by its nature, catch gets all errors from try. Here it gets an unexpected error, but still shows the same "JSON Error" message. That’s wrong and also makes the code more difficult to debug.

To avoid such problems, we can employ the “rethrowing” technique. The rule is simple:

Catch should only process errors that it knows and “rethrow” all others.

The “rethrowing” technique can be explained in more detail as:

  1. Catch gets all errors.
  2. In the catch (err) {...} block we analyze the error object err.
  3. If we don’t know how to handle it, we do throw err.

We check the type of error by instanceof

try {
  user = { /*...*/ };
} catch (err) {
  if (err instanceof ReferenceError) {
    alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable
  }
}

In the code below, we use rethrowing so that catch only handles SyntaxError:

let json = '{ "age": 30 }'; // incomplete data
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name");
  }

  blabla(); // unexpected error

  alert( user.name );

} catch (err) {

  if (err instanceof SyntaxError) {
    alert( "JSON Error: " + err.message );
  } else {
    throw err; // rethrow (*)
  }

}

try…catch…finally

If finally exists, it runs in all cases:

  1. after try, if there were no errors,
  2. after catch, if there were errors.
try {
   ... try to execute the code ...
} catch (err) {
   ... handle errors ...
} finally {
   ... execute always ...
}

finally and return

The finally clause works for any exit from try...catch. That includes an explicit return.

In the example below, there’s a return in try. In this case, finally is executed just before the control returns to the outer code.

function func() {

  try {
    return 1;

  } catch (err) {
    /* ... */
  } finally {
    alert( 'finally' );
  }
}

alert( func() ); // first works alert from finally, and then this one

try...finally

The try...finally construct, without catch clause, is also useful. We apply it when we don’t want to handle errors here (let them fall through), but want to be sure that processes that we started are finalized.

function func() {
  // start doing something that needs completion (like measurements)
  try {
    // ...
  } finally {
    // complete that thing even if all dies
  }
}

Global catch

window.onerror = function(message, url, line, col, error) {
  // ...
};
<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };

  function readData() {
    badFunc(); // Whoops, something went wrong!
  }

  readData();
</script>

The role of the global handler window.onerror is usually not to recover the script execution – that’s probably impossible in case of programming errors, but to send the error message to developers.

Attaching events to a DOM element

There are 3 ways in which event(s) can be attached to a DOM element.

inline HTML

<button onclick="bgChange()">Press me</button>

Not a good idea to mix up JS and HTML

DOM Query selector

	const buttons = document.querySelectorAll('button');
	for (let i = 0; i < buttons.length; i++) {
		buttons[i].onclick = bgChange;
	}

addEventListener

myElement.addEventListener('click', functionA);
  • Can attach multiple event listenders
  • Remove event listeners

source

In web page there is a main thread. Rendering, JS engine and other things are handled by main thread which gives a deterministic order.

https://drive.google.com/file/d/1KegEy31QBOQDBMb9_PPcBDdiwV-uSBsW/view

Definition

  • Functional programming is a programming paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm (a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow.) in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.
  • Functional programming means using functions to the best effect for creating clean and maintainable software.

Concepts

Pure functions

  • Pure functions (or expressions) have no side effects (memory or I/O).
  • If the result of a pure expression is not used, it can be removed without affecting other expressions.
  • If a pure function is called with arguments that cause no side-effects, the result is constant with respect to that argument list (sometimes called referential transparency or idempotence), i.e., calling the pure function again with the same arguments returns the same result. (This can enable caching optimizations such as memoization.)
  • If there is no data dependency between two pure expressions, their order can be reversed, or they can be performed in parallel and they cannot interfere with one another (in other terms, the evaluation of any pure expression is thread-safe).
  • If the entire language does not allow side-effects, then any evaluation strategy can be used; this gives the compiler freedom to reorder or combine the evaluation of expressions in a program (for example, using deforestation).

Immutability

  • Another tenet of functional programming philosophy is not to modify data outside the function. In practice, this means to avoid modifying the input arguments to a function. Instead, the return value of the function should reflect the work done. This is a way of avoiding side effects. It makes it easier to reason about the effects of the function as it operates within the larger system.

First class functions

  • A first class function is a function that is treated as a “thing in itself,” capable of standing alone and being treated independently. Functional programming seeks to take advantage of language support in using functions as variables, arguments, and return values to create elegant code.

Higher-order functions

  • A function that accepts a function as an argument, or returns a function, is known as a higher-order function — a function that operates upon a function.

Curried functions

sources:

  1. Infoworld
  2. Wiki

Recursion

Recursion is a programming pattern that is useful in situations when a task can be naturally split into several tasks of the same kind, but simpler. Or when a task can be simplified into an easy action plus a simpler variant of the same task. Or, as we’ll see soon, to deal with certain data structures.

The execution context and stack

  • The information about the process of execution of a running function is stored in its execution context.
  • The execution context is an internal data structure that contains details about the execution of a function: where the control flow is now, the current variables, the value of this (we don’t use it here) and few other internal details.
  • One function call has exactly one execution context associated with it.

When a function makes a nested call, the following happens:

  1. The current function is paused.
  2. The execution context associated with it is remembered in a special data structure called execution context stack.
  3. The nested call executes.
  4. After it ends, the old execution context is retrieved from the stack, and the outer function is resumed from where it stopped.

Rest and spread syntax

function sumAll(...args) { // args is the name for the array
  let sum = 0;

  for (let arg of args) sum += arg;

  return sum;
}

The “arguments” variable

In old times, rest parameters did not exist in the language, and using arguments was the only way to get all arguments of the function. And it still works, we can find it in the old code.

But the downside is that although arguments is both array-like and iterable, it’s not an array. It does not support array methods, so we can’t call arguments.map(...) for example.

  • Arrow functions do not have "arguments"

If we access the arguments object from an arrow function, it takes them from the outer “normal” function.

Here’s an example:

function f() {
  let showArg = () => alert(arguments[0]);
  showArg();
}

f(1); // 1

Spread syntax

let arr = [3, 5, 1];

alert( Math.max(arr) ); // NaN

alert( Math.max(...arr) ); // 5

There’s a subtle difference between Array.from(obj) and [...obj]:

  1. Array.from operates on both array-likes and iterables.
  2. The spread syntax works only with iterables.

Scopes

Code blocks

{
  // do some job with local variables that should not be seen outside

  let message = "Hello"; // only visible in this block

  alert(message); // Hello
}

alert(message); // Error: message is not defined

Nested functions

A function is called “nested” when it is created inside another function.

Eg:

function sayHiBye(firstName, lastName) {

  // helper nested function to use below
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

Lexical Environment

In JavaScript, every running

  1. function,
  2. code block {...}, and
  3. the script as a whole have an internal (hidden) associated object known as the Lexical Environment.

The Lexical Environment object consists of two parts:

  1. Environment Record – an object that stores all local variables as its properties (and some other information like the value of this).
  2. A reference to the outer lexical environment, the one associated with the outer code.

Step 1. Variables

A “variable” is just a property of the special internal object, Environment Record. “To get or change a variable” means “to get or change a property of that object”.

Image

Image

Rectangles on the right-hand side demonstrate how the global Lexical Environment changes during the execution:

  1. When the script starts, the Lexical Environment is pre-populated with all declared variables.
  2. Initially, they are in the “Uninitialized” state. That’s a special internal state, it means that the engine knows about the variable, but it cannot be referenced until it has been declared with let. It’s almost the same as if the variable didn’t exist.
  3. Then let phrase definition appears. There’s no assignment yet, so its value is undefined. We can use the variable from this point forward.
  4. phrase is assigned a value.
  5. phrase changes the value.

Step 2. Function Declarations

A function is also a value, like a variable.

The difference is that a Function Declaration is instantly fully initialized. When a Lexical Environment is created, a Function Declaration immediately becomes a ready-to-use function (unlike let, that is unusable till the declaration).

Naturally, this behavior only applies to Function Declarations, not Function Expressions where we assign a function to a variable, such as

let say = function(name)....`

Step 3. Inner and outer Lexical Environment

Image

During the function call we have two Lexical Environments: the inner one (for the function call) and the outer one (global):

  1. The inner Lexical Environment corresponds to the current execution of say. It has a single property: name, the function argument. We called say("John"), so the value of the name is "John".
  2. The outer Lexical Environment is the global Lexical Environment. It has the phrase variable and the function itself.

When the code wants to access a variable – the inner Lexical Environment is searched first, then the outer one, then the more outer one and so on until the global one.

In this example the search proceeds as follows:

For the name variable, the alert inside say finds it immediately in the inner Lexical Environment. When it wants to access phrase, then there is no phrase locally, so it follows the reference to the outer Lexical Environment and finds it there.

Image

Step 4. Returning a function

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

Image

What’s different is that, during the execution of makeCounter(), a tiny nested function is created of only one line: return count++. We don’t run it yet, only create.

All functions remember the Lexical Environment in which they were made. Technically, there’s no magic here: all functions have the hidden property named [[Environment]], that keeps the reference to the Lexical Environment where the function was created:

When on an interview, a frontend developer gets a question about “what’s a closure?”, a valid answer would be a definition of the closure and an explanation that all functions in JavaScript are closures, and maybe a few more words about technical details: the [[Environment]] property and how Lexical Environments work.


![Image](https://drive.google.com/uc?id=1whQqAj2R_qc93GAoB884Lw5N6X-TzObQ)

So, counter.[[Environment]] has the reference to {count: 0} Lexical Environment. That’s how the function remembers where it was created, no matter where it’s called. The [[Environment]] reference is set once and forever at function creation time.

Later, when counter() is called, a new Lexical Environment is created for the call, and its outer Lexical Environment reference is taken from counter.[[Environment]]:

Image

Must solve question

  1. Block level scope function
  2. let scope pitfall
  3. Make army array function

The old var

“var” has no block scope

Example 1:

if (true) {
  var test = true; // use "var" instead of "let"
}

alert(test); // true, the variable lives after if

let will throw an error

if (true) {
  let test = true; // use "let"
}

alert(test); // ReferenceError: test is not defined
for (var i = 0; i < 10; i++) {
  var one = 1;
  // ...
}

alert(i);   // 10, "i" is visible after loop, it's a global variable
alert(one); // 1, "one" is visible after loop, it's a global variable

“var” tolerates redeclarations

...but let doesn't

var user = "Pete";

var user = "John"; // this "var" does nothing (already declared)
// ...it doesn't trigger an error

alert(user); // John

“var” variables can be declared below their use (HOISTING)

function sayHi() {
  phrase = "Hello";

  alert(phrase);

  var phrase;
}
sayHi();

Declarations are hoisted, but assignments are not.

IIFE

In the past, as there was only var, and it has no block-level visibility, programmers invented a way to emulate it. What they did was called “immediately-invoked function expressions” (abbreviated as IIFE).

(function() {

  var message = "Hello";

  alert(message); // Hello

})();

So, the parentheses around the function is a trick to show JavaScript that the function is created in the context of another expression, and hence it’s a Function Expression: it needs no name and can be called immediately.

Function object & Named function expression(NFE)

As we already know, a function in JavaScript is a value.

Every value in JavaScript has a type. What type is a function?

In JavaScript, functions are objects.

The “name” property

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi
let sayHi = function() {
  alert("Hi");
};

alert(sayHi.name); 
let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

The “length” property

  • returns the number of function parameters
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

Named Function Expression

Named Function Expression, or NFE, is a term for Function Expressions that have a name.

let sayHi = function(who) {
  alert(`Hello, ${who}`);
};
let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};
let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // Now all fine
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (nested call works)

Use of referencing the function itself

  1. It allows the function to reference itself internally.
  2. It is not visible outside of the function.

Excercise

  1. sum(n1)(n2).....

Property flags and descriptors

Property Flag

  1. value
  2. writable - if true, the value can be changed, otherwise it’s read-only.
  3. enumerable - if true, then listed in loops, otherwise not listed.
  4. configurable - if true, the property can be deleted and these attributes can be modified, otherwise not.

All the property flags are true by default

Getting property flags

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);

Setting property flag

Object.defineProperty(obj, property, descriptor)

If the property exists, defineProperty updates the flag, otherwise it creates the property with given flags and if flag is not provided, its assumed false.

let x = {}

Object.defineProperty(x, 'a', {
	value: 12
})

console.log(Object.getOwnPropertyDescriptor(x, 'a'))
/*
{
	value: 12,
    writable: false,
    configurable: false,
    enumerable: false
}
*/

Non-writable

  1. Option 1
let user = {
  name: 'john'
}

Object.defineProperty(user, 'name', {
 writable: false 
})

user['name'] = 'paul'
// Error in strict mode only, no error in non strict mode and value wouldn't update.
  1. Option 2
let user = {
}

Object.defineProperty(user, 'name',{
 value: 'shivam',
  enumerable: true,
  configurable: true
})

Non-enumerable

let user = {
 name: 'shivam',
  toString: function(){
   console.log(this.name) 
  }
}

Object.defineProperty(user, 'toString', {
	enumerable: false
})
for (let key in user) alert(key); // name , only
  1. Non-enumerable properties are also excluded from Object.keys:
  2. Non-enumerable properties are also excluded from for ... in

Non-configurable

The non-configurable flag (configurable:false) is sometimes preset for built-in objects and properties.
A non-configurable property can’t be deleted, its attributes (not the value) can’t be modified.

For instance, Math.PI is non-writable, non-enumerable and non-configurable:

console.log(Object.getPropertyOwnDescriptor(Math, 'PI'))
/*
{
  "value": 3.141592653589793,
  "writable": false,
  "enumerable": false,
  "configurable": false
}
*/
Math.PI = 3; // Error, because it has writable: false

// delete Math.PI won't work either

Notes:

  • Making a property non-configurable is a one-way road. We cannot change it back with defineProperty.
  • Please note: configurable: false prevents changes of property flags and its deletion, while allowing to change its value.
  • We can change writable: true to false for a non-configurable property, thus preventing its value modification (to add another layer of protection). Not the other way around though.

Object.defineProperties

Sytax Object.defineProperties(obj, { prop1: descriptor })

Object.getOwnPropertyDescriptors

  • Gets all property descriptors at once.
  • Can be used to clone object with descriptors.
let clone = Object.defineproperties({}, Object.getOwnPropertyDescriptors(objToClone))

Normally when we clone an object, we use an assignment to copy properties, like this:

for (let key in user) {
  clone[key] = user[key]
}

…But that does not copy flags. So if we want a “better” clone then Object.defineProperties is preferred.

Another difference is that for..in ignores symbolic and non-enumerable properties, but Object.getOwnPropertyDescriptors returns all property descriptors including symbolic and non-enumerable ones.

Sealing an object globally

  1. Object.preventExtensions(obj) - Forbids the addition of new properties to the object.
  2. Object.seal(obj) - Forbids adding/removing of properties. Sets configurable: false for all existing properties.
  3. Object.freeze(obj) - Forbids adding/removing/changing of properties. Sets configurable: false, writable: false for all existing properties.
  4. Object.isExtensible(obj) - Returns false if adding properties is forbidden, otherwise true.
  5. Object.isSealed(obj) - Returns true if adding/removing properties is forbidden, and all existing properties have configurable: false.
  6. Object.isFrozen(obj) - Returns true if adding/removing/changing properties is forbidden, and all current properties are configurable: false, writable: false

Property getters and setters

` obj = { get propName(){ //code to execute on obj.propname }

set propName(){
// code to execute when setting obj.propname = value
}

} `

Example

If we have an obj with property firstName and lastName, we can create a getter for fullName.

let user = {
  firstname : "john",
  lastname: "mayers",
  get fullname(){
   return `${this.firstname} ${this.lastname}` 
  }
}

Assigning value to a getter will throw an error

let user = {
  get fullName() {
    return `...`;
  }
};

user.fullName = "Test"; // Error (property has only a getter)

Instead create a setter for such property

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

Accessor descriptors

Descriptors for accessor (get and set) properties are different from those for data properties.

For accessor properties, there is no value or writable, but instead there are get and set functions.

That is, an accessor descriptor may have:

  • get – a function without arguments, that works when a property is read,
  • set – a function with one argument, that is called when the property is set,
  • enumerable – same as for data properties,
  • configurable – same as for data properties.

Datatypes

Object are just a place holder in memory, represented in the form of key:value pair.

let user = new Object(); // "object constructor" syntax
let user = {};  // "object literal" syntax

Initialize

let user = {     // an object
  name: "John",  // by key "name" store value "John"
  age: 30        // by key "age" store value 30
};

Addition

user['branch'] = 'cs'

delete

delete user.branch

Computed property

let bag = {
  [fruit]: 5,
};

Property value shorthand

let bag = {
	fruit,
	size
}

Invalid property names

  • Valid : keywords can be property names
let bag = {
  return: 0,
  for: 1,
  let: 3
}
  • Invalid proto can only be an object
let bag.__proto__ = true 

Check for existing property

key in obj

Iterating across all the properties

for (let key in obj){
  console.log(obj[i])
}

Ordering of keys

for (let key in obj)
  • numeric keys are ordered according to their values
  • rest are ordered in the order of its creation

Excercise

let schedule = {};

function isEmpty(obj){
  for (let key in obj){
    return true
  } 
  return false
}
alert( isEmpty(schedule) ); // true

Object referencing and copying

Variable store a reference to the object not the actual object. Primitives store actual values.

Copying the object copies the reference of the object.

let user = {
  name: "John",
  age: 30        
};


const newUser = user // copies by reference
let a = {};
let b = a; // copy the reference

alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true
let a = {};
let b = {}; // two independent objects

alert( a == b ); 

Cloning

  1. Inbuild method Object.assign(destination,[src1, src1, ...])

  2. Excercise....

Garbage Collection

Automatically. Reachability

let user = {
  name: "John"
};
user = undefined;

The basic garbage collection algorithm is called “mark-and-sweep”.

"this"

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" is the "current object"
    alert(this.name);
  }

};

user.sayHi(); // John
let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // unreliable
  }

};

“this” is not bound

Constructor, "new" operator

function User(name) {
  // this = {};  (implicitly)

  // add properties to this
  this.name = name;
  this.isAdmin = false;

  // return this;  (implicitly)
}

When a function is executed with new, it does the following steps:

  1. A new empty object is created and assigned to this.
  2. The function body executes. Usually it modifies this, adds new properties to it.
  3. The value of this is returned.
  4. Any function (except arrow functions, as they don’t have this) can be used as a constructor.

new.target

  • That can be used inside the function to know whether it was called with new, “in constructor mode”, or without it, “in regular mode”.
  • It is undefined for regular calls and equals the function if called with new.
  • We can also make both new and regular calls to do the same, like this:
function User(name) {
  if (!new.target) { // if you run me without new
    return new User(name); // ...I will add new for you
  }

  this.name = name;
}

let john = User("John"); // redirects call to new User
alert(john.name); // John

Return from constructors

  • 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.

Optional chaining

let user = null;

alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
  • the ?. immediately stops (“short-circuits”) the evaluation if the left part doesn’t exist.

  • The optional chaining ?. is not an operator, but a special syntax construct, that also works with functions and square brackets.

  • For Eg

userAdmin.admin?.();

Symbol Type

By specification, only two primitive types may serve as object property keys:

  1. String type
  2. Symbol type
obj[true] === obj["true"]
obj[1] === obj["1"]

A “symbol” represents a unique identifier.

let id = Symbol() //or
let id = Symbol('id')

Symbols are guaranteed to be unique. Even if we create many symbols with the same description, they are different values. The description is just a label that doesn’t affect anything.

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

Symbols - Hidden properties

Symbols allow us to create “hidden” properties of an object, that no other part of code can accidentally access or overwrite.

  • Symbols are skipped by for…in
  • Object.keys(user) also ignores them.
  • In contrast, Object.assign copies both string and symbol properties:

Types

  1. Number
  2. Boolean
  3. Null
  4. undefined
  5. Big INT
  6. String
  7. Symbol

Don't use primitives constructor methods

let zero = new Number(0);

if (zero) { // zero is true, because it's an object
  alert( "zero is truthy!?!" );
}

May use primitive conversion

let num = Number("123"); // convert a string to number

Numbers

let billion = 1000000000;
let billion = 1_000_000_000;
let billion = 1e9;  // 1 billion, literally: 1 and 9 zeroes
1e3 === 1 * 1000; // e3 means *1000
1.23e6 === 1.23 * 1000000; // e6 means *1000000
alert( 7.3e9 );  // 7.3 billions (same as 7300000000 or 7_300_000_000)

Fraction

let mсs = 0.000001;
let mcs = 1e-6; // six zeroes to the left from 1
1.23e-6 === 1.23 / 1000000; // 0.00000123

Hex

let x = 0xFF 
let x = 0xFF

Octal

let x = 0o12;

Binary

let x= 0b01010

toString(base)

The method num.toString(base) returns a string representation of num in the numeral system with the given base.

Methods

x = 123..toString()
Math.round()
Math.ceil()
Math.floor()
Math.trunc()
num.toFixed(2) // 2 decimal places

the funny thing

// Hello! I'm a self-increasing number!
alert( 9999999999999999 ); // shows 10000000000000000

Two zeroes

Another funny consequence of the internal representation of numbers is the existence of two zeroes: 0 and -0.

That’s because a sign is represented by a single bit, so it can be set or not set for any number including a zero.

NaN and Infinite

isFinite(13787829)
isNaN(NaN)
  • isNaN(value) converts its argument to a number and then tests it for being NaN:

  • isFinite(value) converts its argument to a number and returns true if it’s a regular number, not NaN/Infinity/-Infinity

Compare with Object.is

Object.is(NaN, NaN) === true
Object.is(0, -0) === false

parseInt(number, base) and parseFloat()

  • Conversion to numbers
    1. Number()
    2. +value but these methods don't work with values such as '100px', '100rem',. For such cases parseInt and parseFloat is used as:
alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5

alert( parseInt('12.3') ); // 12, only the integer part is returned
alert( parseFloat('12.3.4') ); // 12.3, the second point stops the reading

Other Math functions

  1. Math.random()
  2. Math.max(a, b, c...) / Math.min(a, b, c...)
  3. Math.pow(n, power)

String Type

let single = 'single-quoted';
let double = "double-quoted";

let backticks = `backticks`;

Length of string

str.length

Accessing characters

let str = `Hello`;

// the first character
alert( str[0] ); // H
alert( str.charAt(0) );

for (let char of "Hello") {
  alert(char); // H,e,l,l,o (char becomes "H", then "e", then "l" etc)
}

Strings are immutable

Strings can’t be changed in JavaScript. It is impossible to change a character.

This doesn't work

let str = 'Hi';

str[0] = 'h'; // error
alert( str[0] ); // doesn't work

The usual workaround is to create a whole new string and assign it to str instead of the old one.

let str = 'Hi';

str = 'h' + str[1]; // replace the string

alert( str ); // hi

Methods

Changing the case

'Interface'.toUpperCase()
'Interface'.toLowerCase()
'Interface'[0].toLowerCase()// Return i

Searching for a substring

  1. indexOf =>
str.indexOf(substr, pos)

It looks for the substr in str, starting from the given position pos, and returns the position where the match was found or -1 if nothing can be found. Returns position if found else -1.

  1. lastIndexOf
str.lastIndexOf(substr, position)

str.lastIndexOf(substr, position) that searches from the end of a string to its beginning.

  1. includes

str.includes(substr, pos) returns true/false depending on whether str contains substr within.

alert( "Widget with id".includes("Widget") ); // true
  1. startsWith and endsWith
alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid"
alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get"

substring of a string

  1. str.slice(start [, end])

Returns the part of the string from start to (but not including) end.

let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', the substring from 0 to 5 (not including 5)
alert( str.slice(0, 1) ); // 's', from 0 to 1, but not including 1, so only character at 0
alert( str.slice(2) ); // 'ringify', from the 2nd position till the end
alert( str.slice(-4, -1) ); // 'gif'
  1. str.substring(start [, end]) This is almost the same as slice, but it allows start to be greater than end.
let str = "stringify";
// these are same for substring
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"
  1. str.substr(start [, length]) Returns the part of the string from start, with the given length.
let str = "stringify";
alert( str.substr(2, 4) );
let str = "stringify";
alert( str.substr(-4, 2) ); 

Comparing strings

  1. str.codePointAt(pos) Returns the code for the character at position pos:
// different case letters have different codes
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90
  1. String.fromCodePoint(code)
alert( String.fromCodePoint(90) ); // Z

Misc.

  1. str.trim()
  2. str.repeat(n)

[[Prototype]]

In JavaScript, objects have a special hidden property [[Prototype]] (as named in the specification), that is either null or references another object. That object is called “a prototype”:
Image

The property [[Prototype]] is internal and hidden, but there are many ways to set it.

One of them is to use the special name __proto__, like this:

let animal = {
  walks: true
}

let rabbit = {
 jumps: true 
}

rabbit__proto__ = animal

When we read a property from object, and it’s missing, JavaScript automatically takes it from the prototype. In programming, this is called prototypal inheritance.

The prototype chain can be longer as well

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
};

// walk is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)

Image

Limitation

  1. The references can’t go in circles. JavaScript will throw an error if we try to assign proto in a circle.
  2. The value of proto can be either an object or null. Other types are ignored.

Also it may be obvious, but still: there can be only one [[Prototype]]. An object may not inherit from two others.

Writing doesn’t use prototype

The prototype is only used for reading properties.

Write/delete operations work directly with the object.

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// setter triggers!
admin.fullName = "Alice Cooper"; // (**)

alert(admin.fullName); // Alice Cooper, state of admin modified
alert(user.fullName); // John Smith, state of user protected

The value of “this”

this is not affected by prototypes at all.

No matter where the method is found: in an object or its prototype. In a method call, this is always the object before the dot.

That is actually a super-important thing, because we may have a big object with many methods, and have objects that inherit from it. And when the inheriting objects run the inherited methods, they will modify only their own states, not the state of the big object.

// animal has methods
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};

// modifies rabbit.isSleeping
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)

for…in loop

The for..in loop iterates over inherited properties too.

For example:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// Object.keys only returns own keys
alert(Object.keys(rabbit)); // jumps

// for..in loops over both own and inherited keys
for(let prop in rabbit) alert(prop); // jumps, then eats

If you don't want to access inherited properties use obj.hasOwnProperty(key). It returns true if obj has its own (not inherited) property named key.

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    alert(`Our: ${prop}`); // Our: jumps
  } else {
    alert(`Inherited: ${prop}`); // Inherited: eats
  }
}

Image

F.prototype(function prototype)

Please note that F.prototype here means a regular property named "prototype" on F. It sounds something similar to the term “prototype”, but here we really mean a regular property with this name.

let animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

Setting Rabbit.prototype = animal literally states the following: "When a new Rabbit is created, assign its [[Prototype]] to animal".

Image

F.prototype only used at new F time

F.prototype property is only used when new F() is called, it assigns [[Prototype]] of the new object.

If, after the creation, F.prototype property changes (F.prototype = "another object"), then new objects created by new F will have another object as [[Prototype]], but already existing objects keep the old one.

Default F.prototype, constructor property

Every function has the "prototype" property even if we don’t supply it.

The default "prototype" is an object with the only property constructor that points back to the function itself.

Like this:

function Rabbit(){
  // some code
}

console.log(Rabbit.prototype.constructor === Rabbit) // true

Native prototypes

Object.prototype

let obj = {};
alert( obj ); // "[object Object]" ?

Where’s the code that generates the string "[object Object]"? That’s a built-in toString method, but where is it? The obj is empty!

…But the short notation obj = {} is the same as obj = new Object(), where Object is a built-in object constructor function, with its own prototype referencing a huge object with toString and other methods.

Here’s what’s going on:
Image

When new Object() is called (or a literal object {...} is created), the [[Prototype]] of it is set to Object.prototype

Image

This can be verified this way:

let obj = {};

alert(obj.__proto__ === Object.prototype); // true

alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true

Note: There is no more [[Prototype]] in the chain above Object.prototype.

console.log(Object.prototype.__proto__)

Other built-in prototypes

Other built-in objects such as Array, Date, Function and others also keep methods in prototypes.

For instance, when we create an array [1, 2, 3], the default new Array() constructor is used internally. So Array.prototype becomes its prototype and provides methods. That’s very memory-efficient.

By specification, all of the built-in prototypes have Object.prototype on the top. That’s why some people say that “everything inherits from objects”.

IMAGE

let a = [1,2,3,4]

console.log(a.__proto__ === Array.prototype) //true
console.log(a.__proto__.__proto__ === Object.__proto__ ) //true
console.log(a.__proto__.__proto__)// null

Primitives

If we try to access primitives' properties, temporary wrapper objects are created using built-in constructors String, Number and Boolean. They provide the methods and disappear.

Changing native prototypes

String.prototype.show = function() {
  alert(this);
};

"BOOM!".show(); // BOOM!

In modern programming, there is only one case where modifying native prototypes is approved. That’s polyfilling.

Borrowing from prototypes

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join;

alert( obj.join(',') ); // Hello,world!

Prototype methods, objects without proto

__proto__ is considered outdated and may not be supported by other browser.

Modern methods:

  1. Object.create([proto], descriptor)
  2. Object.getPrototypeOf(obj)
  3. Object.setPrototypeOf(obj, proto)
let animal = {
  walk: true
}

let rabbit = Object.create(animal)

console.log(rabbit.walk //true

Further reading

https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

Syntax:

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
clearTimeout(timerId)

setInterval

Syntax:
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
All arguments have the same meaning as setTimeout. But unlike setTimeout it runs the function not only once, but regularly after the given interval of time.

setTimeout as setInterval

let tID = setTimeout(function tick(){
  // some code
  setTimeout(tick,200)
},200)

Advantage of using this approach Nested setTimeout allows to set the delay between the executions more precisely than setInterval.

If requests are failing then the interval time can be increased

let delay = 5000;

let timerId = setTimeout(function request() {
  ...send request...

  if (request failed due to server overload) {
    // increase the interval to the next run
    delay *= 2;
  }

  timerId = setTimeout(request, delay);

}, delay);

Advantage of using setTimeout instead of setInterval for recurring tasks

  1. Code 1
let i = 1;
setInterval(function() {
  func(i++);
}, 100);
  1. Code 2
let i = 1
setTimeout(function run(){
	func(i)
  setTimeout(run, 100)
},100)

Image

The real delay between func calls for setInterval is less than in the code!

Image The nested setTimeout guarantees the fixed delay (here 100ms).

Zero delay setTimeout

There’s a special use case: setTimeout(func, 0), or just setTimeout(func).
This schedules the execution of func as soon as possible. But the scheduler will invoke it only after the currently executing script is complete.
So the function is scheduled to run “right after” the current script.

For instance, this outputs “Hello”, then immediately “World”:

setTimeout(() => {alert("World")})
alert("hello")

Server side rendering

  • The HTML, Css, JS and content loads initially.
  • Initial load time is low but the page isn't responsive(interactive) instantly since the browser is blocked executing JS.
  • Initial blank screen flicker while loading.
  • But more server requests
  • Not ideal for lot of interactive actions.
Pros Cons
Search engines can crawl for better SEOs Frequent server request
Initial load is faster An overall slow page rendering
Great for static sites Full page reload
Non rich site interactions

SOURCE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment