JavaScript - DOM, Class, and Closure

·

8 min read

JavaScript - DOM, Class, and Closure

DOM

DOM: Document Object Model

  • JavaScript is a light-weight language built to create a web page dynamically running on the browser

How the web page is loaded

  • User = Browser = Client

  • The client (through the browser) sends requests to the server

  • The server sends responses (HTML documents) to the client

  • The browser should interpret these HTML documents (= rendering)

  • Why rendering required?

    • JavaScript cannot understand HTML documents

    • Thus, the browser should interpret this for JS

DOM Tree

  • based on the interpreted contents that JS can understand, DOM Tree is organized

Render Tree

  • DOM Tree + CSSOM (CSS Object Model) Tree

  • The Render Tree is a hierarchical structure of objects that represents the final document model that is actually rendered in the browser, (1) parsed from HTML, CSS, and JavaScript documents.

  • In essence, (2) it creates the final version of the document to be drawn on the browser screen. After that, (3) layout calculation and (4) painting are initiated to draw the picture on the browser. Finally, (5) with compositing layers, it is shown on the actual screen.

  • Browser Rendering Order: (1) ~ (5)

  • More: Browser Rendering

DOM API

  • DOM is a browser built-in API

  • A browser provides APIs related to the DOM to access the browser's DOM object

  • So, we can access & control HTML contents through JS

  • Nodes of all DOMs have methods (verb) and attributes (noun)

Class

  • Class: a blueprint to design the object (sketch for desk)

    • Purpose: to reuse code
  • Instance: the actual object based on the class (the actual desk)

// Class - blueprint
class Person {
    // Constructor - necessity (noun)
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // Method - optional (verb)
    sayHello() {
        console.log(`Hello, my name is ${this.name}.`);
    }

    sayAge() {
        console.log(`I am ${this.age} years old.`);
    }
}

// Instance - actual object
const person1 = new Person("John", 30);
const person2 = new Person("Mary", 25);

person1.sayHello();
person1.sayAge();

person2.sayHello();
person2.sayAge();

Getters & Setters

To check the validity of properties

// Getters and Setters
// OOP
// Class -> Object (Instance)
// Properties (in constructor)
// when changing properties, use getters and setters

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    // getter for width
    get width() {
        // with no underscore, it will be an infinite loop (ERROR: maximum call stack size exceeded)
        // underscore usually means private
        return this._width;
    }
    // setter for width
    set width(value) {
        if (value <= 0) {
            console.log("Width must be positive.");
            return;
        } else if (typeof value !== "number") {
            console.log("Width must be a number.");
            return;
        }
        this._width = value;
    }
    // getter for height
    get height() {
        return this._height;
    }
    // setter for height
    set height(value) {
        if (value <= 0) {
            console.log("Height must be positive.");
            return;
        } else if (typeof value !== "number") {
            console.log("Height must be a number.");
            return;
        }
        this._height = value;
    }

    getArea() {
        console.log(`Dimension = ${this.width} * ${this.height} = ${this.width * this.height}`);
    }
}

const rectangle1 = new Rectangle(10, 5);
rectangle1.getArea();
const rectangle2 = new Rectangle(20, 10);
rectangle2.getArea();

