💼🦋XGitHub ProfileYouTube ChannelInstagramEmailSpotify

The JavaScript Language

Welcome to my JavaScript resource! This is a space where I share what I've been learning about the JavaScript language. As I continue to explore and grow my knowledge, I'll be posting insights, tips, and concepts that I've picked up along the way. I hope you find it helpful and informative!

Variables and Constants

JavaScript provides `var`, `let`, and `const` for declaring variables. Use `let` and `const` for modern JavaScript.

// Example
let name = "Chris";
const age = 25;
var isDeveloper = true;

// const cannot be reassigned
const PI = 3.14159;
// PI = 3.14; // Error!

// let can be reassigned
let score = 0;
score = 100; // OK!

Resources:

Data Types

JavaScript has 7 primitive types and 1 non-primitive type (Object). Understanding types is crucial for writing bug-free code.

// Primitive Types
const str = "Hello";              // String
const num = 42;                   // Number
const bool = true;                // Boolean
const nothing = null;             // Null
const notDefined = undefined;     // Undefined
const sym = Symbol('id');         // Symbol
const bigNum = 9007199254740991n; // BigInt

// Non-primitive (Reference Type)
const obj = { name: "Chris" };    // Object
const arr = [1, 2, 3];           // Array (type of Object)
const func = () => {};           // Function (type of Object)

// Type checking
console.log(typeof str);         // "string"
console.log(Array.isArray(arr)); // true

Resources:

Arrow Functions

Arrow functions provide a concise syntax for writing functions. They also do not bind their own `this`.

// Traditional function
function add(a, b) {
  return a + b;
}

// Arrow function
const add = (a, b) => a + b;

// With single parameter (no parentheses needed)
const double = x => x * 2;

// With no parameters
const getRandom = () => Math.random();

// Multi-line arrow function
const processUser = (user) => {
  const name = user.name.toUpperCase();
  const age = user.age + 1;
  return { name, age };
};

// 'this' binding difference
const obj = {
  name: "Chris",
  greet: function() {
    setTimeout(() => {
      console.log(this.name); // 'Chris' - arrow function uses parent's this
    }, 1000);
  }
};

Arrays and Array Methods

Arrays are ordered collections of values. JavaScript provides powerful methods to manipulate arrays without mutating the original.

// Creating arrays
const fruits = ['apple', 'banana', 'orange'];
const numbers = [1, 2, 3, 4, 5];

// Common array methods
// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter - keep elements that pass a test
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

// reduce - reduce array to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

// find - get first element that passes test
const found = fruits.find(f => f.startsWith('b'));
console.log(found); // 'banana'

// includes, some, every
console.log(fruits.includes('apple')); // true
console.log(numbers.some(n => n > 4)); // true
console.log(numbers.every(n => n < 10)); // true

// Destructuring arrays
const [first, second, ...rest] = fruits;
console.log(first); // 'apple'
console.log(rest); // ['orange']

Objects and Object Methods

Objects are collections of key-value pairs. They're the building blocks of JavaScript applications.

// Creating objects
const user = {
  name: "Chris",
  age: 25,
  skills: ["JavaScript", "CSS", "HTML"],
  greet() {
    return `Hi, I'm ${this.name}`;
  }
};

// Accessing properties
console.log(user.name);        // "Chris"
console.log(user['age']);      // 25
console.log(user.greet());     // "Hi, I'm Chris"

// Object destructuring
const { name, age, city = "Kampala" } = user;
console.log(city); // "Kampala" (default value)

// Object methods
const keys = Object.keys(user);      // ['name', 'age', 'skills', 'greet']
const values = Object.values(user);  // ['Chris', 25, [...], function]
const entries = Object.entries(user); // [['name', 'Chris'], ...]

// Spread operator
const updatedUser = { ...user, age: 26, city: "Kampala" };

// Object.assign
const clone = Object.assign({}, user);

