Execution context

In JavaScript, an execution context is a fundamental concept that plays a crucial role in the language’s runtime behavior and the way code is executed. It represents the environment in which JavaScript code is evaluated and executed. Every time a function is invoked, a new execution context is created.

Components of an Execution context

An execution context consists of three main components:

  • [/] Variable Environment:
    • ➡️ This component contains all the variables declared within the current function’s scope, including function arguments and local variables. It also includes references to variables declared in the outer (lexical) environments, forming a chain known as the scope chain.
  • [/] Lexical Environment:
    • ➡️ Similar to the Variable Environment, the Lexical Environment stores variables, but it also includes additional information related to the scope chain. This information ensures that the function can access variables from its outer (enclosing) scopes, even after the outer functions have completed execution.
  • [/] This Binding:
    • ➡️ The “this” keyword represents the current execution context’s context. The “this” value is determined based on how a function is called (i.e., the calling context). It can refer to different objects or values depending on whether the function is called as a method, constructor, or in a global context.

Types of execution context

There are three types of execution contexts:

  • [/] Global Execution Context:
    • ➡️ It represents the outermost scope in a JavaScript program and is created when the script starts executing. It contains variables declared globally and has a unique relationship with the global object (e.g., the window object in browsers).
  • [/] Function Execution Context:
    • ➡️ Whenever a function is called, a new execution context is created for that function. Each function has its own variable environment and a link to the outer lexical environment that allows access to variables from the containing (parent) function or the global scope.
  • [/] Eval Function Execution Context:
    • ➡️ The “eval()” function in JavaScript can dynamically evaluate code in the context of the calling scope. When “eval()” is used, a new execution context is created for that specific evaluation.

Future OF javaScript

How the cycle of JavaScript Update Works ?

ECMA is pretty much the low when it come to JS, and there are update to this that come in the form of ES Update, And the most popular one is the ES6 which has all there is about modern JS

  • 💡 let
  • 💡 Const
  • 💡 Promises
  • 💡 Arrow function All the morn nice things that was brought was brought in ES6.

ES6 the game changer.

Why was the ES6 such a big deal ?

It’s a big deal because prior to that, ES5 was still running for over 6 years. and even worse between ES4 - ES5 We have got 10 years of difference.

so between ES4 and ES6 there have been over 15 year of difference that has passes. that is why ES6 is such a big game changer!

ES6 has so many things it wanted to bring to JavaScript.

Yearly Updates

One of the nicest thing that ES6 did was to implement the yearly updates. rather than being updated 5 10 or 15 years late. ES6 brought the concept of yearly updates. and people uses the year to refer to the update

  • 💡 ES15 => ES6
  • 💡 ES2016=> ES7 …

This update do not update the language itself rather they update the ECMA script aka the law when it comes to JS, in other words they update the documentation, how JS should work and function and all the features.

And now it's up to the browser to implement these changes themselves, that is why a feature could be implemented and released in JS officially , but the browser haven't supported it because they haven't implemented that feature.

The TC39 Process

As of my last update in September 2021, the TC39 process, also known as the Ecma International Technical Committee 39, is the committee responsible for the standardization of the ECMAScript programming language, which forms the basis of JavaScript

  • ➡️ Stage 0 Strawman:
    • 💡 The process begins with a “strawman” proposal, where anyone in the community can submit an idea for a new JavaScript feature. This stage is informal and allows the initial idea to be discussed and refined.
  • ➡️ Stage 1 Proposal:
    • 💡 Once a proposal gains enough interest and has been reviewed by TC39 members, it can advance to Stage 1. At this point, the proposal should provide a more formal specification, use cases, and an initial API design.
  • ➡️ Stage 2 Draft:
    • 💡 In th- siage, the proposal must have a more detailed specification, including semantics and syntax. Additionally, it should have clear explanations of how the feature interacts with other parts of the language. Implementors start working on creating a preliminary implementation based on the proposal.
  • ➡️ Stage 3 Candidate:
    • 💡 By Sta- 3i the proposal should be complete, and there must be at least one reference implementation and additional implementations showing that the feature is feasible and well-designed. Feedback from these implementations is crucial to address any potential issues.
  • ➡️ Stage 4 Finished:
    • 💡 At this final stage, the proposal has been reviewed thoroughly, implemented in multiple major JavaScript engines, and any issues have been resolved. The TC39 committee then votes to formally accept the proposal. Once accepted, the feature is ready to be included in the next ECMAScript specification release.
  • ➡️ Stage 5 - ECMAScript Specification Release:
    • 💡 After the proposal successfully reaches Stage 4, the feature will be included in the upcoming ECMAScript specification release. The timing of this release depends on the committee’s schedule and the progress of other proposals.

Code compatibility

Polyfills (also spelled “polyfills”) are a concept in web development used to provide modern functionality in older web browsers that do not natively support certain features or APIs. These features and APIs are usually part of newer standards and specifications for web technologies. Polyfills allow developers to “fill in” the gaps and provide support for these features in older browsers so that web applications can work consistently across various browser versions.

How to implement a polyfill

polyfill.js

your polyfill file simply would look like this: with your own function implementation

Math.doubleG = function (number) {
  return number * 2
}
Math.max = function (numbers) {
  return numbers[0]
}

yourfile.js

and you your file you would simply link to that polyfill file and use these functions in your code. most of the time you want to use the polyfile if it is not implemented.

if (Math.doubleG == null) {
  console.log(Math.doubleG(45))
}
console.log(Math.max([43, 22, 56, 73]))
// you have to check before you use your polyfil, it might be implemented and you like a dummy are simply overriding it!.
if (Math.max == null) {
  console.log(Math.max([43, 22, 56, 73]))
}

Transpiler vs Bundler

Transpiler

A transpiler, also known as a source-to-source compiler, is a tool that takes source code written in one programming language and converts it into equivalent source code in another language. The term “transpiler” is often used interchangeably with “compiler,” but it specifically refers to compilers that target a different programming language. Transpilers are commonly used to convert modern code into older versions or to translate code between languages with similar syntax but different features or purposes.

For example,

In the context of JavaScript, a transpiler can convert code written using modern ECMAScript (ES6/ES2015 and beyond) features into an older version like ES5 for compatibility with older browsers.

The term "transpiling" comes from the combination of "transformation" and "compiling."

Popular JavaScript Transpiler: Babel

Bundler

