JavaScript
Fundamentals

Data Types

JavaScript, like most programming languages, has two types of data: Primitive Data Types and Reference Data Types. Understanding the difference between these two categories is crucial because it influences how variables are stored and manipulated in memory. Let’s break them down.

Data Types in JavaScript

JavaScript has two main categories of data types:

  1. Primitive Types: Immutable and stored by value.
  2. Reference Types: Mutable and stored by reference.

Primitive Data Types

Primitives are immutable and copied by value, meaning a copy of the value is passed when assigned or passed to a function.

List of Primitive Types:

  1. undefined – Variable declared but not initialized.

    let x;
    console.log(x); // undefined
  2. null – Represents intentional absence of a value.

    let y = null;
    console.log(y); // null
  3. boolean – Logical values: true or false.

    let isTrue = true;
    let isFalse = false;
  4. number – Includes integers and floats.

    let age = 25;
    let pi = 3.14;
  5. string – A sequence of characters.

    let greeting = "Hello!";
  6. symbol – Unique identifiers.

    let sym1 = Symbol('id');
    let sym2 = Symbol('id');
    console.log(sym1 === sym2); // false
  7. bigint – For large integers beyond Number.MAX_SAFE_INTEGER.

    let largeNum = 12345678901234567890n;

Key Behavior:

  • Pass by Value: A new copy is created.
    let a = 10;
    let b = a; // b is a copy of a
    b = 20;
    console.log(a); // 10
    console.log(b); // 20

Reference Data Types

Reference types are mutable and copied by reference. This means they store a reference (address) to the object in memory. Changes to one variable affect all references.

List of Reference Types:

  1. Objects – Key-value pairs.

    let person = { name: "Alice", age: 30 };
  2. Arrays – Ordered collections.

    let arr = [1, 2, 3];
  3. Functions – Reusable blocks of code.

    function greet() { return "Hello!"; }

Key Behavior:

  • Pass by Reference: The memory reference is copied, not the actual object.
    let obj1 = { name: "Alice" };
    let obj2 = obj1; // obj2 references obj1
     
    obj2.name = "Bob";
    console.log(obj1.name); // "Bob" (both point to the same object)

Comparison: Primitive vs Reference Types

AspectPrimitive TypesReference Types
MutabilityImmutableMutable
StoredBy ValueBy Reference
CopyingCopies valueCopies reference
Examplesnumber, string, booleanobject, array, function

Conclusion

  • Primitive types are simple, immutable, and copied by value.
  • Reference types are complex, mutable, and copied by reference. Understanding the difference between the two helps in managing data more effectively and avoiding unintended mutations.

Shallow vs Deep Copy

Shallow Copy

A shallow copy of an object only copies the top-level properties. If the original object contains nested objects or arrays, the shallow copy will reference the same nested objects.

How to Create a Shallow Copy

  1. Using Object.assign()

    const originalObject = {
      name: "Alice",
      age: 28,
      contact: { email: "alice@example.com", phone: "123-456-7890" }
    };
     
    const shallowCopy1 = Object.assign({}, originalObject);
  2. Using the Spread Operator

    const shallowCopy2 = { ...originalObject };

Example:

const userProfile = {
  name: "Alice",
  contact: { email: "alice@example.com" },
  addresses: [{ city: "Los Angeles" }]
};
 
// Create shallow copy
const shallowCopy = { ...userProfile };
 
// Modify shallow copy
shallowCopy.contact.email = "newemail@example.com";
shallowCopy.addresses[0].city = "San Diego";
 
console.log("Original User Profile:", userProfile);
console.log("Shallow Copy:", shallowCopy);

Output:

Original User Profile:
{
  name: 'Alice',
  contact: { email: 'newemail@example.com' },
  addresses: [{ city: 'San Diego' }]
}

Shallow Copy:
{
  name: 'Alice',
  contact: { email: 'newemail@example.com' },
  addresses: [{ city: 'San Diego' }]
}

Deep Copy

A deep copy duplicates all levels of the object, including nested objects and arrays. The copied object and the original object do not share any references.