Inheritance

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} is speaking!`);
    }
}

const siwon = new Animal("Siwon");
siwon.speak();

// Inheritance
class Dog extends Animal {
    constructor(name, breed) {
        super(name); // inherit from super class
        this.breed = breed;
    }
    // overriding: redefined method
    speak() {
        console.log(`${this.name} is barking!`);
    }
}

const dog1 = new Dog("Bobby", "Poodle");
dog1.speak();

Static Method

It is used when we don't need to create an instance of the class

class Calculator {
    static add(a, b) {
        return a + b;
    }
    static subtract(a, b) {
        return a - b;
    }
}
// no instance made
console.log(Calculator.add(1, 2));
console.log(Calculator.subtract(1, 2));

Closure

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

  • The lexical environment where the function is declared === the information like variables of the outer function when the function is declared
const x = 1;

function outerFunc() {
  const x = 10;
  function innerFunc() {
    console.log(x); // 10 
  }

  innerFunc();
}

outerFunc();
  • Where does x get referenced?

    • innerFunc() scope: no x defined

    • outerFunc() scope: const x = 10 defined -> reference 10

const x = 1;

// Even though innerFunc() is called inside the outerFunc(),
function outerFunc() {
  const x = 10;
  innerFunc(); // 1
}

// since innerFunc() and outerFunc() have different scope,
// x does not get referenced from outerFunc().
function innerFunc() {
  console.log(x); // 1
}

outerFunc();
  • Lexical Scope: JS engine decides the outer scope or the scope depending on where it is "defined", not where it is "called"

Closure and Lexical Environment

In the concept that a (1) nested function can refer to variables of an already terminated (2) outer function when the nested function has a longer lifespan than the outer function, the nested function is called a closure.

const x = 1;

function outer() {
  const x = 10;
  const inner = function () { // inner is closure function
    console.log(x);
  };
  return inner;
}

const innerFunc = outer(); 
// outer() already terminated (outer() execution context already popped from call stack)
innerFunc(); // print 10

(1) inner() - closure function (still reference outer()'s x variable (10) even though outer() is terminated)

(2) outer()

In other words, the execution context of the outer function is removed from the execution context stack, but its lexical environment is not destroyed.

How does this work? => Garbage collector does leave the outer function's LE since its LE still gets referenced

Closure Practice

The purpose of Closure: To change, maintain, and encapsulate the state (surrounding LE) safely

// Vulnerable Count
let num = 0; // not safe: defined in Global scope -> let's make it as a local variable

const increase = function () {
    return ++num;
};

console.log(increase()); // 1
num = 100; // vulnerable - count state can be 'only changed' under increase()
console.log(increase()); // 101
console.log(increase()); // 102
// Not working Count
const increase = function () {
  let num = 0; // we brought this to the local in increase()

  return ++num;
};

// can't maintain the previous value of num
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
// Count with Closure
const increase = (function () {
  let num = 0; // garbage collector does't collect this

  // Closure - this closure function can reference the outer increase()'s LE
  return function () {
    return ++num;
  };
})(); // IIFE(Immediately-involked-function)

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
  • When the above code is executed, the immediately invoked function is immediately called and the function returned by the immediately invoked function is assigned to the increase variable.

  • The function assigned to the increase variable is a closure that remembers the lexical environment of the immediately invoked function, which is determined by the location where it was defined.

  • The immediately invoked function is destroyed after it is called, but the closure returned by the immediately invoked function is assigned to the increase variable and is called.

  • At this point, the closure returned by the immediately invoked function remembers the lexical environment of the immediately invoked function, which is determined by the location where it was defined.

  • Therefore, the closure returned by the immediately invoked function can refer to and change the count state of the free variable num whenever and wherever it is called.

  • num will not be initialized and is a hidden private variable that cannot be directly accessed from outside, so there is no need to worry about unintended changes as with using a global variable.

// More functionality
const counter = (function () {
  let num = 0;

  return {
    increase() {
      return ++num;
    },
    decrease() {
      return num > 0 ? --num : 0;
    },
  };
})();

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0

Closures are used to safely encapsulate state and prevent unintended changes to it by hiding it and allowing only specific functions to modify the state. This allows for secure state changes and maintenance.

Encapsulation & Information Hiding

Encapsulation: binding properties (object's state) + methods (behavior)

  • Java and other programming languages use public, private, and protected. But, JS does not have these
// Constructor
function Person(name, age) {
  this.name = name; // public
  let _age = age; // private

  // Instance Method
  // Thus, everytime Person object is created, it create duplicates
  // : Solution -> prototype
  this.sayHi = function () {
    console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
  };
}

const me = new Person("Choi", 33);
me.sayHi(); // Hi!, My name is Choi. I am 33.
console.log(me.name); // Choi - public
console.log(me._age); // undefined - private

const you = new Person("Lee", 30);
you.sayHi(); // Hi! My name is Lee. I am 30.
console.log(you.name); // Lee - public
console.log(you.age); // undefined - private