A bundler is a tool that takes multiple separate files, typically related to a web application (e.g., JavaScript, CSS, images), and combines them into a smaller number of optimized bundles. The main purpose of a bundler is to improve the performance of web applications by reducing the number of HTTP requests needed to load a page. Bundlers often include various optimizations, such as minification, tree-shaking (removing unused code), and code splitting (dividing code into smaller bundles) to enhance the efficiency of the bundled files.

For Example

when building a modern JavaScript application with multiple modules and dependencies, a bundler like Webpack or Parcel can be used to bundle all the relevant code and assets into optimized files, ready for deployment.

Popular JavaScript Bundlers: Webpack, Parcel, Rollup

  • ➡️ Transpiler:
    • 🔥 Converts source code from one language to another (e.g., modern JavaScript to older JavaScript) or between languages with similar syntax.
  • ➡️ Bundler:
    • 🔥 Combines multiple files and dependencies into optimized bundles to improve web application performance by reducing HTTP requests and applying various code optimizations.
  • 💡 Both transpilers and bundlers are essential tools in modern web development, but they serve distinct purposes in the development and deployment workflows.

Prototype and Proto (prototyping )

#prototype#proto#prototyping In JavaScript, prototyping is a mechanism that allows objects to inherit properties and methods from other objects. It forms the basis of object-oriented programming in JavaScript.

By default, pretty much everything you create in JS is going to have the object as it's base level prototype.

Unless you explicitly create an object and set the base to nothing.(Object.Create(null))

__proto__ vs prototype

__proto__ :

  • [/] __proto__ is a property that exists on every object in JavaScript.
  • [/] It allows you to access the prototype of an object directly.
  • [/] It provides a way to traverse the prototype chain and access properties and methods defined in the prototype.
  • [/] It is considered legacy and has been superseded by the Object.getPrototypeOf() and Object.setPrototypeOf() methods
const obj = {}
console.log(obj.__proto__) // Output: {}
 
const arr = []
console.log(arr.__proto__) // Output: []

prototype:

  • [/] prototype is a property that exists on constructor functions.
  • [/] It is used to define properties and methods that will be inherited by objects created from the constructor function.
  • [/] It is not directly accessible on instances of objects; rather, it is accessed through the __proto__ property.
  • [/] When an object is created using a constructor function with the new keyword, the object’s __proto__ property is linked to the constructor function’s prototype property.
function Person(name) {
  this.name = name
}
 
Person.prototype.greet = function () {
  console.log(`Hello, my name is ${this.name}`)
}
 
const john = new Person("John")
console.log(john.__proto__ === Person.prototype) // Output: true

In this example, the Person.prototype object is used to define the greet() method, which will be inherited by objects created from the Person constructor function. Overall, __proto__ allows direct access to an object’s prototype, while prototype is used on constructor functions to define properties and methods that will be inherited by objects created from the constructor. However, it’s worth noting that direct manipulation of __proto__ is generally discouraged in favor of using the Object.getPrototypeOf() and Object.setPrototypeOf() methods for accessing and modifying prototypes.

Prototypes Explained.

In JavaScript, prototypes are a fundamental concept that allows objects to inherit properties and methods from other objects. They serve as a blueprint or template for creating new objects.

What are Prototypes?

A prototype in JavaScript is an object that is associated with another object. Every object in JavaScript has an internal prototype property that references its prototype object.

Keep Search up the tree and you shall find.

  • ➡️ When you access a property or method on an object, JavaScript first checks if the object itself has that property.
    • ➡️ If it doesn’t, it looks up the prototype chain to find the property or method in its prototype object.
      • ➡️ This process continues until the property is found or the end of the prototype chain is reached.

Tip Why Use Prototypes?

Prototypes enable object inheritance and provide a way to share properties and methods between multiple objects.

  • [?] Instead of duplicating the same properties and methods in every object, you can define them once in a prototype and have multiple objects inherit those properties and methods.
  • 💡 This promotes code reusability and helps in organizing and maintaining your codebase.

Before understanding how prototypes works we need to understand how the constructor function works

function Person(name, age) {
  this.name = name
  this.age = age
  this.printName = function () {
    console.log(this.name)
  }
}
const person = new Person("Gisa Ilunga Daniel", 24)
person.printName()

The above code roughly translates to

// though running this will generate an error , but this is roughly how the constructor function works.
function Person(name,age){
  const this = {};
  this.name = name;
  this.age = age;
  this.printName = function () {
    console.log(this.name);
  };
  return this;
 } 
What the heck is a prototype then ?

The prototype which is like the base of our person, is an empty object.

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.printName = function () {
    console.log(this.name);
  };
}
 
const person = new Person("Gisa Ilunga Daniel", 24);
person.
Creating a prototype that has no object as its base level prototype.

There is a way to get around this behavior, let’s say you want to create an object that has no base prototype or whatsoever you can use the Object.create()

Object.create(null)
// Create object with no prototype
Creating an object that has a prototype of our person object
function Person(name) {
  this.name = name
}
const person = new Person("Gisa")
const obj = Object.create(person)
console.log(obj)
Tricks of prototype
// This construction function is inheriting all the abilities of the object, because the object is it's prototype.
// whatever you prototype is you inherit everything from that prototyp
function Person(name, age) {
  this.name = name
  this.age = age
}
 
// Available through the instance of the prototype.
 
Person.prototype.printName = function () {
  console.log(this.name)
}
 
// Define things that are globaly available on the person obj
Person.sayHi = function () {
  console.log(this.name)
}
 
// We don't need an instance to access say hi, cause it's defined
// Globally
Person.sayHi()
 
const person1 = new Person("Gisa", "Ilunga")
// Access our print name that is inherited through our instance
 
person1.printName()
 
//person1.sayHi(); // We don't have an access here

Prototypes in JavaScript provide a way to share properties and methods between objects through inheritance. They allow for code reusability, making it easier to create and manage objects.