// Optional chaining
console.log(user?.address?.street); // undefined (no error)

// Dynamic property names
const prop = "favoriteColor";
const preferences = {
  [prop]: "blue"
};

Promises and Async/Await

Promises represent eventual completion (or failure) of an asynchronous operation. Async/await makes async code look synchronous.

// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("Operation successful!");
    } else {
      reject("Operation failed!");
    }
  }, 1000);
});

// Using Promises with .then/.catch
myPromise
  .then(result => console.log(result))
  .catch(error => console.error(error));

// Async/Await syntax
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;
  } catch (error) {
    console.error("Failed to fetch user:", error);
    throw error;
  }
}

// Multiple promises
async function fetchMultiple() {
  // Wait for all promises
  const [users, posts] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);
  
  return { users, posts };
}

// Promise.race - resolves with first settled promise
const fastest = await Promise.race([
  fetch('/api/fast'),
  fetch('/api/slow')
]);

Destructuring Assignment

Destructuring allows you to unpack values from arrays or properties from objects into distinct variables. It makes code cleaner and more readable.

// Array destructuring
const colors = ['red', 'green', 'blue', 'yellow'];
const [primary, secondary, ...others] = colors;
console.log(primary);   // 'red'
console.log(others);    // ['blue', 'yellow']

// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b);      // 2, 1

// Object destructuring
const person = {
  name: 'Chris',
  age: 25,
  location: {
    city: 'Kampala',
    country: 'Uganda'
  }
};

// Basic destructuring with renaming
const { name: userName, age } = person;
console.log(userName);  // 'Chris'

// Nested destructuring
const { location: { city, country } } = person;
console.log(city);      // 'Kampala'

// Default values
const { theme = 'dark' } = person;
console.log(theme);     // 'dark'

// Function parameter destructuring
function greet({ name, age = 18 }) {
  return `${name} is ${age} years old`;
}

greet({ name: 'Chris', age: 25 }); // 'Chris is 25 years old'

// Rest in object destructuring
const { name, ...details } = person;
console.log(details);   // { age: 25, location: {...} }

Template Literals

Template literals provide an easy way to interpolate variables and expressions into strings. They also support multi-line strings.

// Basic string interpolation
const name = "Chris";
const age = 25;
const message = `Hello, my name is ${name} and I'm ${age} years old.`;
console.log(message);

// Multi-line strings
const multiline = `
  This is a multi-line string.
  It preserves line breaks and
  makes formatting much easier!
`;

// Expression interpolation
const items = ['apple', 'banana', 'orange'];
console.log(`You have ${items.length} items in your cart.`);
console.log(`Total: $${(19.99 * 1.15).toFixed(2)}`);

// Tagged templates
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i] || '';
    return result + str + `<mark>${value}</mark>`;
  }, '');
}

const highlighted = highlight`Hello ${name}, you are ${age} years old`;
// "Hello <mark>Chris</mark>, you are <mark>25</mark> years old"

// Nesting templates
const nested = `User: ${name} (${age > 18 ? `Adult: ${age}` : 'Minor'})`;

// Raw strings
const path = String.raw`C:\Users\Chris\Documents`;
console.log(path); // C:\Users\Chris\Documents (backslashes preserved)

Spread and Rest Operators

The spread operator (...) expands iterables into individual elements, while rest collects multiple elements into an array.

// Spread with arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// Copy array (shallow)
const original = [1, 2, 3];
const copy = [...original];

// Spread with objects
const user = { name: 'Chris', age: 25 };
const location = { city: 'Kampala', country: 'Uganda' };
const profile = { ...user, ...location, age: 26 }; // age overwritten

// Rest parameters in functions
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15

// Rest in destructuring
const [first, ...remaining] = [1, 2, 3, 4, 5];
console.log(first);     // 1
console.log(remaining); // [2, 3, 4, 5]

// Practical examples
// Convert NodeList to Array
const divs = [...document.querySelectorAll('div')];