How to Create a Deep Copy

  1. Using JSON.parse() and JSON.stringify()

    const deepCopy = JSON.parse(JSON.stringify(originalObject));
  2. Using a Recursive Function

    function deepCopy(obj) {
      if (obj === null || typeof obj !== 'object') return obj;
      if (Array.isArray(obj)) return obj.map(deepCopy);
      const copy = {};
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          copy[key] = deepCopy(obj[key]);
        }
      }
      return copy;
    }

Example:

const userProfile = {
  name: "Alice",
  contact: { email: "alice@example.com" },
  addresses: [{ city: "Los Angeles" }]
};
 
// Create deep copy
const deepCopy = JSON.parse(JSON.stringify(userProfile));
 
// Modify deep copy
deepCopy.contact.email = "anothernewemail@example.com";
deepCopy.addresses[0].city = "Seattle";
 
console.log("Original User Profile:", userProfile);
console.log("Deep Copy:", deepCopy);

Output:

Original User Profile:
{
  name: 'Alice',
  contact: { email: 'alice@example.com' },
  addresses: [{ city: 'Los Angeles' }]
}

Deep Copy:
{
  name: 'Alice',
  contact: { email: 'anothernewemail@example.com' },
  addresses: [{ city: 'Seattle' }]
}

Summary

  • Shallow Copy: Copies only the top level, with nested objects being referenced. Changes to nested objects affect both the original and copied objects.
  • Deep Copy: Duplicates all levels of an object, making the copied object fully independent of the original. Changes to the deep copy do not affect the original object.

Type Coercion

Type coercion is the process of converting one data type to another. JavaScript is a loosely typed language, meaning it allows implicit and explicit conversions between different data types.

Types of Type Coercion:

  1. Implicit Coercion: JavaScript automatically converts types when an operation involves mismatched types.
  2. Explicit Coercion: The developer manually converts one data type to another.

Implicit Type Coercion

JavaScript tries to automatically convert types when it encounters expressions involving different data types. Implicit coercion usually happens in operations like addition, equality comparisons (==), and conditionals.

Examples of Implicit Coercion:

  1. String + Number: JavaScript converts the number to a string and concatenates.

    let result = "5" + 10;
    console.log(result); // "510"
  2. String - Number: JavaScript converts the string to a number for subtraction.

    let result = "5" - 2;
    console.log(result); // 3
  3. Boolean + Number: true is converted to 1, and false is converted to 0.

    let result = true + 2;
    console.log(result); // 3
  4. null + Number: null is converted to 0.

    let result = null + 5;
    console.log(result); // 5
  5. undefined + Number: undefined becomes NaN (Not-a-Number).

    let result = undefined + 3;
    console.log(result); // NaN

Common Implicit Coercion in Conditional Statements:

  • Falsy Values: false, 0, "" (empty string), null, undefined, NaN are all falsy.
  • Truthy Values: Everything else (non-zero numbers, non-empty strings, objects) is considered truthy.
if ("") {
  console.log("This won't execute, because '' is falsy");
}
 
if ("Hello") {
  console.log("This will execute, because 'Hello' is truthy");
}

Explicit Type Coercion

Explicit coercion is when you manually convert a value to another type using JavaScript methods.

Examples of Explicit Coercion:

  1. String to Number: Using Number(), parseInt(), or the unary + operator.

    let str = "123";
    let num = Number(str);  // 123
    let num2 = +str;        // 123
  2. Number to String: Using String() or .toString().

    let num = 123;
    let str = String(num);  // "123"
    let str2 = num.toString();  // "123"
  3. Boolean to Number: true becomes 1, and false becomes 0.

    let bool = true;
    let num = Number(bool);  // 1
  4. Number to Boolean: Any non-zero number becomes true; 0 becomes false.

    let num = 10;
    let bool = Boolean(num);  // true
    let zeroBool = Boolean(0);  // false

Example: Convert between types explicitly

let strNum = "42";
console.log(typeof strNum); // "string"
 
let convertedNum = Number(strNum);
console.log(typeof convertedNum); // "number"

Equality Comparisons

One of the most common areas where type coercion occurs is equality comparisons. JavaScript provides two types of equality operators:

  1. == (Loose Equality): Allows type coercion. It converts the operands to the same type before comparing them.
  2. === (Strict Equality): Does not allow type coercion. It checks both the value and the type.

