Javascript

Javascript

JavaScript Core Concepts

Hey there! Whether you're brushing up on your JavaScript skills or just getting back into coding after a break, it's always helpful to revisit the fundamentals. JavaScript can sometimes feel like a quirky friend who's full of surprises, but once you understand the basics, it becomes a lot easier to work with. So, without further ado, let’s go over some of the core concepts of JavaScript.

1. Variables and Data Types

Declaring Variables

So, in JavaScript, we’ve got three ways to declare variables: var, let, and const. Nowadays, you’ll mostly stick with let and const (trust me, nobody wants to deal with the quirks of var anymore).

  • let: We Use this when we know the value of our variable is going to change.
  • const: This one’s for the stuff we’re sure won’t change (constants, obviously).
let score = 10;
const name = "Chan";


Data Types

JavaScript keeps it simple when it comes to types, but it’s still important to know the basics:

  • Primitives like String, Number, Boolean, Undefined, Null, Symbol, and BigInt.
  • Non-Primitives (Also called Reference Types) like Objects which include everything else like Array, Function, and, well, Object itself.
  • Primitives are immutable a.k.a read-only (cannot be changed), while non-primitives are mutable.
    • When a variable holds one of these primitive types, we can’t modify the value itself. We can only reassign that variable to a new value.
    • You can read more on this A Visual Guide to References in JavaScript. (I assure you, you will be able to visualize things easily after you read that).
  • Primitives are stored directly in memory, while non-primitives are stored as references to locations in memory.
  • Primitives are compared by value (e.g., 5 === 5 is true), while non-primitives are compared by reference (e.g., two objects with the same properties are not equal unless they refer to the same location in memory).

let age = 25;             // Number
let isStudent = true;     // Boolean
let userName = "Abilene"; // String
let car = {               // Object
    make: "Toyota",
    model: "Camry"
};

Think of primitives as the simple stuff, and objects as the more complex, custom-built stuff.

2. Functions

Function Declaration

Functions are pretty much the core of any JavaScript code. We write them to reuse code and keep things organized. Here’s a classic way to write one:

function greet(name) {
    return `Hello, ${name}!`;
}
console.log(greet("Chan"));  // Output: Hello, Chan!


Arrow Functions

These are like the cool, modern way to write functions, especially when we want a shorter syntax or when we want this to behave a bit differently.

const greet = (name) => `Hello, ${name}!`;
console.log(greet("Abilene"));  // Output: Hello, Abilene!


If you're ever in doubt: when in doubt, start with an arrow function.

3. Scope and Closures

Scope

We can think of scope like different “rooms” in our code. Some variables only exist inside certain rooms, while others can be accessed from anywhere in the house. With let and const, we get block scope—meaning variables are only available inside the block (curly braces) where they’re declared.

function showMessage() {
    let message = "Hello";  // Block-scoped variable
    console.log(message);
}
console.log(message);  // Error: message is not defined


Closures

Closures sound fancy, but they’re actually pretty straightforward. It’s when a function remembers the variables outside of its scope, even after the outer function has finished running.

function outer() {
    let outerVar = "I'm from the outer function!";
    
    function inner() {
        console.log(outerVar); // Can access outerVar
    }
    return inner;
}
const innerFunc = outer();
innerFunc();  // Output: I'm from the outer function!


Closures are super useful when we need a function to “remember” things.

4. Objects and Prototypes

Objects

Objects are basically JavaScript's way of creating a structure for data. They’re just collections of key-value pairs. You’ll use them all the time for everything from user data to configuration settings.

const person = {
    name: "Chan",
    age: 25,
    greet: function() {
        return `Hi, I'm ${this.name}`;
    }
};
console.log(person.greet());  // Output: Hi, I'm Chan


Prototypes

Every object in JavaScript has a prototype, which is like an invisible “template” that allows it to inherit properties or methods from other objects.

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.greet = function() {
    return `Hi, I'm ${this.name}`;
};
const chan = new Person("Chan", 25);
console.log(chan.greet());  // Output: Hi, I'm Chan


Prototypes can feel a bit abstract, but they're just the backbone of how inheritance works in JavaScript.

5. The this Keyword

Ah, the infamous this keyword! It’s one of those things that can trip us up if we’re not careful. But it’s really just a reference to the object that’s currently executing the code. It can refer to different things depending on how a function is called.

const car = {
    make: "Toyota",
    start: function() {
        console.log(this.make);  // 'this' refers to the 'car' object
    }
};
car.start();  // Output: Toyota


With arrow functions, this doesn’t change like it normally would in regular functions, so it can save us some headaches in certain situations.

6. Asynchronous JavaScript

Callbacks

Callbacks are functions passed into other functions to be executed later, usually when some asynchronous task finishes (like loading data).

function loadData(callback) {
    setTimeout(() => {
        callback("Data loaded!");
    }, 1000);
}
loadData((data) => {
    console.log(data);  // Output: Data loaded! (after 1 second)
});


Promises

Promises make asynchronous code a bit easier to handle by giving us a structured way to deal with success or failure.