Temporal dead zone(TDZ

A variable declared with letconst, or class is said to be in a "temporal dead zone" (TDZ) from the start of the block until code execution reaches the line where the variable is declared and initialized.

Object Comparison

Usually, when you compare data types like int and strings in JavaScript, you use the equality operators (== and ===). However, comparing objects with == and === will not work.

Ways of comparing objects

Using JSON.stringify()

To fix this, one option is to stringify both objects and then use the equality operators.

const obj1 = { a: 1, b: 2 }
const obj2 = { a: 1, b: 2 }
console.log(JSON.stringify(obj1) === JSON.stringify(obj2)) // true

If the order of the properties is different, the above method will evaluate as false even though the properties are the same:

const obj1 = { a: 1, b: 2 }
const obj2 = { b: 2, a: 1 }
console.log(JSON.stringify(obj1) === JSON.stringify(obj2)) // false

To overcome this issue, we can use a JavaScript library, called lodash, instead.

Using lodash

lodash is a JavaScript library that offers _.isEqual(value1, value2), which performs a deep comparison between two values to check if they are equivalent.

import _ from "lodash"
const obj1 = { a: 1, b: 2 }
const obj2 = { b: 2, a: 1 }
console.log(_.isEqual(obj1, obj2)) // true

Using ES6

Javascript(ES6) can also be used without using any third-party library to systematically compare the two objects. The order of the properties in the two objects is different.

const obj1 = { name: "Jack", age: 25 }
const obj2 = { age: 25, name: "Jack" }
let objEqual = false
const obj1Keys = Object.keys(obj1).sort()
const obj2Keys = Object.keys(obj2).sort()
 
if (obj1Keys.length !== obj2Keys.length) {
  console.log(objEqual)
} else {
  const areEqual = obj1Keys.every((key, index) => {
    const objValue1 = obj1[key]
    const objValue2 = obj2[obj2Keys[index]]
    return objValue1 === objValue2
  })
  if (areEqual) {
    objEqual = true
    console.log(objEqual)
  } else {
    console.log(objEqual)
  }
}

The new operator/keyword

#new The new operator lets developers create an instance of a user-defined object type or of one of the built-in object types that has a constructor function.

function Car(make, model, year) {
  this.make = make
  this.model = model
  this.year = year
}
 
const car1 = new Car("Eagle", "Talon TSi", 1993)
 
console.log(car1.make)
// Expected output: "Eagle"

### Syntax of the new operator

new constructor()
new constructor()
new constructor(arg1)
new constructor(arg1, arg2)
new constructor(arg1, arg2, /* …, */ argN)

Description

The new keyword in JavaScript is used to create instances of a constructor function or a class.

It is an operator that initializes a new object and binds it to the constructor's prototype.

When you use new with a constructor function or a class, it performs the following steps:

  1. [>] Creates a blank, plain JavaScript object. For convenience, let’s call it newInstance.
  2. [>] Points newInstance’s Prototype to the constructor function’s prototype property, if the prototype is an Object. Otherwise, newInstance stays as a plain object with Object.prototype as its Prototype.
  3. [>] Binds the this keyword inside the constructor function or class methods to the newly created object.
    • 🔖 In other words: Executes the constructor function with the given arguments, binding newInstance as the this context (i.e. all references to this in the constructor function now refer to newInstance).
  4. [>] Executes the constructor function or class, initializing the properties of the new object.
  5. [>] If the constructor function or class does not return any object explicitly, it returns the newly created object.
    • 🔖 If the constructor function returns a non-primitive, this return value becomes the result of the whole new expression. Otherwise, if the constructor function doesn’t return anything or returns a primitive, newInstance is returned instead. (Normally constructors don’t return a value, but they can choose to do so to override the normal object creation process.)

Usage of the new operator

Using new with Constructor Functions:

function Person(name, age) {
  this.name = name
  this.age = age
}
 
const john = new Person("John", 30)
console.log(john.name) // Output: "John"
console.log(john.age) // Output: 30

Using new with Classes:

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}
 
const jane = new Person("Jane", 25)
console.log(jane.name) // Output: "Jane"
console.log(jane.age) // Output: 25

Important Things to Know:

  1. Constructor Functions vs. Classes: Constructor functions and classes are two different ways to define blueprints for creating objects. The new keyword can be used with both to create instances. However, classes are a more modern and recommended approach, especially when working with ES6 and above.
  2. Capitalization Conventions: It is a convention to capitalize the name of constructor functions and classes to distinguish them from regular functions and variables. This makes it clear that the function or class should be used with the new keyword to create instances.
  3. Return Value from Constructors: If a constructor function or class returns an object explicitly, the new operator will return that object instead of the newly created object. This can be useful for certain advanced use cases, but generally, it’s better to avoid returning objects from constructors.
  4. instanceof Operator: You can use the instanceof operator to check if an object is an instance of a specific constructor function or class. It returns true if the object was created using the new keyword with the specified constructor or class.
console.log(john instanceof Person)
// Output: true
console.log(jane instanceof Person)
// Output: true
  1. Object.create(null) vs. new Object(): When you use new Object(), the newly created object inherits from the default Object.prototype. However, if you use Object.create(null), the newly created object does not have any prototype and is considered a “pure” object with no default properties or methods. This can be useful in certain scenarios when you want to create objects without inheriting any default behavior.
  2. Constructor Property: Objects created with constructors or classes have a special property called constructor, which points back to the constructor function or class used to create them. This property is useful for checking the type of an object or creating new instances dynamically.
console.log(john.constructor === Person)
// Output: true
console.log(jane.constructor === Person)
// Output: true

Classes can only be instantiated with the new operator — attempting to call a class without new will throw a TypeError.

Object Oriented Programming(OOP)

#oop

  • Objects:
    • Objects are fundamental units in JavaScript and are used to represent entities, combining data (properties) and behaviors (methods) related to that entity within a single unit.

Classes are a template for creating objects.

Classes: Classes are blueprints for creating objects. They define the structure and behavior of objects of a particular type. In JavaScript, classes were introduced in ECMAScript 2015 (ES6) to provide a more organized way to create objects.

With the introduction of classes in ES6, JavaScript's OOP capabilities became more familiar to developers coming from other programming languages with classical OOP features. Developers can now use the class keyword to define classes, constructor method to initialize objects, and extends keyword for inheritance.

Creating your classes.

Classes are in fact “special functions”, and just as you can define function expressions and function declarations, a class can be defined in two ways:

  • ➡️ class expression
  • ➡️ class declaration.

### Class Declaration vs Class Expression

// Declaration
class Rectangle {
  constructor(height, width) {
    this.height = height
    this.width = width
  }
}
 
// Expression; the class is anonymous but assigned to a variable
const Rectangle = class {
  constructor(height, width) {
    this.height = height
    this.width = width
  }
}
 
// Expression; the class has its own name
const Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height
    this.width = width
  }
}

Hoisting when it comes to classes

Unlike function declarations, class declarations are not hoisted (or, in some interpretations, hoisted but with the temporal dead zone restriction), which means you cannot use a class before it is declared.

Class Terminologies

Class body

The body of a class is the part that is in curly brackets {}. This is where you define class members, such as methods or constructor.

Class elements