Loose Equality (==) Example:

  • In the case of ==, JavaScript attempts to coerce types to make the comparison succeed.

    console.log(1 == "1");  // true (string "1" is coerced to number 1)
    console.log(true == 1);  // true (true is coerced to 1)
    console.log(null == undefined);  // true (special case)
    console.log([] == false);  // true (empty array is coerced to false)
  • Explanation:

    • 1 == "1": The string "1" is coerced into a number 1 for comparison.
    • true == 1: true is coerced to 1, and the comparison succeeds.

Strict Equality (===) Example:

  • With ===, type coercion is not allowed, and both the value and type must be identical.

    console.log(1 === "1");  // false (no coercion, number vs string)
    console.log(true === 1);  // false (boolean vs number)
    console.log([] === false);  // false (object vs boolean)
  • Explanation:

    • 1 === "1": This is false because the types are different (number vs string).
    • true === 1: This is false because boolean is not equal to number.

Type Coercion Rules

  1. Number and String: The string is converted to a number.

    console.log(2 == "2"); // true (string "2" is coerced to number 2)
  2. Boolean and Number: true becomes 1, and false becomes 0.

    console.log(0 == false); // true (false coerced to 0)
  3. null and undefined: These are only loosely equal to each other and not to anything else.

    console.log(null == undefined); // true
    console.log(null == 0);         // false
  4. Objects: Objects are not coerced; they are compared by reference.

    let obj1 = {};
    let obj2 = {};
    console.log(obj1 == obj2);  // false (different references)

Example of Type Coercion

// Example 1: Implicit Type Coercion with `==`
console.log('5' == 5); 
// Output: true
// Explanation: The string '5' is coerced to the number 5, so they are equal.
 
console.log('5' == '5'); 
// Output: true
// Explanation: Both values are strings and are equal.
 
console.log(0 == false); 
// Output: true
// Explanation: The number 0 is coerced to false, so they are considered equal.
 
console.log(null == undefined); 
// Output: true
// Explanation: `null` and `undefined` are considered equal when using `==`.
 
 
// Example 2: Strict Equality with `===`
console.log('5' === 5); 
// Output: false
// Explanation: The types are different (string vs number), so they are not strictly equal.
 
console.log('5' === '5'); 
// Output: true
// Explanation: Both values are strings and are equal.
 
console.log(0 === false); 
// Output: false
// Explanation: The types are different (number vs boolean), so they are not strictly equal.
 
console.log(null === undefined); 
// Output: false
// Explanation: The types are different, so they are not strictly equal.
 
 
// Example 3: Implicit Coercion in Arithmetic
console.log('10' + 5); 
// Output: '105'
// Explanation: The number 5 is coerced into a string, resulting in concatenation.
 
console.log('10' - 5); 
// Output: 5
// Explanation: The string '10' is coerced into the number 10, so the subtraction yields 5.
 
console.log('10' * '2'); 
// Output: 20
// Explanation: Both strings are coerced into numbers, so the multiplication yields 20.
 
console.log('10' / '2'); 
// Output: 5
// Explanation: Both strings are coerced into numbers, so the division yields 5.
 
 
// Example 4: Implicit Coercion with `==` and Arrays
console.log([] == false); 
// Output: true
// Explanation: The empty array [] is coerced into an empty string "", and "" is loosely equal to false.
 
console.log([] == ![]); 
// Output: true
// Explanation: ![] is false, and the empty array [] is coerced into an empty string "", which is loosely equal to false.
 
console.log([] == [] + []); 
// Output: true
// Explanation: [] + [] results in an empty string "", so [] == "" is true.
 
console.log([] == [1] - [1]); 
// Output: false
// Explanation: [] - [1] results in -1, and [] == -1 is false.
 
 
// Example 5: Explicit Type Conversion
console.log(Number('5')); 
// Output: 5
// Explanation: The string '5' is explicitly converted to the number 5.
 
console.log(String(5)); 
// Output: '5'
// Explanation: The number 5 is explicitly converted to the string '5'.
 
console.log(Boolean('0')); 
// Output: true
// Explanation: Non-empty strings are truthy, so '0' evaluates to true.
 
console.log(Boolean('')); 
// Output: false
// Explanation: Empty strings are falsy, so '' evaluates to false.
 