// Max/Min of array
const scores = [89, 94, 76, 81];
const highest = Math.max(...scores); // 94

// Clone and modify
const updateUser = (user, updates) => ({
  ...user,
  ...updates,
  updatedAt: new Date()
});

Closures

A closure gives you access to an outer function's scope from an inner function. It's created when a function is defined, not when it's executed.

// Basic closure example
function outer(x) {
  // Inner function has access to outer's variables
  return function inner(y) {
    return x + y;
  };
}

const addFive = outer(5);
console.log(addFive(3)); // 8
console.log(addFive(7)); // 12

// Private variables with closures
function createCounter() {
  let count = 0; // Private variable
  
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2
// console.log(count); // Error! count is not accessible

// Common pitfall - loops and closures
// Wrong way
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // Prints 3, 3, 3
}

// Correct way - using let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // Prints 0, 1, 2
}

// Or using IIFE
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(() => console.log(index), 100); // Prints 0, 1, 2
  })(i);
}

Resources:

The 'this' Keyword

The 'this' keyword refers to the object that is executing the current function. Its value depends on how the function is called.

// 'this' in different contexts

// 1. Global context
console.log(this); // window (in browser)

// 2. Object method
const user = {
  name: 'Chris',
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
};
user.greet(); // "Hello, I'm Chris"

// 3. Constructor function
function Person(name) {
  this.name = name;
}
const person = new Person('Chris');
console.log(person.name); // 'Chris'

// 4. Arrow functions (inherit 'this')
const obj = {
  name: 'Chris',
  regularFunc: function() {
    console.log(this.name); // 'Chris'
    
    const arrowFunc = () => {
      console.log(this.name); // 'Chris' (inherited)
    };
    arrowFunc();
  }
};

// 5. Explicit binding - call, apply, bind
function introduce(greeting) {
  console.log(`${greeting}, I'm ${this.name}`);
}

const user1 = { name: 'Chris' };
const user2 = { name: 'Jane' };

introduce.call(user1, 'Hi');        // "Hi, I'm Chris"
introduce.apply(user2, ['Hello']);  // "Hello, I'm Jane"

const boundFunc = introduce.bind(user1);
boundFunc('Hey'); // "Hey, I'm Chris"

// Common mistake
const button = {
  text: 'Click me',
  click() {
    console.log(this.text);
  }
};

// setTimeout(button.click, 1000); // undefined - 'this' is lost
setTimeout(() => button.click(), 1000); // 'Click me' - works!

Classes

ES6 classes provide a cleaner syntax for creating objects and implementing inheritance. They're syntactic sugar over JavaScript's prototype-based inheritance.

// Basic class syntax
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // Method
  greet() {
    return `Hi, I'm ${this.name}`;
  }
  
  // Getter
  get info() {
    return `${this.name} is ${this.age} years old`;
  }
  
  // Setter
  set nickname(value) {
    this._nickname = value;
  }
  
  // Static method
  static species() {
    return 'Homo sapiens';
  }
}

const chris = new Person('Chris', 25);
console.log(chris.greet()); // "Hi, I'm Chris"
console.log(chris.info);    // "Chris is 25 years old"
console.log(Person.species()); // "Homo sapiens"

// Inheritance
class Developer extends Person {
  constructor(name, age, language) {
    super(name, age); // Call parent constructor
    this.language = language;
  }
  
  greet() {
    return `${super.greet()} and I code in ${this.language}`;
  }
  
  code() {
    return `${this.name} is coding in ${this.language}`;
  }
}

const dev = new Developer('Chris', 25, 'JavaScript');
console.log(dev.greet()); // "Hi, I'm Chris and I code in JavaScript"
console.log(dev.code());  // "Chris is coding in JavaScript"

// Private fields (ES2022)
class BankAccount {
  #balance = 0; // Private field
  