In object-oriented programming, a class is a blueprint for creating objects with a specific structure and behavior. When we refer to a “class element,” we are talking about the individual parts or members of a class, which can be categorized based on three aspects:

  • 🔥 Kind:

    • ➡️ The “Kind” refers to the type of member within the class. There are four main kinds of class elements:
      • [/] Getter:
        • ➡️ A getter is a method that is used to retrieve the value of a class property (also known as an attribute or field). It allows external code to access the value of a private property.
      • [/] Setter:
        • ➡️ A setter is a method that is used to set the value of a class property. It allows external code to modify the value of a private property, usually performing some validation or logic before setting the value.
      • [/] Method:
        • ➡️ A method is a function defined within the class. It represents a behavior that the class can perform. Methods can interact with the class’s properties and other methods.
      • [/] Field:
        • ➡️ A field refers to a class property or attribute. It holds data and represents the state of the object created from the class.
  • 🔥 Location:

    • ➡️ The “Location” determines whether the class element is associated with the class itself (static) or with instances of the class (instance).
      • [/] Static:
        • ➡️ Static class elements belong to the class itself, rather than to instances of the class. They are shared among all instances of the class and can be accessed using the class name. For example, a static method can be called directly on the class, not on an object.
      • [/] Instance:
        • ➡️ Instance class elements are specific to individual objects created from the class. Each instance of the class has its own set of instance members (properties and methods) that can have different values.
  • 🔥 Visibility:

    • ➡️ The “Visibility” refers to the access control of class elements, determining whether they can be accessed from outside the class or only from within the class itself.

      • [/] Public:
        • ➡️ Public class elements can be accessed from both inside and outside the class. They are accessible to other classes and external code.
      • [/] Private:
        • ➡️ Private class elements can only be accessed from within the class itself. They are not accessible from outside the class, including other classes.

The combination of these three aspects allows for fine-grained control over how the class’s members are used and interact with other parts of the code. This is an essential feature of object-oriented programming that promotes encapsulation, data hiding, and code reusability.

Class Structure

class MyClass {
  // Constructor
  constructor() {
    // Constructor body
  }
  // Instance field
  myField = "foo"
  // Instance method
  myMethod() {
    // myMethod body
  }
  // Static field
  static myStaticField = "bar"
  // Static method
  static myStaticMethod() {
    // myStaticMethod body
  }
  // Static block
  static {
    // Static initialization code
  }
  // Fields, methods, static fields, and static methods all have
  // "private" forms
  #myPrivateField = "bar"
}

Dynamics in defining your classes

To create a class you simply use the class keyword

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  printName() {
    console.log(this.name)
  }
}

creating the same object using constructor function you would do something like this

function Person(name, age) {
  this.name = name
  this.age = age
}
// Available through the
// instance of the prototype.
Person.prototype.printName = function () {
  console.log(this.name)
}

Instantiation (Constructing a class)

After a class has been declared, you can create instances of it using the new operator.

const instanceName=new ClassName(constructor..)

Static Method and Properties

A static method is a method that belongs to a class rather than an instance of a class.

The static keyword defines a static method or field for a class. Static properties (fields and methods) are defined on the class itself instead of each instance.

Static methods are often used to:

  • 🔥 create utility functions for an application
    • 💡*whereas static fields are useful for**
    • 💡 caches,
    • 💡 fixed-configuration,
    • [I] or any other data that don’t need to be replicated across instances.

To add static properties of methods in our class we have two choices:

We would use the usual method of adding them directly to our class Name as we did for our constructor function

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  printName() {
    console.log(this.name)
  }
}
 
Person.sayHi = function () {
  console.log("Tell me")
}
Person.nickName = "Anonymous"

Or you can use the static keyword, which frankly is just another beautiful way of doing things. (but it is somehow modern and therefor not supported everywhere.)

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  printName() {
    console.log(this.name)
  }
  //Create a method that is static, create a method that is on the actual class. not instance
  static printHi() {
    console.log("Tell me")
  }
  // create a property that is on the actual class not instance.
  static nickName = "Anonymous"
}

Exercise: code conversion

You have your factory function, now your job is to create a constructor function version of this object, and a class version

function createUser(email, password, language) {
  return {
    email,
    password,
    language,
    printPassword() {
      console.log(this.password)
    },
  }
}
Constructor method

The first constructor implementation would be:

function CreateUser(email, password, language) {
  this.email = email
  this.password = password
  this.language = language
  this.printPassword = function () {
    console.log(this.password)
  }
}

This would be a bad implementation because for each instance you will basically create a new function

The best implementation would be to add the method to the prototype itself

function CreateUser(email, password, language) {
  this.email = email
  this.password = password
  this.language = language
}
CreateUser.prototype.printPassword = function () {
  console.log(this.password)
}
Class method
class CreateUserClass {
  constructor(email, password, language) {
    this.email = email
    this.password = password
    this.language = language
  } // The printPassword function will be added on the prototype.
  printPassword() {
    console.log(this.password)
  }
}

Getters and Setters in our classes

Getters and setters are a powerful feature in JavaScript that allows you to control the access and manipulation of object properties. They provide a way to define special methods to get the value of a property (getter) and set the value of a property (setter) as if they were regular object properties.

This mechanism adds an extra layer of abstraction and enables you to enforce validation, perform data transformations, or hide the underlying implementation details.

Getters:

A getter is a special method in a class that is used to retrieve the value of a specific property. When you access a property using the dot notation (e.g., object.property), the getter associated with that property is automatically called, and its return value is returned as the result of the property access.

Getters are defined using the get keyword followed by an identifier, which represents the property name.

The method does not take any arguments, as it is meant to return a value:

class MyClass {
  constructor() {
    this._myProperty = 42
  }
 
  get myProperty() {
    return this._myProperty
  }
}
 
const obj = new MyClass()
console.log(obj.myProperty) // Output: 42

In this example, the myProperty getter allows you to access the value of the _myProperty property without directly accessing the property itself. It provides a level of indirection and encapsulation.

Setters:

A setter is another special method in a class used to set the value of a specific property. When you assign a value to a property using the assignment operator (e.g., object.property = value), the setter associated with that property is automatically called with the value to be set.

Setters are defined using the set keyword followed by an identifier, which represents the property name. The method takes one parameter, which represents the value to be set:

class MyClass {
  constructor() {
    this._myProperty = 42
  }
  set myProperty(value) {
    this._myProperty = value
  }
}
 
const obj = new MyClass()
obj.myProperty = 100 // Setter is called with value 100
console.log(obj._myProperty) // Output: 100

In this example, the myProperty setter allows you to set the value of the _myProperty property while providing an opportunity to perform any additional logic or validation before actually assigning the value.

Private Properties with Getters and Setters:

One common use of getters and setters is to create private properties.

In JavaScript, there is no native support for private properties, but you can achieve a similar effect by using a naming convention.

By convention, properties starting with an underscore (e.g., _myProperty) are considered private and should not be directly accessed outside the class.

class MyClass {
  constructor() {
    this._myPrivateProperty = 42
  }
 
  get myPrivateProperty() {
    return this._myPrivateProperty
  }
 
  set myPrivateProperty(value) {
    if (value >= 0) {
      this._myPrivateProperty = value
    }
  }
}
 
const obj = new MyClass()
 
obj.myPrivateProperty = 100
// Setter is called with value 100 (valid)
console.log(obj.myPrivateProperty)
// Output: 100
 
obj.myPrivateProperty = -1
// Setter is called with value -1 (invalid)
 
console.log(obj.myPrivateProperty)
// Output: 100 (value not changed due to validation)

Inheritance with classes

Inheritance is a fundamental concept in object-oriented programming (OOP) that allows one class to inherit properties and methods from another class. The class that is being inherited from is called the “parent class” or ”superclass,” and the class that inherits from it is called the “child class” or “subclass.”

In JavaScript, inheritance is achieved through the prototype chain.

Every object in JavaScript has a prototype, which is another object that serves as a fallback for property and method lookups.

When a property or method is accessed on an object, JavaScript first checks if it exists on the object itself. If not, it looks up the prototype chain to find the property or method in the prototype object.

// Parent class (superclass)
class Animal {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
 
  eat() {
    console.log(`${this.name} is eating.`)
  }
 
  sleep() {
    console.log(`${this.name} is sleeping.`)
  }
}
 
// Child class (subclass)
class Dog extends Animal {
  constructor(name, age, breed) {
    // Call the constructor of the parent class using 'super'
    super(name, age)
    this.breed = breed
  }
 
  bark() {
    console.log(`${this.name} is barking.`)
  }
}
 
// Create instances of the classes
const animal = new Animal("Generic Animal", 3)
const dog = new Dog("Buddy", 5, "Labrador")
 
animal.eat() // Output: "Generic Animal is eating."
dog.eat() // Output: "Buddy is eating."
 
dog.bark() // Output: "Buddy is barking."
  • ➡️ We have an Animal class with a constructor that sets the name and age properties and two methods, eat() and sleep().
  • ➡️ We define a Dog class that extends Animal using the extends keyword. This means Dog inherits from Animal.
  • ➡️ The Dog class has its constructor, which calls the super() method to invoke the constructor of the parent class (Animal) and sets the breed property.
  • ➡️ The Dog class also has its own method, bark().
  • ➡️ When we create an instance of Dog (dog), it has access to the properties and methods of both Dog and Animal. This is because of the prototype chain.

Method Overriding:

Child classes can override methods inherited from the parent class. When a method is called on a child instance, JavaScript first looks for that method in the child class. If not found, it searches up the prototype chain to the parent class.

class Cat extends Animal {
  constructor(name, age, color) {
    super(name, age)
    this.color = color
  }
 
  // Method overriding
  sleep() {
    console.log(`${this.name} is sleeping soundly.`)
  }
}
 
const cat = new Cat("Whiskers", 2, "Gray")
cat.sleep() // Output: "Whiskers is sleeping soundly." (method overridden)

Accessing Parent Class Methods:

Child classes can access methods from the parent class using the super keyword. This is useful when you want to extend the functionality of the parent class method in the child class.

class Cow extends Animal {
  constructor(name, age, color) {
    super(name, age)
    this.color = color
  }
 
  // Extending the functionality of the parent class method
  eat() {
    super.eat()
    // Call the eat() method of the parent class
    console.log(`${this.name} is eating grass.`)
  }
}
 
const cow = new Cow("Moo", 4, "White")
cow.eat()
// Output: "Moo is eating." "Moo is eating grass."

Prototype Chain:

Inheritance in JavaScript works by linking objects through their prototypes, creating a chain of objects that falls back to each other when properties or methods are accessed.

console.log(dog.hasOwnProperty("name"))
// Output: true
console.log(dog.hasOwnProperty("bark"))
// Output: false (inherited from prototype)
console.log(dog.hasOwnProperty("eat"))
// Output: false (inherited from prototype)
console.log(Dog.prototype.isPrototypeOf(dog))
// Output: true
console.log(Animal.prototype.isPrototypeOf(dog))
// Output: true
console.log(Object.prototype.isPrototypeOf(dog))
// Output: true

Multiple Inheritance (Mixins):

JavaScript does not support multiple inheritance directly. However, you can achieve something similar using mixins, where you can copy properties and methods from multiple sources into a single object.

// Mixin for swimming capability
const swimMixin = {
  swim() {
    console.log(`${this.name} is swimming.`)
  },
}
 
class Duck extends Animal {
  constructor(name, age) {
    super(name, age)
    Object.assign(this, swimMixin)
  }
}
 
const duck = new Duck("Quacky", 1)
duck.swim() // Output: "Quacky is swimming."

Info Note: In modern JavaScript, you can also use ES6 modules to organize and import/export classes, making it easier to manage and share code across multiple files. Additionally, JavaScript frameworks and libraries like React, Angular, and Vue.js use inheritance and component-based architecture to build scalable web applications.

Public Private Protected

In JavaScript, access modifiers are not native language features like in some other object-oriented programming languages (e.g., Java or C++). JavaScript does not have explicit keywords like public, private, or protected to control access to properties and methods. Instead, JavaScript relies on naming conventions and closure mechanisms to achieve similar effects.

Here are the common access modifier conventions used in JavaScript:

Public:

By default, all properties and methods in JavaScript classes are public. This means they can be accessed and modified from outside the class.

class Person {
  constructor(name) {
    this.name = name // Public property
  }
 
  sayHello() {
    return `Hello, my name is ${this.name}.` // Public method
  }
}
 
const person = new Person("John")
console.log(person.name)
// Output: "John"
console.log(person.sayHello())
// Output: "Hello, my name is John."

Private (Convention using underscore _):

JavaScript uses naming conventions to indicate that a property or method should be treated as private. Prefixing a property or method name with an underscore (_) is a common practice to signify that it should not be accessed or modified directly from outside the class.

class Person {
  constructor(name) {
    this._name = name
    // Private property (convention using underscore)
  }
 
  _privateMethod() {
    return "This is a private method."
    // Private method (convention using underscore)
  }
}
 
const person = new Person("Daniel")
console.log(person._name)
// Output: "Daniel" (Although it's private, it can still be accessed)
console.log(person._privateMethod()) // Output: "This is a private method." (It can still be called)