console.log(Boolean(0)); 
// Output: false
// Explanation: The number 0 is falsy.
 
console.log(Boolean(1)); 
// Output: true
// Explanation: The number 1 is truthy.

Summary

  • Implicit coercion happens automatically when JavaScript tries to convert types during operations (e.g., string + number, boolean in conditions).
  • Explicit coercion requires using specific methods to manually convert data types (e.g., Number(), String()).
  • == allows type coercion during comparison, while === does not, making it stricter and more predictable.

Scope in JavaScript

Understanding scope in JavaScript and the differences between var, let, and const is crucial for writing predictable and bug-free code. Let’s break this down in detail with examples.

Global Scope

Variables declared outside of any function or block are in the global scope. They are accessible from anywhere in the code.

var globalVar = 'I am global';
 
function displayGlobal() {
  console.log(globalVar); // Accessible here
}
 
displayGlobal();
console.log(globalVar); // Accessible here as well

Explanation:

  • globalVar is declared in the global scope.
  • It can be accessed from anywhere in the code, including inside functions.

Function Scope

Variables declared inside a function are only accessible within that function. This is known as function scope.

function exampleFunction() {
  var functionVar = 'I am inside a function';
  console.log(functionVar); // Accessible here
}
 
exampleFunction();
console.log(functionVar); // Error: functionVar is not defined

Explanation:

  • functionVar is declared inside exampleFunction and is only accessible within that function.
  • Trying to access functionVar outside exampleFunction results in an error.

Block Scope

Variables declared within a block (e.g., inside curly braces {}) are only accessible within that block. This scope is introduced with let and const.

if (true) {
  let blockLet = 'I am inside a block';
  const blockConst = 'I am also inside a block';
  console.log(blockLet);  // Accessible here
  console.log(blockConst); // Accessible here
}
 
console.log(blockLet); // Error: blockLet is not defined
console.log(blockConst); // Error: blockConst is not defined

Explanation:

  • blockLet and blockConst are declared inside an if block.
  • They are only accessible within that block, and trying to access them outside results in an error.

Differences Between var, let, and const

var

  • Scope: Function scope.
  • Hoisting: Variables declared with var are hoisted to the top of their function or global scope. They are initialized with undefined.
  • Re-declaration: Variables declared with var can be re-declared and updated.
function testVar() {
  console.log(a); // undefined (hoisted)
  var a = 10;
  console.log(a); // 10
}
 
testVar();

Explanation:

  • var a is hoisted and initialized with undefined before any code is executed.

let

  • Scope: Block scope.
  • Hoisting: Variables declared with let are hoisted but not initialized. They remain in a "temporal dead zone" from the start of the block until the declaration is encountered.
  • Re-declaration: Variables declared with let cannot be re-declared in the same scope.
function testLet() {
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
  let a = 10;
  console.log(a); // 10
}
 
testLet();

Explanation:

  • let a is not initialized until its declaration is encountered, causing a ReferenceError if accessed before.

const

  • Scope: Block scope.
  • Hoisting: Variables declared with const are hoisted but not initialized, similar to let. They are also in the "temporal dead zone."
  • Re-declaration: Variables declared with const cannot be re-declared or reassigned. const requires initialization at the time of declaration.
function testConst() {
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
  const a = 10;
  console.log(a); // 10
  a = 20; // TypeError: Assignment to constant variable
}
 
testConst();

Explanation:

  • const a must be initialized when declared and cannot be reassigned after initialization. Accessing it before initialization results in a ReferenceError.

Summary with Examples

Here is a consolidated example demonstrating all three:

var globalVar = 'I am global';
 
function scopeExample() {
  var functionVar = 'I am function scoped';
  if (true) {
    let blockLet = 'I am block scoped with let';
    const blockConst = 'I am block scoped with const';
    console.log(globalVar); // Accessible
    console.log(functionVar); // Accessible
    console.log(blockLet);  // Accessible
    console.log(blockConst); // Accessible
  }
  console.log(blockLet); // Error: blockLet is not defined
  console.log(blockConst); // Error: blockConst is not defined
}
 
scopeExample();
console.log(globalVar); // Accessible
console.log(functionVar); // Error: functionVar is not defined

