Web
JavaScript
let, const Keywords and Block Level Scope

let, const Keywords and Block-Level Scope

Problems with Variables Declared Using the var Keyword

  • Until ES5, the only way to declare a variable was by using the var keyword.
  • Variables declared with the var keyword have unique characteristics that differentiate them from other languages. If not used carefully, they can cause significant issues.
  1. Allowing Duplicate Variable Declarations

    var x = 1;
    var y = 1;
    // Variables declared with var can be re-declared within the same scope.
    // If an initializer is present, JavaScript treats it as if the var keyword is omitted.
    var x = 100;
    // If no initializer is present, the declaration is ignored.
    var y;
     
    console.log(x); // 100
    console.log(y); // 1
  2. Function-Level Scope

    • Variables declared using var are only scoped to function code blocks.
    • → If declared outside a function, they remain global even when declared within a block.
    var x = 1;
     
    if (true) {
      // x is a global variable because var does not have block scope.
      // The existing global x is redeclared, potentially causing unintended value changes.
      var x = 10;
    }
     
    console.log(x); // 10
    var i = 10;
     
    // The i declared in the for loop is a global variable,
    // and the existing i variable is redeclared.
    for (var i = 0; i < 5; i++) {
      console.log(i); // 0 1 2 3 4
    }
     
    // The value of i has changed unintentionally.
    console.log(i); // 5
    • Function-level scope increases the risk of excessive global variables and unintended redeclarations.
  3. Variable Hoisting

    • Variables declared with var are hoisted, meaning their declarations appear to be moved to the top of the scope.
      → Due to hoisting, variables declared with var can be referenced before their declaration, but they will be initialized with undefined.

      // At this point, the variable foo has already been declared due to hoisting (1. Declaration phase)
      // The variable foo is initialized to undefined (2. Initialization phase)
      console.log(foo); // undefined
       
      // Assign a value to the variable (3. Assignment phase)
      foo = 123;
       
      console.log(foo); // 123
       
      // Variable declarations are implicitly executed by the JavaScript engine before runtime.
      var foo;
    • Although referencing a variable before its declaration does not cause an error, it can lead to readability issues and unexpected behavior.

let Keyword

  • To address the shortcomings of var, ES6 introduced let and const.
  • Let's examine let by focusing on its differences from var.
  1. Preventing Duplicate Declarations

    • Unlike var, let does not allow duplicate declarations within the same scope. → If a variable is redeclared along with an assigned value, it can unintentionally overwrite the previously declared variable's value.
    • If a variable with the same name is redeclared using the let keyword, a syntax error occurs.
    var foo = 123;
    // Variables declared with the `var` keyword can be redeclared within the same scope.
    // The following variable declaration behaves as if the `var` keyword is omitted by the JavaScript engine.
    var foo = 456;
     
    let bar = 123;
    // Variables declared with the `let` or `const` keywords do not allow redeclaration within the same scope.
    let bar = 456; // SyntaxError: Identifier 'bar' has already been declared
  2. Block-Level Scope

    • Variables declared with let are limited to the block where they are declared.
    • However, variables declared with the let keyword follow block-level scope, which means they are recognized as local scope in all code blocks (such as functions, if statements, for loops, while loops, try/catch blocks, etc.).
    let foo = 1; // Global variable
     
    {
      let foo = 2; // Local variable
      let bar = 3; // Local variable
    }
     
    console.log(foo); // 1
    console.log(bar); // ReferenceError: bar is not defined
  3. Variable Hoisting

    • Unlike variables declared with the var keyword, variables declared with the let keyword behave as if variable hoisting does not occur.
    console.log(foo); // ReferenceError: foo is not defined
    let foo;
    • If you reference a variable declared with the let keyword before its declaration, a ReferenceError will occur.

    • Variables declared with the var keyword undergo an implicit "declaration phase" and "initialization phase" by the JavaScript engine before runtime.
      → During the declaration phase, the variable identifier is registered in the scope (lexical environment of the execution context), notifying the JavaScript engine of the variable's existence. Then, in the initialization phase, the variable is initialized with undefined.
      → Therefore, even if you access the variable before its declaration, no error occurs because the variable exists in the scope. However, undefined is returned. Once the variable assignment statement is reached, the value is finally assigned.

      // Variables declared with the `var` keyword undergo the declaration and initialization phases before runtime.
      // Therefore, they can be referenced before the variable declaration statement.
      console.log(foo); // undefined
       
      var foo;
      console.log(foo); // undefined
       
      foo = 1; // The assignment phase is executed at the assignment statement.
      console.log(foo); // 1
    • Variables declared with the let keyword go through separate declaration and initialization phases.
      → Before runtime, the JavaScript engine implicitly executes the declaration phase first, but the initialization phase is executed only when the variable declaration statement is reached.

      • f you try to access the variable before the initialization phase is executed, a ReferenceError will occur.
      • Variables declared with the let keyword cannot be referenced from the beginning of the scope until the initialization phase starts (i.e., the variable declaration statement).
      • The section from the start of the scope to the start of the initialization phase, where the variable cannot be accessed, is called the Temporal Dead Zone (TDZ).
      // Before runtime, the declaration phase is executed, but the variable is not yet initialized.
      // In the **Temporal Dead Zone (TDZ)** before initialization, the variable cannot be referenced.
      console.log(foo); // ReferenceError: foo is not defined
       
      let foo; // The initialization phase is executed at the variable declaration statement.
      console.log(foo); // undefined
       
      foo = 1; // The assignment phase is executed at the assignment statement.
      console.log(foo); // 1
    • Variables declared with the let keyword appear to not be hoisted. However, this is not actually the case.

      let foo = 1; // Global variable
       
      {
        console.log(foo); // ReferenceError: Cannot acccess 'foo' before initialization
        let foo = 2; // Local variable
      }
      • If variables declared with the let keyword were not hoisted, the example above should output the value of the global variable foo.
      • However, since variables declared with let are still hoisted, a ReferenceError occurs.
      • JavaScript hoists all declarations (var, let, const, function, function*, class, etc.), including those introduced in ES6.
      • However, declarations using let, const, and class (introduced in ES6) behave as if they are not hoisted.