It's important to note that this is just a naming convention, and it does not actually restrict access.

Properties and methods with the underscore prefix are still technically public and can be accessed from outside the class. Developers should exercise caution and respect the convention not to access private members directly.

Protected (Convention using underscore and subclassing):

JavaScript does not have a built-in protected access modifier. However, developers often use a combination of underscore naming convention and careful subclassing to achieve protected-like behavior.

When a property or method is prefixed with an underscore and a subclass is created, the subclass can still access the underscored property or method, simulating the protected behavior.

class Animal {
  constructor(name) {
    this._name = name
    // Protected property (convention using underscore)
  }
}
 
class Dog extends Animal {
  constructor(name, breed) {
    super(name)
    this.breed = breed
  }
 
  bark() {
    return `Woof, I'm ${this._name}, 
    the ${this.breed}.`
    // Accessing the protected property
  }
}
 
const dog = new Dog("Buddy", "Labrador")
console.log(dog.bark())
// Output: "Woof, I'm Buddy, the Labrador."

Although the _name property is not officially protected, the subclass Dog can still access it because it's following the naming convention and adhering to the intended usage.

Remember that JavaScript's access modifiers are not as strict as those in some other languages. Developers should be mindful of the conventions and best practices to maintain encapsulation and prevent unintended access to sensitive data.

JavaScript Program Execution and call stack simulation.


Event loops in js

In JavaScript, the event loop is a fundamental concept that manages the execution of asynchronous operations and ensures that the program remains responsive. It is responsible for handling events, callbacks, and promises in a non-blocking manner.

The event loop ensures that synchronous code is executed in a blocking manner, while asynchronous code is executed in a non-blocking manner. This allows JavaScript to handle multiple tasks concurrently without blocking the main thread

Threads of Execution:

JavaScript is single-threaded, meaning it has only one thread of execution. A thread of execution refers to the sequence in which statements are executed in your code. JavaScript executes code line by line, one at a time, in a specific order.

Synchronous Code:

Synchronous code is code that runs sequentially, one line at a time. Each line of code is executed and completed before moving on to the next line. Synchronous operations block the execution of subsequent code until the current operation is finished.

console.log("Start")
console.log("Middle")
console.log("End")

In this code, each console.log statement will be executed in order: first ‘Start’, then ‘Middle’, and finally ‘End’. The execution of subsequent statements waits for the previous statement to finish.

Synchronous operations "block the execution of subsequent code"

This simply means that when a synchronous operation is running, it prevents the execution of any code that comes after it until it completes.

console.log("Start")
// Synchronous operation
for (let i = 0; i < 1000000000; i++) {
  // Simulating a time-consuming operation
}
console.log("End")

In this code, there is a loop that performs a time-consuming operation. While the loop is running, it consumes CPU cycles, and the subsequent console.log('End') statement is not executed until the loop finishes. The execution of the subsequent code is blocked until the synchronous operation is completed.

Asynchronous Code:

Asynchronous code allows multiple operations to be initiated, and their results are handled later without blocking the execution of subsequent code. It allows the program to continue running while waiting for long-running operations, such as network requests or file I/O, to complete.

Asynchronous code in JavaScript is typically achieved through the use of callbacks, Promises, or async/await syntax.

Callbacks: Callbacks are functions passed as arguments to other functions. They are executed at a later time when a certain event or condition occurs. Callback functions allow you to specify what should happen after an asynchronous operation completes.

function asyncOperation(callback) {
  setTimeout(() => {
    callback(null, "Async operation completed")
  }, 2000)
}
console.log("Start")
asyncOperation((error, result) => {
  if (error) {
    console.error("Error:", error)
  } else {
    console.log(result)
  }
})
console.log("End")

In this example, the asyncOperation function simulates an asynchronous operation by using setTimeout to delay the execution. It takes a callback function as an argument and calls it once the operation is complete. The console.log('Start') and console.log('End') statements are executed immediately, while the callback is called asynchronously after a 2-second delay.

Promises: Promises provide a more structured way to handle asynchronous code. A Promise represents the eventual completion or failure of an asynchronous operation and allows you to attach callbacks to handle the success or failure of that operation.

function asyncOperation() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Async operation completed")
    }, 2000)
  })
}
 
console.log("Start")
 
asyncOperation()
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error("Error:", error)
  })
console.log("End")

In this example, the asyncOperation function returns a Promise that resolves after a 2-second delay. The console.log('Start') and console.log('End') statements are executed synchronously. The then method is used to handle the resolved value of the Promise, while the catch method is used to handle any potential errors.

Async/Await: Async/await is a modern syntax introduced in ES2017 (ES8) that provides a more concise and readable way to write asynchronous code. It allows you to write asynchronous code in a more synchronous-like manner using the async and await keywords.

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}
 
async function asyncOperation() {
  await delay(2000)
  return "Async operation completed"
}
 
console.log("Start")
;(async () => {
  try {
    const result = await asyncOperation()
    console.log(result)
  } catch (error) {
    console.error("Error:", error)
  }
})()
 
console.log("End")

In this example, the asyncOperation function is an asynchronous function that uses the await keyword to pause the execution until the delay Promise resolves. The function returns a value once the delay is completed. The outer function is an immediately-invoked async function that wraps the asynchronous code. The try/catch block is used to handle any potential errors. The console.log('Start') and console.log('End') statements are executed synchronously.

The event loop ensures that synchronous code is executed in a blocking manner, while asynchronous code is executed in a non-blocking manner. This allows JavaScript to handle multiple tasks concurrently without blocking the main thread.

It's important to note that long-running synchronous operations can block the event loop and make the application unresponsive. To avoid this, time-consuming tasks should be offloaded to Web Workers or broken down into smaller chunks using techniques like asynchronous programming or Promises

Event Loop Overview