Explanation:

  1. Global Scope: globalVar is accessible everywhere.
  2. Function Scope: functionVar is only accessible within scopeExample.
  3. Block Scope: blockLet and blockConst are only accessible within the if block.
  4. Hoisting and Temporal Dead Zone: var variables are hoisted and initialized with undefined, while let and const are hoisted but not initialized, causing errors if accessed before their declaration.

This detailed explanation and examples should help clarify the different types of scope and the behavior of var, let, and const in JavaScript.

Understanding Scoping with Tricky Examples

In JavaScript, understanding scope is crucial for writing predictable code. Let’s dive into some tricky examples to explore how variable declarations (var, let, const) interact with different scopes and closures.

Example: Scoping and Shadowing

function outerFunction() {
  var x = 10;
  let y = 20;
  const z = 30;
 
  function innerFunction() {
    var x = 40;
    let y = 50;
    const z = 60;
 
    console.log('Inside innerFunction:');
    console.log('x:', x); // Output: 40, `x` inside `innerFunction()` shadows the `x` from `outerFunction()`
    console.log('y:', y); // Output: 50, `y` inside `innerFunction()` shadows the `y` from `outerFunction()`
    console.log('z:', z); // Output: 60, `z` inside `innerFunction()` shadows the `z` from `outerFunction()`
  }
 
  innerFunction();
 
  console.log('Inside outerFunction:');
  console.log('x:', x); // Output: 10, `x` inside `outerFunction()` is unaffected by `x` inside `innerFunction()`
  console.log('y:', y); // Output: 20, `y` inside `outerFunction()` is unaffected by `y` inside `innerFunction()`
  console.log('z:', z); // Output: 30, `z` inside `outerFunction()` is unaffected by `z` inside `innerFunction()`
}
 
outerFunction();

Hoisting

Hoisting is a JavaScript behavior where variable and function declarations are moved to the top of their containing scope during the compilation phase. This allows variables and functions to be used before they are declared in the code.

Key Concepts

  1. Variable Hoisting:

    • Only the declaration is hoisted, not the initialization. Variables declared with var are initialized to undefined until the actual assignment line is executed.
  2. Function Hoisting:

    • Entire function declarations are hoisted, allowing functions to be called before their declaration appears in the code.
  3. Temporal Dead Zone (TDZ):

    • For let and const, declarations are hoisted but are not initialized until their declaration line is reached. Accessing these variables before their declaration results in a ReferenceError due to the TDZ.

Example of Hoisting

// Variable hoisting with var
console.log(x); // Output: undefined (declaration hoisted, initialization not)
var x = 10;
console.log(x); // Output: 10 (value assigned)
 
// Function hoisting
greet(); // Output: "Hello, world!" (function can be called before declaration)
function greet() {
  console.log("Hello, world!");
}
 
// Function expression hoisting
try {
  sayHi(); // Throws ReferenceError: Cannot access 'sayHi' before initialization
} catch (error) {
  console.error(error.message); // Output: Cannot access 'sayHi' before initialization
}
var sayHi = function() {
  console.log("Hi!");
};

Example of Temporal Dead Zone (TDZ)

function example() {
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
  let a = 5;
 
  console.log(b); // ReferenceError: Cannot access 'b' before initialization
  const b = 10;
}
 
example();

Why Hoisting and TDZ Matter

  1. Predictable Behavior:

    • Hoisting allows functions to be called before they are declared, and TDZ helps prevent accessing variables before they are initialized, leading to more predictable code.
  2. Error Prevention:

    • TDZ prevents usage of let and const variables before they are declared, reducing potential bugs and improving code reliability.

Disadvantages

  1. Confusion:

    • Hoisting can be confusing, especially when dealing with variables declared with var versus let and const.
  2. Debugging Difficulty:

    • Errors related to hoisting and TDZ can be challenging to debug, particularly in complex codebases.

Best Practices

  • Declare Variables and Functions at the Top: To avoid issues, always declare variables and functions at the top of their scope.
  • Use let and const: Prefer let and const for block-scoped variables to avoid hoisting pitfalls and leverage TDZ for safer code.
  • Understand Scope and Initialization: Be aware of how and when variables are initialized to ensure correct usage.

Understanding hoisting and the Temporal Dead Zone helps in writing clear, reliable JavaScript code, preventing common errors and improving code maintainability.