const fetchData = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data fetched!");
    }, 1000);
});

fetchData
    .then(data => console.log(data))  // Output: Data fetched!
    .catch(error => console.log(error));


Async/Await

If we want to avoid the whole “callback hell” situation, async/await is the modern, readable way to handle asynchronous code.

async function loadData() {
    const result = await fetchData();
    console.log(result);  // Output: Data fetched!
}
loadData();


7. Event Loop and Concurrency

JavaScript’s event loop is what lets it be single-threaded but still handle asynchronous tasks like a boss. Whenever we have async stuff like setTimeout, the event loop steps in and processes things in the background while our code continues to run.

console.log("Start");

setTimeout(() => {
    console.log("Async operation completed");
}, 1000);

console.log("End");

// Output: 
// Start
// End
// Async operation completed (after 1 second)


So, JavaScript may be single-threaded, but it’s super efficient at juggling different tasks.

8. Error Handling

Try/Catch

Mistakes happen! That’s why JavaScript gives us try/catch to handle errors gracefully.

try {
    let result = riskyOperation();
} catch (error) {
    console.error("An error occurred:", error);
}


Throwing Errors

We can also throw our own errors if we want to stop the program when something goes wrong.

function checkAge(age) {
    if (age < 18) {
        throw new Error("Age must be at least 18");
    }
    return "Access granted";
}

try {
    checkAge(16);
} catch (error) {
    console.log(error.message);  // Output: Age must be at least 18
}

9. Higher-Order Functions

So, what’s the deal with Higher-Order Functions? Sounds fancy, right? But it’s actually pretty simple. A higher-order function is just a function that either:

  1. Takes another function as an argument, or
  2. Returns a function as its result.

Imagine functions as those awesome tools in your toolbox. Now, picture a super tool that can take other tools as input and even spit out new, customized tools. That's a higher-order function in JavaScript! 🤯

// Higher-order function that takes another function as an argument

function doMath(operation, a, b) {
    return operation(a, b);
}

// Functions that we pass in as arguments

function add(x, y) {
    return x + y;
}

function multiply(x, y) {
    return x * y;
}

// Using the higher-order function

console.log(doMath(add, 3, 4));        // Outputs: 7
console.log(doMath(multiply, 3, 4));   // Outputs: 12


Why use them?
They make our code more flexible and reusable. Functions like map, filter, and reduce are classic examples of higher-order functions. They allow us to perform powerful operations on arrays with minimal code.

10. Pure Functions

Next up, we have Pure Functions. Think of pure functions like the perfect guests at a party: they don’t bring any drama, and they don’t mess with anyone else’s stuff. A pure function is a function that:

  • Always returns the same output given the same input.
  • Doesn’t modify anything outside of its own scope (no side effects).
// Pure function

function pureAdd(a, b) {
    return a + b;
}

// Impure function (modifies a global variable)

let total = 0;
function impureAdd(a) {
    total += a;  // This modifies 'total', making it impure
}


Pure functions are predictable and easy to test, making them a core concept of functional programming. They don’t rely on anything but their inputs, which makes them super reliable.

11. Deep Copy vs Shallow Copy

Alright, here’s where things get a bit tricky: Deep Copy vs Shallow Copy. If you've ever worked with objects or arrays in JavaScript, you've probably run into a situation where you thought you copied something, only to find out later that both the original and the copy are changing. 😱

That’s because JavaScript objects (and arrays) are reference types, meaning that when we copy them, we're often just copying the reference, not the actual data. But the depth of that copy matters!

Shallow Copy

A shallow copy only copies the top-level properties. If the object has nested objects, only the reference to those objects gets copied.

const original = { name: "Chan", address: { city: "Earth" } };
const shallowCopy = { ...original };

shallowCopy.name = "John";
shallowCopy.address.city = "Mars";

console.log(original.name);       // Chan (no change, good!)
console.log(original.address.city);  // Mars (uh oh, nested object changed!)

See what happened there? We copied original, but since the address object is nested, both original and shallowCopy point to the same address object. So when we changed the city in shallowCopy, it also changed in original.

Deep Copy

A deep copy means copying not just the top-level properties, but also all nested objects. In other words, we’re creating an entirely new, independent copy of the object.

const original = { name: "Chan", address: { city: "Earth" } };
const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.address.city = "Mars";

console.log(original.address.city);  // Earth (yay, no change this time!)


By using JSON.parse(JSON.stringify()), we made a deep copy of the object. Now, changing the deepCopy won’t affect the original object.

When to Use Shallow or Deep Copy?

  • Shallow Copy: Fine when we don’t have nested objects (or when we don’t care if they get changed).
  • Deep Copy: Needed when we have complex objects and want to ensure that changes to the copy don’t affect the original.

Wrapping It Up

JavaScript is full of cool stuff, and the more we get comfortable with these concepts, the easier it becomes to write clean, efficient, and bug-free code. Happy coding! 🚀


Thanks for reading. Wish you a wonderful day. 😎