Here's an overview of how the event loop works in JavaScript:

  • 🔥 Single-threaded Nature:
    • 💡 JavaScript is single-threaded, meaning it has only one thread of execution. This thread is responsible for executing your JavaScript code sequentially.
  • 🔥 Call Stack:
    • 💡 The call stack is a data structure that keeps track of function calls in progress. When a function is called, it is pushed onto the stack, and when it completes, it is popped off the stack.
  • 🔥 Event Queue:
    • 💡 The event queue is a queue that holds events and callbacks. When an asynchronous event or callback is triggered, it is added to the event queue.
  • 🔥 Event Loop:
    • 💡 The event loop continuously checks the call stack and the event queue. If the call stack is empty, it takes the next event or callback from the event queue and pushes it onto the call stack for execution.
  • 🔥 Non-Blocking Behavior:
    • 💡 Asynchronous operations in JavaScript, such as setTimeout, AJAX requests, or Promise callbacks, are handled by the event loop. These operations are initiated, and their associated callbacks are scheduled to be executed later when the call stack is empty.
  • 🔥 Event-driven Programming - 💡 JavaScript is event-driven, meaning it responds to events like user actions, timers, and network responses. When an event occurs, the corresponding callback is added to the event queue and eventually executed by the event loop.

    The important thing to understand is that in JavaScript, regardless of whether the code is synchronous or asynchronous, it still runs on a single thread of execution. Asynchronous operations are managed by the event loop, which schedules and handles the execution of callbacks and Promises when they are ready.

JavaScript Queues

### In JavaScript, there are typically four main queues that are commonly referred to:

  1. [>] Event Queue (also known as Task Queue):
    • [/] This queue stores events and their corresponding callback functions. Events include things like DOM events (e.g., click, keypress) and other asynchronous tasks (e.g., timers, network requests). The callback functions in the event queue are processed by the Event Loop when the call stack is empty.
  2. [>] Callback Queue (also known as Message Queue):
    • [/] This queue is similar to the event queue and stores the callbacks of completed asynchronous tasks, such as timers and network requests. These callbacks are enqueued in the callback queue when their respective tasks are finished, and they are executed by the Event Loop when the call stack is empty.
  3. [>] Microtask Queue (also known as Promise Queue):
    • [/] This queue is specifically used for handling microtasks. Microtasks are tasks with a higher priority than regular tasks and are usually created by resolving Promises. Microtasks are executed before the next cycle of the event loop begins, prioritizing them over regular tasks in the callback queue.
  4. [>] Animation Frame Queue:
    • [/] This queue is used to schedule animations for smooth rendering. It allows developers to schedule functions to be executed before the browser performs the next repaint. Functions scheduled using requestAnimationFrame are enqueued in the animation frame queue.

The Event Loop plays a crucial role in managing the flow of tasks between these queues and the call stack, ensuring asynchronous operations are executed efficiently without blocking the main thread.

ES5 Web Browser APIs with callback functions

  • 💡 Problems
    • ➡️ Our response data is only available in the callback function - Callback hell
    • ➡️ Maybe it feels a little odd to think of passing a function into another function only for it to run much later
  • 💡 Benefits
    • ➡️ Super explicit once you understand how it works under-the-hood

Rules to executing Asynchronous code

When running asynchronous code in JavaScript, there are rules that dictate the order in which tasks are executed in relation to the event queue, microtask queue, and callback queue. These rules are essential for understanding how the Event Loop handles asynchronous operations. Here's a general outline of the rules:

  • [/] Synchronous Code Execution:
    • 💡 JavaScript executes synchronous code in the order it appears in the script. Functions are called and executed, and the call stack keeps track of the function calls.
  • [/] Asynchronous Code Execution:
    • 💡 When asynchronous operations (e.g., timers, network requests, user events) are encountered, they are moved out of the regular execution flow, and their callbacks are placed in the corresponding queues.
  • [/] Event Queue and Callback Queue:
    • 💡 The event queue (task queue) and the callback queue (message queue) are two separate queues. Events and their corresponding callbacks are placed in the event queue, while callbacks of completed asynchronous tasks (e.g., timers, network requests) are placed in the callback queue.
  • [/] Microtask Queue:
    • 💡 The microtask queue is separate from the event queue and callback queue. Microtasks are typically created by resolving Promises. Microtasks have higher priority than regular tasks in the event queue.
  • [/] Event Loop:
    • 💡 The Event Loop continuously checks the call stack and, when it’s empty, starts processing tasks from the queues.

The rule of thumb for task execution order is as follows:

  • 🔥 Synchronous tasks are executed in the order they appear in the script.

  • 🔥 When the call stack is empty (no synchronous tasks left to execute), the Event Loop follows these steps:

    • ➡️ a. It first checks the microtask queue. If there are any microtasks, they are all executed in the order they were added, one after the other, until the microtask queue is empty.
    • ➡️ b. After processing all microtasks, the Event Loop checks the animation frame queue. If there are any animation frame requests, it processes them in the order they were added.
    • ➡️ c. If there are no animation frame requests, the Event Loop moves on to the regular task queue (event queue) and processes the oldest task (callback) in the queue. After processing one task, the Event Loop checks the microtask queue again before moving to the next task in the event queue.
    • ➡️ d. The Event Loop repeats this process, going through the microtask queue before each task in the event queue, until both queues are empty.

This process ensures that microtasks have higher priority than regular tasks in the event queue, and it helps keep asynchronous operations responsive and efficient in JavaScript applications.

#### We have rules for the execution of our asynchronously delayed code

Hold promise-deferred functions in a microtask queue and callback function in a task queue (Callback queue) when the Web Browser Feature (API) finishes Add the function to the Call stack (i.e. run the function) when:

  • ➡️ Call stack is empty & all global code run (Have the Event Loop check this condition) Prioritize functions in the microtask queue over the Callback queue

What goes where ?

In JavaScript, different tasks are placed in specific queues based on their nature and priority. Here’s a breakdown of the tasks that go into the microtask queue, callback queue, and event queue:

  1. [i] Microtask Queue (Promise Queue):

    • ✅ Tasks placed in the microtask queue have higher priority than regular tasks in the callback queue. They are executed before regular tasks during the Event Loop.
    • [/] Things that go into the microtask queue:
      • ➡️ Promise callbacks: When a Promise is resolved (fulfilled or rejected), its respective then() or catch() callbacks are enqueued in the microtask queue. This ensures that Promise handlers are executed before regular tasks in the callback queue.
      • ➡️ Mutation Observer: Mutation Observer callbacks are enqueued in the microtask queue when DOM mutations are observed. This allows these callbacks to be executed before the next rendering cycle, ensuring that any DOM changes are handled consistently.
  2. [i] Callback Queue (Task Queue or Message Queue):

    • ✅ The callback queue holds callbacks of completed asynchronous tasks, such as timers, network requests, and user interactions.
    • [/] Things that go into the callback queue:
      • ➡️ setTimeout and setInterval: When the specified time for a timer (created using setTimeout or setInterval) elapses, the associated callback is placed in the callback queue.
      • ➡️ Network Requests: When an asynchronous network request (e.g., AJAX or fetch) is completed, its corresponding callback is enqueued in the callback queue.
      • ➡️ User Interaction Events: Events like button clicks, keyboard inputs, and other user interactions trigger their corresponding callback functions, which are placed in the callback queue.
  3. [i] Event Queue (Task Queue):

    • ✅ The event queue stores events and their corresponding callback functions. It is used for handling various asynchronous events, including DOM events and other types of events.
    • [/] Things that go into the event queue:
      • ➡️ DOM Events: When an event (e.g., click, keypress) occurs on a DOM element, the event and its associated callback (event handler) are placed in the event queue.
      • ➡️ Asynchronous Tasks: Other asynchronous tasks, such as timers and scheduled tasks, are placed in the event queue with their respective callbacks.