Closures

What's Closure

Closure: In JavaScript, a closure is a function that retains access to its lexical scope, even after the outer function has finished executing. This allows the inner function to access variables and parameters from the outer function, effectively preserving and manipulating state across different executions.

User Account Manager

This example demonstrates how closures can encapsulate private data and manage user-specific operations.

function createUserManager(initialBalance) {
  let balance = initialBalance; // Private variable to store the user's balance
 
  return {
    deposit: function(amount) {
      if (amount > 0) {
        balance += amount;
        console.log(`Deposited: $${amount}`);
      } else {
        console.log('Deposit amount must be positive');
      }
    },
    withdraw: function(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        console.log(`Withdrew: $${amount}`);
      } else {
        console.log('Invalid withdrawal amount');
      }
    },
    getBalance: function() {
      return balance;
    }
  };
}
 
// Creating two user accounts
const user1 = createUserManager(1000);
const user2 = createUserManager(500);
 
// Operations on user1
user1.deposit(200);        // Deposited: $200
console.log(user1.getBalance()); // Output: 1200
user1.withdraw(500);       // Withdrew: $500
console.log(user1.getBalance()); // Output: 700
 
// Operations on user2
user2.deposit(300);        // Deposited: $300
console.log(user2.getBalance()); // Output: 800
user2.withdraw(100);       // Withdrew: $100
console.log(user2.getBalance()); // Output: 700
 
// Checking that user1 and user2 have independent balances
console.log(user1.getBalance()); // Output: 700
console.log(user2.getBalance()); // Output: 700

Explanation:

  1. createUserManager Function:

    • Initializes a private balance variable and returns an object with methods to interact with it.
  2. Closure in Action:

    • Each method (deposit, withdraw, getBalance) forms a closure around the balance variable, allowing them to access and modify it even after createUserManager has finished executing.
  3. Encapsulation:

    • The balance variable is encapsulated within the closure, preventing direct access from outside the createUserManager function. This allows controlled access through the provided methods.

Function Executes Only Once

This example demonstrates a function that ensures another function is executed only once, regardless of how many times it is called.

function createOnceFunction(fn) {
  let executed = false; // Track whether the function has been executed
 
  return function(...args) {
    if (!executed) {
      fn(...args); // Execute the function only once
      executed = true; // Mark as executed
    } else {
      console.log('Function already executed'); // Notify if already executed
    }
  };
}
 
// Example function to execute only once
const initialize = () => console.log('Initialized');
 
// Create a function that executes `initialize` only once
const initializeOnce = createOnceFunction(initialize);
 
// Test the once function
initializeOnce(); // Output: Initialized
initializeOnce(); // Output: Function already executed
initializeOnce(); // Output: Function already executed

Explanation:

  1. createOnceFunction Function:

    • Initializes a variable executed to track if the provided function fn has been called.
    • Returns a function that checks if fn has been executed. If not, it executes fn and sets executed to true.
  2. Closure in Action:

    • The inner function retains access to the executed variable and the function fn through the closure.
    • This ensures fn is executed only once, regardless of how many times the returned function is called.
  3. Use Case:

    • Ideal for scenarios like configuration initialization, logging, or any task that should only be performed a single time throughout the application’s lifecycle.

Advantages of Closures

  1. Data Encapsulation:

    • Closures provide private variables and methods, enhancing data protection and encapsulation.
  2. State Management:

    • Closures help manage and preserve state across different function calls, useful for complex logic and persistent data.
  3. Function Factories:

    • Closures enable creating functions with customized behaviors and maintaining state, like creating functions that execute only once.

Disadvantages of Closures

  1. Increased Memory Usage:

    • Closures can lead to higher memory consumption because they retain references to their lexical environment.
  2. Complexity:

    • Overusing or deeply nesting closures can make code harder to understand and debug.

Best Practices

  • Use Closures Wisely: Apply closures judiciously to balance memory usage and code complexity.
  • Encapsulate Related Functionality: Group related functionality using closures to improve code organization.
  • Monitor Performance: Be aware of potential performance impacts and optimize closure usage where necessary.

Closures are a powerful feature in JavaScript that enable advanced patterns for managing state and encapsulating logic, making them a valuable tool for building efficient and maintainable applications.