Global Object and let

  • Global variables declared with the var keyword, global functions, and implicitly declared global variables (variables assigned without declaration) become properties of the global object window.

  • When referencing properties of the global object, you can think of window as a reference.

    // This example should be executed in a browser environment.
     
    // Global variable
    var x = 1;
    // Implicit global
    y = 2;
    // Global function
    function foo() {}
     
    // A global variable declared with `var` becomes a property of the global object `window`.
    console.log(window.x); // 1
    // Properties of the global object `window` can be used like global variables.
    console.log(x); // 1
     
    // An implicit global also becomes a property of the global object `window`.
    console.log(window.y); // 2
    console.log(y); // 2
     
    // A global function declared using a function declaration also becomes a property of the global object `window`.
    console.log(window.foo); // f foo() {}
    // Properties of the global object `window` can be used like global variables.
    console.log(foo); // f foo() {}
  • Global variables declared with the let keyword do not become properties of the global object. This means they cannot be accessed using window.foo, for example.

  • Instead, let global variables exist within an invisible conceptual block (the declarative environment record of the global lexical environment).

    // This example should be executed in a browser environment.
     
    let x = 1;
     
    // Global variables declared with `let` or `const` are not properties of the global object `window`.
    console.log(window.x); // undefined
    console.log(x); // 1

const Keyword

  • Used to declare constants, but not limited to constants.
  • The const keyword is mostly similar to the let keyword, so we will focus on the differences.
  1. Declaration and Initialization

    • A variable declared with const must be initialized at the time of declaration. Otherwise, a syntax error occurs.

      const foo; // SyntaxError: Missing initializer in const declaration
    • Like variables declared with let, variables declared with const have block-level scope and behave as if hoisting does not occur.

      {
        // Appears as if hoisting does not occur.
        console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
        const foo = 1;
        console.log(foo); // 1
      }
       
      // Has block-level scope.
      console.log(foo); // ReferenceError: foo is not defined
  2. Reassignment Prohibited

    • Variables declared with var or let can be freely reassigned, but variables declared with const cannot be reassigned.

      const foo = 1;
      foo = 2; // TypeError: Assignment to constant variable
  3. Constants

    • When a variable declared with const is assigned a primitive value, that value cannot be changed because primitive values are immutable, and const prevents reassignment.
      → As a result, there is no way to modify the assigned value.
  4. const Keyword and Objects

  • If a variable declared with const is assigned a primitive value, the value cannot be changed. However, if it is assigned an object, the object's properties can be modified.

    const person = {
      name: "Lee",
    };
     
    // Objects are mutable, so modifications are possible without reassignment.
    person.name = "Kim";
    console.log(person); // {name:"Kim"}
  • The const keyword only prevents reassignment, but it does not mean immutability.
    → While reassigning a new value is not allowed, modifying an object’s properties—such as adding, deleting, or changing property values—is still possible.
    → Even if the object is modified, the reference value stored in the variable remains unchanged.

var vs let vs const

  • By default, use const for variable declarations. Use let only when reassignment is necessary.
  • Using const prevents unintended reassignment, making the code safer.
  • The recommended usage of var, let, and const is as follows:
    • If using ES6, avoid the var keyword.
    • Use let only when reassignment is necessary, and keep the variable’s scope as narrow as possible.
    • Use const for primitive values and objects that are read-only (i.e., values that do not require reassignment). Since const prevents reassignment, it is safer than var or let.
  • At the time of declaration, it may not always be clear whether a variable needs reassignment. → Therefore, always declare variables with const first. If reassignment is later found to be necessary, switching to let is not too late.