JavaScript Hard Parts. Overview
Functional JavaScript: Higher-order functions, Closure, Currying, Nested Function Scope, Retaining Function Memory, Arrow Functions.
Asynchronicity in JavaScript: Callback Functions, Promises, Async/Await, Event Loop.
OOP and Classes in JavaScript: Constructor, Object, Prototype.
Other Concepts: Factory Function, Prototype Chain, hasOwnProperty.
Functional JavaScript
Key Concepts:
- Higher-order functions are functions that either take one or more functions as arguments or return a function.
// Example 1: Function that takes a callback
const operateOnNumbers = (a, b, operation) => operation(a, b);
const add = (x, y) => x + y;
const multiply = (x, y) => x * y;
console.log(operateOnNumbers(5, 3, add)); // Output: 8
console.log(operateOnNumbers(5, 3, multiply)); // Output: 15
// Example 2: Array higher-order functions
const numbers = [1, 2, 3, 4, 5];
const squaredNumbers = numbers.map(x => x * x);
console.log(squaredNumbers); // Output: [1, 4, 9, 16, 25]
const evenNumbers = numbers.filter(x => x % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]
const sum = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // Output: 15
- Closure is the combination of a function and the lexical environment within which that function was declared. This allows the function to access variables from its outer scope even after that scope has finished execution.
function outerFunction(x) {
// Inner function is a closure because it "closes over" the variable x
function innerFunction(y) {
return x + y;
}
return innerFunction;
}
// Create a closure by calling outerFunction with argument 10
const closure = outerFunction(10);
// Use the closure to access the variable x from the outer scope
const result = closure(5);
console.log(result); // Output: 15
- Currying is a technique in functional programming where a function is transformed into a sequence of functions, each taking a single argument. The curried function returns a new function with each argument until all arguments are provided, and the final result is produced.
// Non-curried function
function add(x, y, z) {
return x + y + z;
}
console.log(add(1, 2, 3)); // Output: 6
// Curried version of the same function
function curriedAdd(x) {
return function(y) {
return function(z) {
return x + y + z;
};
};
}
console.log(curriedAdd(1)(2)(3)); // Output: 6
- Nested Function Scope:
In JavaScript, a nested function is a function declared within another function. This creates a scope chain, where the inner function has access to its own scope as well as the scope of the outer function. This concept is known as “nested function scope.”
function outerFunction() {
let outerVariable = "I am from outer scope";
function innerFunction() {
let innerVariable = "I am from inner scope";
console.log(outerVariable); // Access outer scope variable
console.log(innerVariable); // Access inner scope variable
}
innerFunction();
}
outerFunction();
- Retaining Function Memory:
When a function is defined inside another function, it forms a closure. A closure allows the inner function to “remember” and retain access to the variables and parameters of its outer function, even after the outer function has finished executing. This retention of memory is what’s commonly referred to as “retaining function memory.”
function outerFunction(x) {
// Inner function is a closure, retaining memory of 'x'
function innerFunction(y) {
return x + y;
}
return innerFunction;
}
// Create a closure by calling outerFunction with argument 10
const closure = outerFunction(10);
// The closure retains memory of 'x' from its outer scope
const result = closure(5);
console.log(result); // Output: 15
- Arrow Functions in JavaScript behave differently with regard to the
this
keyword compared to regular functions. The key distinction is that arrow functions do not have their ownthis
context; instead, they inheritthis
from the surrounding code (lexical scoping). This can impact howthis
behaves in different situations.
function RegularFunction() {
this.value = 1;
// Regular function with its own 'this' context
setTimeout(function() {
this.value++;
console.log('Inside regular function:', this.value); // 'this' is not the same as the outer 'this'
}, 1000);
// Arrow function inheriting 'this' from the surrounding code
setTimeout(() => {
this.value++;
console.log('Inside arrow function:', this.value); // 'this' is the same as the outer 'this'
}, 2000);
}
const obj = new RegularFunction(); // Assuming used as a constructor
Asynchronicity in JavaScript
Asynchronicity refers to the ability to execute operations independently of the main program flow, allowing non-blocking code execution. This is crucial for handling tasks that might take time, such as fetching data from a server, reading a file, or making network requests, without freezing the entire application.
Key Concepts:
- Callback Functions: In the early days of JavaScript, callbacks were the primary mechanism for handling asynchronous operations. Functions are passed as arguments to other functions and executed upon the completion of the asynchronous task.
fetchDataFromServer((data) => {
console.log(data);
});
- Promises: Introduced in ECMAScript 6 (ES6), promises provide a more structured way to deal with asynchronous code. They represent the eventual completion or failure of an asynchronous operation and offer methods like
then
andcatch
for handling success and errors.
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation or computation
// If successful, call resolve with the result
// If unsuccessful, call reject with an error
});
myPromise
.then(result => {
// Handle the resolved value
})
.catch(error => {
// Handle the rejected error
});
- Async/Await: A syntax sugar built on top of promises,
async/await
makes asynchronous code look more like synchronous code, enhancing readability. Theasync
keyword is used to define an asynchronous function, andawait
is used to pause execution until the promise is resolved.
async function fetchData() {
try {
const data = await fetchFromServer();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
- Event Loop: JavaScript’s event loop is the mechanism that enables asynchronous execution. It continually checks the message queue for pending messages (events) and processes them one at a time, allowing the main thread to remain responsive.
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 1000);
console.log('End');
OOP and Classes in JavaScript
Object-Oriented Programming is a programming paradigm that uses objects — bundles of related data and methods — to design and structure code. In JavaScript, OOP is achieved through the use of objects, prototypes, and constructors.
Key Concepts:
- Contructor is a special method that is called when an object is created from a class. It is used to initialize the object’s properties and set up its initial state. Constructors are typically defined within class declarations or expressions.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
}
const john = new Person('John', 155);
john.greet(); // Output: Hello, my name is John and I'm 155 years old.
- Object is a fundamental data type that represents a collection of key-value pairs, where each key is a string (or a symbol in ECMAScript 6+) and each value can be of any data type, including other objects. Objects are used to model and represent real-world entities and concepts in the code.
// Object Literal:
const person = {
name: 'John',
age: 30,
address: {
city: 'Example City',
country: 'Example Country'
},
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
// Using the new keyword and a Constructor Function:
function Person(name, age) {
this.name = name;
this.age = age;
}
const john = new Person('John', 30);
// Object.create:
const car = Object.create(null);
car.make = 'Toyota';
car.model = 'Camry';
console.log(car.make); // Output: Toyota
console.log(car['model']); // Output: Camry
- Prototype is an internal property of objects that is used for inheritance. Each object in JavaScript has a prototype, and it is through this prototype chain that objects inherit properties and methods from other objects.
// Constructor function
function Animal(name) {
this.name = name;
}
// Adding a method to the prototype
Animal.prototype.sound = function() {
console.log('Some generic sound');
};
// Creating instances
const cat = new Animal('Whiskers');
const dog = new Animal('Buddy');
// Accessing properties and methods
console.log(cat.name); // Output: Whiskers
cat.sound(); // Output: Some generic sound
console.log(dog.name); // Output: Buddy
dog.sound(); // Output: Some generic sound
Other Concepts:
- Factory Function in JavaScript is a function that produces objects. Instead of using a constructor with the
new
keyword, a factory function is used to encapsulate the process of creating and initializing an object. This approach provides more control over the creation process and allows for the creation of multiple instances with different initializations.
// Factory function to create person objects
function createPerson(name, age) {
// Object to be returned
const person = {};
// Properties
person.name = name;
person.age = age;
// Method
person.sayHello = function() {
console.log(`Hello, my name is ${person.name} and I'm ${person.age} years old.`);
};
return person;
}
// Creating person objects using the factory function
const john = createPerson('John', 25);
const jane = createPerson('Jane', 30);
// Accessing properties and methods
john.sayHello(); // Output: Hello, my name is John and I'm 25 years old.
jane.sayHello(); // Output: Hello, my name is Jane and I'm 30 years old.
- Prototype Chain in JavaScript is a mechanism that allows objects to inherit properties and methods from other objects by forming a chain of prototypes. Each object has a prototype, and if a property or method is not found on the object itself, JavaScript looks up the chain to find it in the object’s prototype, and so on.
// Parent class
class Animal {
constructor(name) {
this.name = name;
}
sound() {
console.log('Some generic sound');
}
}
// Child class inheriting from Animal
class Dog extends Animal {
constructor(name, breed) {
// Calling the parent constructor using super
super(name);
this.breed = breed;
}
bark() {
console.log('Woof! Woof!');
}
}
// Creating an instance
const myDog = new Dog('Buddy', 'Golden Retriever');
// Accessing properties and methods
console.log(myDog.name); // Output: Buddy
myDog.sound(); // Output: Some generic sound
myDog.bark(); // Output: Woof! Woof!
hasOwnProperty
is a method available on objects in JavaScript. It is used to check whether an object has a property with a specified name as a direct property of that object (not inherited from its prototype chain).
const myObject = {
name: 'John',
age: 25,
};
console.log(myObject.hasOwnProperty('name')); // Output: true
console.log(myObject.hasOwnProperty('age')); // Output: true
console.log(myObject.hasOwnProperty('gender')); // Output: false