Exploring Modern JS Features

JavaScript has seen significant advancements over the years, with ECMAScript (ES) versions bringing new features and syntax improvements to the language. In this article, we will delve into some of the key features introduced in ES6 and ES14, providing a clear definition and practical examples for each.

ES6 Features

  1. Arrow Functions

    • Definition: Arrow functions offer a concise syntax for writing functions and inherit the this context from their surrounding scope, making them particularly useful for functions that need to preserve the this context from their surrounding code.
    • Example:
      // Traditional function
      function add(a, b) {
        return a + b;
      }
       
      // Arrow function
      const add = (a, b) => a + b;
       
      console.log(add(2, 3)); // Output: 5
  2. Classes

    • Definition: Classes provide a cleaner syntax for creating objects and handling inheritance, offering a more structured approach compared to prototype-based inheritance.
    • Example:
      class Person {
        constructor(name) {
          this.name = name;
        }
       
        greet() {
          return `Hello, ${this.name}!`;
        }
      }
       
      const person = new Person('Alice');
      console.log(person.greet()); // Output: Hello, Alice!
  3. Template Literals

    • Definition: Template literals enable embedded expressions and multi-line strings using backticks, making string interpolation and formatting more straightforward.
    • Example:
      const name = 'Bob';
      const age = 25;
      const message = `My name is ${name} and I am ${age} years old.`;
       
      console.log(message); // Output: My name is Bob and I am 25 years old.
  4. Destructuring Assignment

    • Definition: Destructuring assignment allows for the extraction of values from arrays and objects into variables, simplifying code and improving readability.
    • Example:
      // Array destructuring
      const [a, b] = [1, 2];
      console.log(a, b); // Output: 1 2
       
      // Object destructuring
      const { x, y } = { x: 10, y: 20 };
      console.log(x, y); // Output: 10 20
  5. Default Parameters

    • Definition: Default parameters allow you to specify default values for function parameters, simplifying function calls and avoiding undefined issues.
    • Example:
      function greet(name = 'Guest') {
        return `Hello, ${name}!`;
      }
       
      console.log(greet()); // Output: Hello, Guest!
      console.log(greet('John')); // Output: Hello, John!
  6. Rest and Spread Operators

    • Definition: The rest operator collects multiple function arguments into an array, while the spread operator expands an array into individual elements, streamlining array manipulation.
    • Example:
      // Rest parameters
      function sum(...numbers) {
        return numbers.reduce((acc, num) => acc + num, 0);
      }
      console.log(sum(1, 2, 3)); // Output: 6
       
      // Spread operator
      const arr1 = [1, 2];
      const arr2 = [3, 4];
      const combined = [...arr1, ...arr2];
      console.log(combined); // Output: [1, 2, 3, 4]
  7. Enhanced Object Literals

    • Definition: Enhanced object literals simplify the definition of object properties and methods using shorthand syntax, making the code more concise.
    • Example:
      const name = 'Eve';
      const obj = {
        name,
        greet() {
          return `Hello, ${this.name}!`;
        }
      };
       
      console.log(obj.greet()); // Output: Hello, Eve!
  8. Promises

    • Definition: Promises represent the completion or failure of an asynchronous operation, providing a cleaner approach to handling asynchronous code compared to traditional callbacks.
    • Example:
      const fetchData = new Promise((resolve, reject) => {
        setTimeout(() => resolve('Data received'), 1000);
      });
       
      fetchData
        .then(result => console.log(result)) // Output: Data received
        .catch(error => console.log(error));
  9. Modules

    • Definition: Modules allow code to be split into separate files, with import and export statements facilitating code reuse and organization.
    • Example:
      // In math.js
      export const add = (a, b) => a + b;
       
      // In main.js
      import { add } from './math.js';
      console.log(add(2, 3)); // Output: 5
  10. Symbol

    • Definition: Symbols are unique and immutable primitives used primarily as object property keys, ensuring that property names do not clash.
    • Example:
      const uniqueSymbol = Symbol('description');
      const obj = {
        [uniqueSymbol]: 'This is a unique value'
      };
       
      console.log(obj[uniqueSymbol]); // Output: This is a unique value