  deposit(amount) {
    this.#balance += amount;
  }
  
  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100
// console.log(account.#balance); // Error! Private field

Resources:

Modules (ES6)

Modules allow you to break up your code into separate files. Each module has its own scope and can export/import functionality.

// math.js - Named exports
export const PI = 3.14159;
export function add(a, b) {
  return a + b;
}
export function multiply(a, b) {
  return a * b;
}

// main.js - Named imports
import { PI, add, multiply } from './math.js';
console.log(PI);           // 3.14159
console.log(add(2, 3));    // 5

// Alternative import syntax
import * as math from './math.js';
console.log(math.PI);      // 3.14159
console.log(math.add(2, 3)); // 5

// user.js - Default export
export default class User {
  constructor(name) {
    this.name = name;
  }
}

// Can also export like this
const User = class { /* ... */ };
export default User;

// app.js - Default import
import User from './user.js';
const user = new User('Chris');

// Mixed exports
// utils.js
export default function log(message) {
  console.log(message);
}
export const VERSION = '1.0.0';
export const API_URL = 'https://api.example.com';

// Using mixed imports
import log, { VERSION, API_URL } from './utils.js';

// Dynamic imports
async function loadModule() {
  const module = await import('./heavy-module.js');
  module.doSomething();
}

// Re-exporting
// index.js
export { add, multiply } from './math.js';
export { default as User } from './user.js';

Resources:

Error Handling

Proper error handling is crucial for robust applications. JavaScript provides try...catch for synchronous code and Promise methods for async errors.

// Basic try...catch
try {
  // Code that might throw an error
  const data = JSON.parse('invalid json');
} catch (error) {
  console.error('Parsing failed:', error.message);
} finally {
  console.log('This always runs');
}

// Throwing custom errors
function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero!');
  }
  return a / b;
}

try {
  const result = divide(10, 0);
} catch (error) {
  console.error(error.message); // "Division by zero!"
}

// Custom error classes
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

function validateEmail(email) {
  if (!email.includes('@')) {
    throw new ValidationError('Invalid email format');
  }
}

// Async error handling
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error; // Re-throw if needed
  }
}

// Promise error handling
fetchData()
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// Global error handling
window.addEventListener('error', (event) => {
  console.error('Global error:', event.error);
});

window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
});

Map and Set

Map and Set are collection types introduced in ES6. Map holds key-value pairs where keys can be any type, while Set stores unique values.

// Map - key-value pairs with any type of key
const map = new Map();

// Adding entries
map.set('name', 'Chris');
map.set(42, 'The answer');
map.set(true, 'Boolean key');
const objKey = { id: 1 };
map.set(objKey, 'Object as key!');

// Getting values
console.log(map.get('name'));    // 'Chris'
console.log(map.get(42));        // 'The answer'
console.log(map.get(objKey));    // 'Object as key!'

// Map methods
console.log(map.has('name'));    // true
console.log(map.size);           // 4
map.delete(42);
// map.clear(); // Remove all entries

// Iterating Map
for (const [key, value] of map) {
  console.log(`${key} => ${value}`);
}

// Creating Map from array
const userMap = new Map([
  ['name', 'Chris'],
  ['age', 25],
  ['city', 'Kampala']
]);

// Set - collection of unique values
const set = new Set();

// Adding values
set.add(1);
set.add(2);
set.add(2); // Duplicate, won't be added
set.add('hello');

console.log(set.size); // 3
console.log(set.has(2)); // true

// Creating Set from array (removes duplicates)
const numbers = [1, 2, 2, 3, 3, 3, 4];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3, 4]

// Set operations
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);

// Union
const union = new Set([...setA, ...setB]); // {1, 2, 3, 4, 5}

// Intersection
const intersection = new Set(
  [...setA].filter(x => setB.has(x))
); // {3}

// Difference
const difference = new Set(
  [...setA].filter(x => !setB.has(x))
); // {1, 2}