In summary

  • 🔥 Microtask Queue (Promise Queue): For high-priority tasks like Promise callbacks and Mutation Observer callbacks.
  • 🔥 Callback Queue (Task Queue or Message Queue): For regular asynchronous tasks like timers, network requests, and user interaction events.
  • 🔥 Event Queue (Task Queue): For various asynchronous events, including DOM events and other types of events.

What Really happens under the hood.

We have all heard about JavaScript and Node.js being single-threaded, but what does it mean in practical terms?

It means that JavaScript can do one thing at a time. For example, we cannot simultaneously multiply and sum numbers. We usually do operations in sequence. We add and then multiply or vice versa. Modern computers are fast, and the result of two or more consecutive tasks seems to be computed simultaneously, but there are exceptions.

We all have tried to scrape data from that slow website or waited more than thirty seconds before getting the result of a database query. Do we want to block our single thread from executing more tasks because of a slow database query? Luckily, Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

We have god our codes that we need to execute.

In our example, the line of code console.log('Starting Node.js') is added to the call stack and prints Starting Node.js to the console. By doing so, it reaches the end of the log function and is removed from the call stack.

The following line of code is a database query. These tasks are immediately popped off because they may take a long time. They are passed to Libuv, which asynchronously handles them in the background. At the same time, Node.js can keep running other code without blocking its single thread.

While Libuv handles the query in the background, our JavaScript is not blocked and can continue with console.log(”Before query result”).

When the query is done, its callback is pushed to the I/O Event Queue to be run shortly**.** The event loop connects the queue with the call stack. It checks if the latter is empty and moves the first queue item for execution.

The event loop, the delegation, and the asynchronous processing mechanism are Node.js's secret ingredients to process thousands of connections, read/write gigantic files, handling timers while working on other parts of our code.

Promises

Introducing the readability enhancer - Promises

What is it ?

In JavaScript, a Promise is a built-in object that represents a placeholder for a value that may not be available yet but will be resolved at some point in the future. It is used for handling asynchronous operations in a more elegant and organized way, making it easier to write and manage asynchronous code.

Promises were later standardized in ECMAScript 6 (ES6) through the Promises/A+ specification, making them a part of the core JavaScript language. They have since become a fundamental feature of modern JavaScript development, and their ease of use and clean syntax have significantly improved the way developers handle asynchronous operations.

How do they work ?

Special objects built into JavaScript that get returned immediately when we make a call to a web browser API/feature (e.g. fetch) that’s set up to return promises (not all are)

  • 💡 Promises act as a placeholder for the data we hope to get back from the web browser feature’s background work
  • 💡 We also attach the functionality we want to defer running until that background work is done (using the built in .then method)
  • 💡 Promise objects will automatically trigger that functionality to run
  • 💡 The value returned from the web browser feature’s work (e.g. the returned data from the server using fetch) will be that function’s input/argument

Iterators

We regularly have lists or collections or data where we want to go through each item and do something to each element

We have all done this.

const numbers = [4, 5, 6]
for (let i = 0; i < numbers.length; i++) {
  console.log(numbers[i])
}

We’re going to discover there’s a new beautiful way of thinking about using each element one-byone

Using a for loop to access array elements in JavaScript is not necessarily "not great," but it may not always be the most convenient or expressive way to work with arrays, especially when dealing with more complex data structures or when you want to perform specific operations on each element

Wouldn’t it be amazing if we rethink a collection of data more of a stream of element that comes toward me, that I can run a function that is simply going to instantly return an element from my stream of data ?

With Iterators we are simply rethinking our data not as a static collection that we have to go over and get element manually to which we have to run functionalities on,

Rather, having a function that simply returns a stream or flow of data in a way we want it to be.

We no longer have the intermediate step of getting the elements (manually going into the data collection and grab the next element)

Iterators were introduced in JavaScript to provide a more flexible and concise way to iterate over collections, including arrays.

Our for should be thought of as a process of, or rather a method of :

  • [/] Accessing each element
  • [/] Perform what we wan to do to each element.

    where iterators shine is that Iterators automate the accessing of each element so we can focus on what to do to each element and make it available to us in a smooth way

Imagine if we could create a function that stored numbers and each time we ran the function it would return out an element (the next one) from numbers. NOTE: It’d have to remember which element was next up somehow

But this would let us think of our array/list as a ‘stream’/flow of data with our function returning the next element from our ‘stream’ - this makes our code more readable and more functional

#### But it starts with us returning a function from another function. (Closure)

function createFunction(array) {
  let i = 0
  function inner() {
    const element = array[i]
    i++
    return element
  }
  return inner
}
const returnNextElement = createFunction([4, 5, 6])
const element1 = returnNextElement()
const element2 = returnNextElement()

## The Bond

  • [/] When the function inner is defined, it gets a bond to the surrounding Local Memory in which it has been defined
  • [/] When we return out inner, that surrounding live data is returned out too - attached on the ‘back’ of the function definition itself (which we now give a new global label returnNextElement)
  • [/] When we call returnNextElement and don’t find array or in the execution context, we look into the function definition’s ‘backpack’ of persistent live data
  • [/] The ‘backpack’ is officially known as the C.O.V.E. or ‘closure’

## So iterators turn our data into ‘streams’ of actual values we can access one after another

  • 💡 Now we have functions that hold our underlying array, the position we’re currently at in the array, and return out the next item in the ‘stream’ of elements from our array when run
  • 💡 This lets us have for loops that show us the element itself in the body on each loop and more deeply allows us to rethink arrays as flows of elements themselves which we can interact with by calling a function that switches that flow on to give us our next element
  • 💡 We have truly ‘decoupled’ the process of accessing each element from what we want to do to each element

An Iterator object is an object that conforms to the iterator protocol by providing a next() method that returns an iterator result object. The Iterator.prototype object is a hidden global object that all built-in iterators inherit from. It provides a @@iterator method that returns the iterator object itself, making the iterator also iterable.