ES14 Features

  1. Logical Assignment Operators

    • Definition: Logical assignment operators combine logical operations with assignment in a single expression, simplifying common operations.
    • Example:
      let x = 0;
      x ||= 10; // x = x || 10
      console.log(x); // Output: 10
       
      let y = 5;
      y &&= 0; // y = y && 0
      console.log(y); // Output: 0
  2. Numeric Separators

    • Definition: Numeric separators (_) improve the readability of large numbers by allowing digit grouping.
    • Example:
      const billion = 1_000_000_000;
      console.log(billion); // Output: 1000000000
       
      const price = 1_299.99;
      console.log(price); // Output: 1299.99
  3. WeakRefs

    • Definition: WeakRef allows the creation of weak references to objects, which can be garbage-collected while still being referenced.
    • Example:
      let obj = { name: 'A weak reference' };
      const weakRef = new WeakRef(obj);
       
      console.log(weakRef.deref()); // Output: { name: 'A weak reference' }
      obj = null; // obj is eligible for garbage collection
      console.log(weakRef.deref()); // Output: undefined
  4. FinalizationRegistry

    • Definition: FinalizationRegistry registers cleanup callbacks that are called when objects are garbage-collected, useful for resource management.
    • Example:
      const registry = new FinalizationRegistry((heldValue) => {
        console.log(`Cleaned up: ${heldValue}`);
      });
       
      let obj = { name: 'To be cleaned up' };
      registry.register(obj, obj.name);
       
      obj = null; // obj is eligible for garbage collection
      // The cleanup callback will be triggered when obj is collected
  5. Top-Level Await

    • Definition: Top-level await allows await to be used at the top level of modules, simplifying asynchronous code without requiring async functions.
    • Example:
      // In an ES module
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      console.log(data);
  6. Array Methods (at, findLast, findLastIndex)

    • Definition: New array methods at, findLast, and findLastIndex enhance array manipulation by allowing easier access to elements from the end and finding elements based on conditions.
    • Example:
      const arr = [10, 20, 30, 40, 50];
       
      console.log(arr.at(-1)); // Output: 50 (last element)
      console.log(arr.findLast(x => x < 40)); // Output: 30 (last element less than 40)
      console.log(arr.findLastIndex(x => x < 40)); // Output: 2 (index of last element less than 40)
  7. Class Fields

    • Definition: Class fields allow defining instance fields directly within class bodies, including private fields for better encapsulation.
    • Example:
      class MyClass {
        publicField = 'Public Field';
        #privateField = 'Private Field';
       
        getPrivateField() {
          return this.#privateField;
        }
      }
       
      const instance = new MyClass();
      console.log(instance.publicField); // Output: Public Field
      console.log(instance.getPrivateField()); // Output: Private Field
  8. Private Methods and Accessors

    • Definition: Private methods and properties, indicated by #, are used within classes to provide encapsulation and hide internal details from outside access.

    • Example:

      class MyClass {
        #privateMethod() {
          return 'This is a private method';
        }
       
        publicMethod() {
          return this.#privateMethod();
        }
      }
       
      const instance = new MyClass();
      console.log(instance.publicMethod()); // Output: This is a private method
      // console.log(instance.#privateMethod()); // SyntaxError: Private field '#privateMethod' must be declared in an enclosing class
  9. WeakMap and WeakSet Enhancements

    • Definition: Enhancements to WeakMap and WeakSet improve memory management by ensuring objects can be garbage-collected when no longer needed.
    • Example:
      const weakMap = new WeakMap();
      let key = {};
      weakMap.set(key, 'value');
      console.log(weakMap.get(key)); // Output: value
      key = null; // key is eligible for garbage collection
  10. Logical Nullish Assignment

    • Definition: Logical nullish assignment combines the nullish coalescing operator with assignment to simplify setting default values.
    • Example:
      let foo = null;
      foo ??= 'default'; // foo = foo ?? 'default'
      console.log(foo); // Output: default
       
      let bar = 'value';
      bar ??= 'default'; // bar remains 'value'
      console.log(bar); // Output: value

These features from ES6 and ES14 showcase JavaScript’s evolution, aiming to make code more efficient, readable, and powerful. Understanding these features can significantly enhance your coding practices and keep you up-to-date with modern JavaScript development.