In this repository, I will be talking about JavaScript and its features. I will be covering the following topics:
- Data types in JavaScript
- Strings in JavaScript
- Declaring Objects in JS
- Copy Objects in JS
- Declaring Functions in js
- Types of functions in JS
- Functions are First Class Objects
- JS is single-threaded programming language
- Callback Functions
- Higher Order Functions
- JS built-in HOF
- Promises in js
- Callback Hell and how to avoid it
Examples of primitive and non-primitive data types in JavaScript:
-
Primitive data types
-
Non-primitive data types
Primitive Data Types Non-Primitive Data Types String Object Number Array Boolean Function Null Date Undefined Set Symbol Map - Boolean: A boolean represents a logical value of either
true
orfalse
.
let isRaining = true; // boolean value console.log(typeof isRaining); // "boolean"
- Number: A number represents a numeric value, including integers and floating-point numbers.
let age = 30; // integer let price = 9.99; // floating-point number console.log(typeof age); // "number" console.log(typeof price); // "number"
- String: A string represents a sequence of characters.
let message = "Hello, world!"; // string console.log(typeof message); // "string"
- Undefined: A variable that has been declared but has not been assigned a value is
undefined
.
let firstName; // undefined console.log(typeof firstName); // "undefined"
- Null: A variable that is explicitly assigned the value
null
represents an intentional absence of any object value.
let middleName = null; // null console.log(typeof middleName); // "object"
- Symbol: A symbol represents a unique identifier.
const id = Symbol("id"); // symbol console.log(typeof id); // "symbol"
- Object: An object is a collection of key-value pairs and represents a complex entity or data structure.
const person = { firstName: "John", lastName: "Doe", age: 30, hobbies: ["reading", "coding", "traveling"], }; // object console.log(typeof person); // "object"
- Array: An array is an ordered list of values.
const numbers = [1, 2, 3, 4, 5]; // array console.log(typeof numbers); // "object"
- Function: A function is a reusable block of code that performs a specific task.
function greet(name) { console.log(`Hello, ${name}!`); } greet("Alice"); // logs "Hello, Alice!" greet("Bob"); // logs "Hello, Bob!"
- Date: A date represents a specific moment in time.
const now = new Date(); // current date and time console.log(typeof now); // "object"
- Set: A set is a collection of unique values.
const set = new Set([1, 2, 3, 4, 5]); // set console.log(typeof set); // "object"
- Map: A map is a collection of key-value pairs.
const map1 = new Map(); map1.set("a", 1); map1.set("b", 2); map1.set("c", 3); console.log(map1.get("a")); // Expected output: 1 map1.set("a", 97); console.log(map1.get("a")); // Expected output: 97 console.log(map1.size); // Expected output: 3 map1.delete("b"); console.log(map1.size); // Expected output: 2
const map = new Map([ ["a", 1], ["b", 2], ["c", 3], ]); // map console.log(map); // Map(3) {"a" => 1, "b" => 2, "c" => 3} console.log(typeof map); // "object"
Primitive Data Types Non-Primitive Data Types Immutable Mutable Passed by value Passed by reference Stored in stack Stored in heap Copied by value Copied by reference Compared by value Compared by reference Accessed by value Accessed by reference - Here are some code examples to illustrate the differences between primitive data types and non-primitive data types in JavaScript:
- Immutable vs Mutable
Primitive data types are immutable, meaning their value cannot be changed after they are created:
let a = "Hello"; a[0] = "J"; // This has no effect, a remains "Hello" console.log(a); // Output: "Hello"
Non-primitive data types are mutable, meaning their values can be changed after they are created:
let obj = { name: "John" }; obj.name = "Jane"; // Changes the value of the 'name' property console.log(obj); // Output: {name: "Jane"}
- Passed by value vs Passed by reference
Primitive data types are passed by value, meaning a copy of their value is passed to a function:
function changeValue(a) { a = 10; } let x = 5; changeValue(x); console.log(x); // Output: 5
Non-primitive data types are passed by reference, meaning a reference to their memory location is passed to a function:
function changeValue(obj) { obj.name = "Jane"; } let person = { name: "John" }; changeValue(person); console.log(person); // Output: {name: "Jane"}
- Stored in stack vs Stored in heap
Primitive data types are stored in the stack:
let a = 5;
Non-primitive data types are stored in the heap:
let obj = { name: "John" };
- Copied by value vs Copied by reference
Primitive data types are copied by value:
let a = 5; let b = a; // Copies the value of a to b b = 10; console.log(a); // Output: 5
Non-primitive data types are copied by reference:
let obj1 = { name: "John" }; let obj2 = obj1; // Copies the reference to obj1 to obj2 obj2.name = "Jane"; console.log(obj1); // Output: {name: "Jane"}
- Compared by value vs Compared by reference
Primitive data types are compared by value:
let a = 5; let b = 5; console.log(a === b); // Output: true
Non-primitive data types are compared by reference:
let obj1 = { name: "John" }; let obj2 = { name: "John" }; console.log(obj1 === obj2); // Output: false
- Accessed by value vs Accessed by reference
Primitive data types are accessed by value:
let a = 5; let b = a; // Copies the value of a to b b = 10; console.log(a); // Output: 5
Non-primitive data types are accessed by reference:
let obj = { name: "John" }; console.log(obj.name); // Output: "John"
- Boolean: A boolean represents a logical value of either
-
Strings are a sequence of characters, which can be either alphabets, numbers, special characters, or a combination of all three. They are used to represent text in a program.
-
Strings are immutable, meaning their values cannot be changed after they are created.
-
Strings are primitive data types in JavaScript, which means they are copied by value.
-
Strings are stored in the stack.
-
Strings are compared by value.
-
Strings are accessed by value.
- Strings can be declared using single quotes, double quotes, or backticks. Here are some examples:
let str1 = "Hello"; let str2 = "World"; let str3 = `Hello World`;
- The backtick is a new addition to JavaScript, and it allows you to use template literals. Template literals are enclosed by backticks, and they allow you to embed expressions inside a string. Here is an example:
let name = "John"; let str = `Hello ${name}`; console.log(str); // Output: "Hello John"
- Template literals are useful when you want to create a string that contains variables or expressions.
- You can also use backticks to create multi-line strings:
let str = `This is a multi-line string`;
- This is useful when you want to create a string that spans multiple lines.
- You can also use backticks to create strings that contain single quotes or double quotes:
let str1 = `This string contains a single quote (')`; let str2 = `This string contains a double quote (")`;
- This is useful when you want to create a string that contains single quotes or double quotes.
- You can also use backticks to create strings that contain backticks:
let str = `This string contains a backtick (\`)`;
- This is useful when you want to create a string that contains backticks.
-
Strings have many built-in methods that allow you to perform operations on them. Here are some common string methods in alphabetical order:
-
The
charAt()
method returns the character at the specified index in a string. The index of the first character is 0, the index of the second character is 1, and so on. If the index is greater than or equal to the length of the string,charAt()
returns an empty string.let str = "Hello World"; console.log(str.charAt(0)); // Output: "H" console.log(str.charAt(1)); // Output: "e" console.log(str.charAt(2)); // Output: "l" console.log(str.charAt(3)); // Output: "l" console.log(str.charAt(4)); // Output: "o" console.log(str.charAt(10)); // Output: "d" console.log(str.charAt(11)); // Output: ""
-
The
charCodeAt()
method returns the Unicode value of the character at the specified index in a string. The index of the first character is 0, the index of the second character is 1, and so on. If the index is greater than or equal to the length of the string,charCodeAt()
returns NaN.let str = "Hello World"; console.log(str.charCodeAt(0)); // Output: 72 console.log(str.charCodeAt(1)); // Output: 101 console.log(str.charCodeAt(2)); // Output: 108 console.log(str.charCodeAt(3)); // Output: 108 console.log(str.charCodeAt(4)); // Output: 111 console.log(str.charCodeAt(10)); // Output: 100 console.log(str.charCodeAt(11)); // Output: NaN
-
The
concat()
method combines two or more strings and returns a new string. This method does not change the existing strings, but instead returns a new string.let str1 = "Hello"; let str2 = "World"; console.log(str1.concat(" ", str2)); // Output: "Hello World"
-
The
endsWith()
method determines whether a string ends with the characters of a specified string. This method returns true if the string ends with the specified string, and false otherwise.let str = "Hello World"; console.log(str.endsWith("World")); // Output: true console.log(str.endsWith("World!")); // Output: false
-
The
includes()
method determines whether a string contains the characters of a specified string. This method returns true if the string contains the specified string, and false otherwise.let str = "Hello World"; console.log(str.includes("World")); // Output: true console.log(str.includes("World!")); // Output: false
-
The
indexOf()
method returns the index of the first occurrence of a specified value in a string. This method returns -1 if the value is not found.let str = "Hello World"; console.log(str.indexOf("World")); // Output: 6 console.log(str.indexOf("World!")); // Output: -1
-
The
lastIndexOf()
method returns the index of the last occurrence of a specified value in a string. This method returns -1 if the value is not found.let str = "Hello World"; console.log(str.lastIndexOf("World")); // Output: 6 console.log(str.lastIndexOf("World!")); // Output: -1
-
The
match()
method searches a string for a match against a regular expression, and returns the matches, as an Array object.const paragraph = "The quick brown fox jumps over the lazy dog. It barked."; const regex = /[A-Z]/g; const found = paragraph.match(regex); console.log(found); // Expected output: Array ["T", "I"]
-
The
matchAll()
method returns an iterator of all results matching a string against a regular expression, including capturing groups.const regexp = /t(e)(st(\d?))/g; const str = "test1test2"; const found = str.matchAll(regexp); const array = [...found]; console.log(array[0]); // Expected output: Array ["test1", "e", "st1", "1"] console.log(array[1]); // Expected output: Array ["test2", "e", "st2", "2"]
-
The
padStart()
method pads the current string with another string until the resulting string reaches the given length. The padding is applied from the start of the current string.let str = "Hello World"; console.log(str.padStart(15, " ")); // Output: " Hello World" console.log(str.padStart(15, "Hello ")); // Output: "Hello Hello World" console.log(str.padStart(15, "*")); // Output: "---**Hello World"
-
The
padEnd()
method pads the current string with another string until the resulting string reaches the given length. The padding is applied from the end of the current string.let str = "Hello World"; console.log(str.padEnd(15, " ")); // Output: "Hello World " console.log(str.padEnd(15, " World")); // Output: "Hello World World" console.log(str.padEnd(15, "*")); // Output: "Hello World---**"
-
The
repeat()
method returns a new string with a specified number of copies of the string it was called on.let str = "Hello World"; console.log(str.repeat(3)); // Output: "Hello WorldHello WorldHello World"
-
The
replace()
method searches a string for a specified value, or a regular expression, and returns a new string where the specified values are replaced.let str = "Hello World"; console.log(str.replace("World", "World!")); // Output: "Hello World!"
-
The
replaceAll()
method returns a new string with all matches of a pattern replaced by a replacement.let str = "Hello World"; console.log(str.replaceAll("World", "World!")); // Output: "Hello World!"
-
The
search()
method searches a string for a specified value, or regular expression, and returns the position of the match. The search value can be string or a regular expression pattern. This method returns -1 if no match is found.let str = "Hello World"; console.log(str.search("World")); // Output: 6 console.log(str.search("World!")); // Output: -1
-
The
slice()
method extracts a section of a string and returns it as a new string, without modifying the original string. This method takes two parameters: the start index and the end index (end index is not included).let str = "Hello World"; console.log(str.slice(0, 5)); // Output: "Hello" console.log(str.slice(6, 11)); // Output: "World"
-
Using
padStart()
withslice()
to mask a string:const fullNumber = "2034399002125581"; const last4Digits = fullNumber.slice(-4); // => "5581" const maskedNumber = last4Digits.padStart(fullNumber.length, "*"); console.log(maskedNumber); // Expected output: "---5581"
-
Using
padEnd()
withslice()
to mask a string:const fullNumber = "2034399002125581"; const last4Digits = fullNumber.slice(-4); // => "5581" const maskedNumber = last4Digits.padEnd(fullNumber.length, "*"); console.log(maskedNumber); // Expected output: "5581---
-
The
split()
method splits a string into an array of substrings, and returns the new array. This method takes two parameters: the separator and the limit.let str = "Hello World"; console.log(str.split(" ")); // Output: ["Hello", "World"] console.log(str.split(" ", 1)); // Output: ["Hello"]
-
The
startsWith()
method determines whether a string begins with the characters of a specified string. This method returns true if the string begins with the specified string, and false otherwise.let str = "Hello World"; console.log(str.startsWith("Hello")); // Output: true console.log(str.startsWith("Hello!")); // Output: false
-
The
substr()
method returns a portion of the string, starting at the specified index and extending for a given number of characters afterwards. This method takes two parameters: the start index and the length.let str = "Hello World"; console.log(str.substr(0, 5)); // Output: "Hello" console.log(str.substr(6, 5)); // Output: "World"
-
The
substring()
method returns a portion of the string, starting at the specified index and extending for a given number of characters afterwards. This method takes two parameters: thestart index
and theend index
(end index is not included).let str = "Hello World"; console.log(str.substring(0, 5)); // Output: "Hello" console.log(str.substring(6, 11)); // Output: "World"
-
The
toLowerCase()
method converts a string to lowercase letters.let str = "Hello World"; console.log(str.toLowerCase()); // Output: "hello world"
-
The
toUpperCase()
method converts a string to uppercase letters.let str = "Hello World"; console.log(str.toUpperCase()); // Output: "HELLO WORLD"
-
The
trim()
method removes whitespace from both ends of a string.let str = " Hello World "; console.log(str.trim()); // Output: "Hello World"
-
The
trimStart()
method removes whitespace from the beginning of a string.let str = " Hello World "; console.log(str.trimStart()); // Output: "Hello World "
-
The
trimEnd()
method removes whitespace from the end of a string.let str = " Hello World "; console.log(str.trimEnd()); // Output: " Hello World"
-
In JavaScript, there are multiple ways to declare an object. Here are some common methods:
The most common way to create an object in JavaScript is to use an object literal. This is simply a comma-separated list of name-value pairs inside curly braces.
const person = {
name: "John",
age: 30,
address: {
street: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
},
};
Another way to create an object is to use the Object constructor. This method creates an empty object, which you can then add properties and methods to using dot notation.
const person = new Object();
person.name = "John";
person.age = 30;
person.address = {
street: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
};
The Object.create() method creates a new object, using an existing object as the prototype. This method allows you to create an object with a specific prototype, which can be useful for inheritance.
const personProto = {
greeting: function () {
console.log("Hello, my name is " + this.name);
},
};
const person = Object.create(personProto);
person.name = "John";
person.age = 30;
-
Prototype chain
is a feature of JavaScript that allows objects to inherit properties and methods from other objects. -
In JavaScript, every object has a
prototype
object, which is essentially another object from which the first object inherits properties and methods. -
When a property or method is called on an object, JavaScript first looks for that property or method on the object itself.
-
If the property or method is not found, JavaScript then looks for it on the object's prototype.
-
If it is not found there, JavaScript looks on the prototype's prototype, and so on, creating a
chain of objects
until the property or method is found or there is no further prototype to search. -
Here is an example of prototype chain in JavaScript:
// Create an object const myObj = { a: 1, }; // Create another object that inherits from myObj const myOtherObj = Object.create(myObj); myOtherObj.b = 2; // Add a property to myObj's prototype myObj.prototype.c = 3; // Call a method on myOtherObj console.log(myOtherObj.a); // Output: 1 // Call a method on myOtherObj that is not defined on it console.log(myOtherObj.c); // Output: 3
-
In this example, we first create an object
myObj
with a propertya
equal to1
. We then create another objectmyOtherObj
usingObject.create(myObj)
, which setsmyOtherObj
's prototype tomyObj
. We also givemyOtherObj
its own propertyb
equal to2
. -
Next, we add a property
c
tomyObj
's prototype object by using the.prototype
syntax. This means thatc
is now a property ofmyObj
's prototype, and therefore all objects that inherit frommyObj
will also have access toc
. -
Finally, we call
console.log(myOtherObj.a)
, which logs the value ofmyOtherObj.a
to the console. SincemyOtherObj
inherits frommyObj
, it has access to thea
property defined onmyObj
, so the output is1
. We then callconsole.log(myOtherObj.c)
, which logs the value ofmyOtherObj.c
to the console.myOtherObj
does not have its ownc
property, so JavaScript looks forc
onmyOtherObj
's prototype object (which ismyObj
), and finds it there. Therefore, the output is3
. -
This example demonstrates the basics of prototype chain in JavaScript. By using prototype inheritance, we can create complex object hierarchies and easily share properties and methods between objects.
In JavaScript, a function constructor is a function that is used to create objects.
-
A function constructor is defined using the
function
keyword, and it is conventionally named with an initialuppercase
letter to differentiate it from regular functions. -
The function constructor is called with the
new
keyword to create new objects. -
Here's an example:
// Function constructor function Person(name, age) { this.name = name; this.age = age; } // Creating objects using the function constructor const person1 = new Person("John", 30); const person2 = new Person("Jane", 25); // Accessing the properties of the objects console.log(person1.name); // Output: "John" console.log(person2.age); // Output: 25
-
In the example above, we define a function constructor
Person
that takes two parameters,name
andage
. When the function constructor is called with thenew
keyword, it creates a new object and sets thename
andage
properties on the object using thethis
keyword. -
We create two objects
person1
andperson2
using thePerson
function constructor and access their properties using dot notation. -
Function constructors can be useful for creating multiple objects with similar properties and methods. By defining a function constructor, we can create objects with the same set of properties and methods without duplicating code. We can also add methods to the prototype object of the function constructor to allow all objects created by the constructor to inherit those methods.
-
In JavaScript, you can add methods to a function constructor by adding them to the
prototype
property of the constructor function. -
Here's an example:
// Function constructor function Person(name, age) { this.name = name; this.age = age; } // Adding a method to the prototype object of the function constructor Person.prototype.greet = function () { console.log("Hello, my name is " + this.name); }; // Creating an object using the function constructor const person1 = new Person("John", 30); // Calling the method added to the prototype object person1.greet(); // Output: "Hello, my name is John"
-
In this example, we define a function constructor
Person
and add agreet
method to its prototype object. Thegreet
method uses thethis
keyword to refer to the current object and logs a greeting to the console. -
When we create a new object
person1
using thePerson
function constructor, the[[Prototype]]
of the object is set to thePerson.prototype
object, so thegreet
method is available on the object. We call thegreet
method onperson1
, and it logs a greeting to the console. -
Adding methods to the prototype object of a function constructor allows all objects created by the constructor to inherit those methods. This can be more memory-efficient than adding methods directly to each object, as the methods are shared among all objects created by the constructor.
-
When a function constructor is called without the
new
keyword in JavaScript, it does not create a new object to serve as thethis
value for the function. Instead, the function will execute in the global context (i.e., thewindow
object in the browser or theglobal
object in Node.js) and any properties or methods that are defined within the function will be added to the global object. -
For example, consider the following function constructor:
function Person(name, age) { this.name = name; this.age = age; }
-
If you call this constructor with the
new
keyword like this:const person1 = new Person("John", 30);
-
A new object will be created and
this
will refer to that object.person1
will be an instance of thePerson
object with its ownname
andage
properties. -
However, if you call the constructor without the
new
keyword like this:const person2 = Person("Jane", 25);
-
The function will execute in the global context and
this
will refer to the global object. Thename
andage
properties will be added to the global object instead of a new object.person2
will be undefined, since the function does not return anything explicitly. -
It's generally considered a best practice to always use the
new
keyword when calling function constructors in JavaScript to avoid unintended side effects and to ensure that thethis
value is set correctly.
-
To ensure that the new keyword is used when calling a function constructor in JavaScript, you can use the following technique:
-
You can use the
new.target
meta property to check if the new keyword is used when calling a function constructor in JavaScript. -
The
new.target
property is available inside a function constructor and its value will be a reference to the constructor function that was called with thenew
keyword. -
If the function is called without the
new
keyword,new.target
will beundefined
. -
Here's an example of how you can use
new.target
to check if thenew
keyword is used:function Person(name, age) { // Check if the function is called with the new keyword if (!new.target) { return new Person(name, age); } // Otherwise, continue with the normal constructor logic this.name = name; this.age = age; }
-
Create a
Car
function constructor that takes three parameters:make
,model
, andyear
. -
The function constructor should set these properties on the object created by it.
-
Add a method to the Car function constructor's prototype object called
start()
. This method should log a message to the console indicating that the car has started. -
Add a method to the Car function constructor's prototype object called
stop()
. This method should log a message to the console indicating that the car has stopped. -
Create two Car objects and call their
start()
andstop()
methods. -
Here's some example code to get you started:
const car1 = new Car("Toyota", "Corolla", 2020); const car2 = new Car("Honda", "Civic", 2019); car1.start(); // Output: "The Toyota Corolla has started." car2.start(); // Output: "The Honda Civic has started."
Click to show/hide solution
// Solution: function Car(make, model, year) { this.make = make; this.model = model; this.year = year; } Car.prototype.start = function () { console.log(`The ${this.make} ${this.model} has started.`); }; Car.prototype.stop = function () { console.log(`The ${this.make} ${this.model} has stopped.`); }; const car1 = new Car("Toyota", "Corolla", 2020); const car2 = new Car("Honda", "Civic", 2019); car1.start(); // Output: "The Toyota Corolla has started." car2.start(); // Output: "The Honda Civic has started." car1.stop(); // Output: "The Toyota Corolla has stopped." car2.stop(); // Output: "The Honda Civic has stopped."
-
Create a
BankAccount
function constructor that takes one parameter:balance
. -
The function constructor should set the balance property on the object created by it.
-
Add three methods to the BankAccount function constructor's prototype object:
deposit()
,withdraw()
, andgetBalance()
. -
The
deposit()
method should take a parameteramount
and add it to thebalance
property. -
The
withdraw()
method should take a parameteramount
and subtract it from thebalance
property. -
The
getBalance()
method shouldreturn
the currentbalance
. -
Create a
BankAccount
object and call itsdeposit()
,withdraw()
, andgetBalance()
methods. -
Here's some example code to get you started:
const account = new BankAccount(1000); console.log(account.getBalance()); // Output: 1000 account.deposit(500); console.log(account.getBalance()); // Output: 1500 account.withdraw(200); console.log(account.getBalance()); // Output: 1300
Click to show/hide solution
// Solution: function BankAccount(balance) { this.balance = balance; } BankAccount.prototype.deposit = function (amount) { this.balance += amount; }; BankAccount.prototype.withdraw = function (amount) { if (amount > this.balance) { console.log("Insufficient funds."); return; } this.balance -= amount; }; BankAccount.prototype.getBalance = function () { return this.balance; }; const account = new BankAccount(1000); console.log(account.getBalance()); // Output: 1000 account.deposit(500); console.log(account.getBalance()); // Output: 1500 account.withdraw(200); console.log(account.getBalance()); // Output: 1300 account.withdraw(2000); // Output: "Insufficient funds."
-
ES6 introduced the class syntax, which provides a more traditional object-oriented programming approach to creating objects.
class Person { constructor(name, age) { this.name = name; this.age = age; } greeting() { console.log("Hello, my name is " + this.name); } } const person = new Person("John", 30);
-
These are some of the common ways to declare objects in JavaScript. Depending on your use case, one method may be more appropriate than the others.
-
Convert the
Car
function constructor from the previous exercise to an ES6 class.Click to show/hide solution
class Car { // Your code here constructor(make, model, year) { this.make = make; this.model = model; this.year = year; } start() { console.log(`The ${this.make} ${this.model} has started.`); } stop() { console.log(`The ${this.make} ${this.model} has stopped.`); } }
-
Create two
Car
objects and call theirstart()
andstop()
methods.Click to show/hide solution
// Solution: const car1 = new Car("Toyota", "Corolla", 2020); const car2 = new Car("Honda", "Civic", 2019); car1.start(); // Output: "The Toyota Corolla has started." car2.start(); // Output: "The Honda Civic has started." car1.stop(); // Output: "The Toyota Corolla has stopped." car2.stop(); // Output: "The Honda Civic has stopped."
-
Convert the
BankAccount
function constructor from the previous exercise to an ES6 class.Click to show/hide solution
// Solution: class BankAccount { constructor(balance) { this.balance = balance; } deposit(amount) { this.balance += amount; } withdraw(amount) { if (amount > this.balance) { console.log("Insufficient funds."); return; } this.balance -= amount; } getBalance() { return this.balance; } }
-
Create a
BankAccount
object and call itsdeposit()
,withdraw()
, andgetBalance()
methods.Click to show/hide solution
// Solution: const account = new BankAccount(1000); console.log(account.getBalance()); // Output: 1000 account.deposit(500); console.log(account.getBalance()); // Output: 1500 account.withdraw(200); console.log(account.getBalance()); // Output: 1300 account.withdraw(2000); // Output: "Insufficient funds."
In JavaScript, objects are non-primitive data types and are stored in the heap. When an object is assigned to a variable or passed as an argument to a function, a reference to its memory location is copied, not a copy of its value.
There are two ways to create a copy of an object:
-
Shallow Copy
-
Deep Copy
-
Shallow copy creates a new object that shares the same memory references as the original object for its properties.
-
This means that if the property value is an object, the new object will reference the same memory location as the original object.
-
Shallow copy is achieved through the
Object.assign()
method or thespread operator (ES6)
.-
Here's an example of shallow copying an object using
Object.assign()
:let originalObj = { name: "John", age: 30, address: { street: "123 Main St", city: "Anytown", state: "CA", }, }; let newObj = Object.assign({}, originalObj); console.log(newObj); // Output: {name: "John", age: 30, address: {street: "123 Main St", city: "Anytown", state: "CA"}} console.log(originalObj === newObj); // Output: false console.log(originalObj.address === newObj.address); // Output: true
As you can see, the
newObj
is a shallow copy oforiginalObj
.newObj
has its own memory space, but the "address
" property ofnewObj
references the same memory location as the "address
" property oforiginalObj
. -
Here's an example of
shallow
copying an object using thespread operator
:let originalObj = { name: "John", age: 30, address: { street: "123 Main St", city: "Anytown", state: "CA", }, }; let newObj = { ...originalObj }; console.log(newObj); // Output: {name: "John", age: 30, address: {street: "123 Main St", city: "Anytown", state: "CA"}} console.log(originalObj === newObj); // Output: false console.log(originalObj.address === newObj.address); // Output: true
As you can see, the
newObj
is a shallow copy oforiginalObj
.newObj
has its own memory space, but the "address
" property ofnewObj
references the same memory location as the "address
" property of originalObj.
-
-
Deep copy
, on the other hand, creates a completely new object with its own memory space for all properties, including those that are objects themselves. -
This ensures that changes made to the copied object
do not affect
the original object or any other copies of the object. -
There are different ways to achieve
deep copy
, one of which is using theJSON.parse()
andJSON.stringify()
methods. -
The
JSON.parse()
andJSON.stringify()
methods can be used to copy an object, creating a deep copy. This method works by first serializing the original object to a JSON string usingJSON.stringify()
, and then deserializing the JSON string back into an object usingJSON.parse()
.-
Here's an example of deep copying an object using
JSON.parse()
andJSON.stringify()
:let originalObj = { name: "John", age: 30, address: { street: "123 Main St", city: "Anytown", state: "CA", }, }; let newObj = JSON.parse(JSON.stringify(originalObj)); console.log(newObj); // Output: {name: "John", age: 30, address: {street: "123 Main St", city: "Anytown", state: "CA"}} console.log(originalObj === newObj); // Output: false console.log(originalObj.address === newObj.address); // Output: false
As you can see,
newObj
is a deep copy oforiginalObj
.newObj
has its own memory space for all properties, including the "address
" property. Therefore, changes made tonewObj
will not affect theoriginalObj
or any other copies of the object.***Note that using
JSON.parse()
andJSON.stringify()
may not work for all cases, such as when the object containsfunctions
orsymbols
. In those cases, other methods of deep copying may be necessary.
-
-
The spread operator is a new addition to the set of operators in JavaScript ES6. It takes in an iterable (e.g an array) and expands it into individual elements.
-
The spread operator is commonly used to make shallow copies of JS objects. Using this operator makes the code concise and enhances its readability.
-
Examples with Arrays:
const arr = [1, 2, 3]; console.log(...arr);
const arr = [1, 2, 3]; const arr2 = [4, 5, 6]; const arr3 = [...arr, ...arr2]; console.log(arr3);
const arr = [1, 2, 3]; const arr2 = [...arr, 4, 5, 6]; console.log(arr2);
- Example with Objects:
const person = { name: "John", age: 30 }; const clone = { ...person }; console.log(clone);
const person = { name: "John", age: 30 }; const clone = { ...person, location: "USA" }; console.log(clone);
const person = { name: "John", age: 30 }; const clone = { location: "USA", ...person }; console.log(clone);
const person = { name: "John", age: 30 }; const clone = { ...person, age: 40 }; console.log(clone);
const person = { firstName: "John", lastName: "Doe", age: 30 }; const address = { city: "New York", state: "NY", zip: "10001" }; const user = { ...person, ...address }; console.log(user);
-
In JavaScript, there are several ways to declare functions. Here are some of the most common ones:
-
Function Declaration
: This is the most common way to declare a function. It uses the "function
" keyword followed by the function name, parameters, and function body.function functionName(parameter1, parameter2) { // function body return something; }
-
Function Expression
: In this method, you declare a function as a variable andassign
it a value using the "function" keyword.var functionName = function (parameter1, parameter2) { // function body return something; };
-
Arrow Function
: This is a new way to declare functions in ES6 (ECMAScript 2015). Arrow functions are shorter and have a more concise syntax compared to regular functions.var functionName = (parameter1, parameter2) => { // function body return something; }; var functionName = parameter1 => { // function body to do something and then return something return something; }; var functionName = (parameter1, parameter2) => return something; var functionName = parameter1 => return something;
-
Method Definition
: This is used to declare a function as aproperty
of anobject
.var obj = { functionName: function (parameter1, parameter2) { // function body return something; }, };
Some examples of different types of functions in JavaScript based on their behavior and side-effects:
-
Pure function:
A pure function is a function that doesn't modify any data outside of its own scope and always returns the same result for the same input. Here is an example:
function add(a, b) { return a + b; }
-
Impure function:
An impure function is a function that modifies data outside of its own scope or has side effects. Here is an example:
let count = 0; function incrementCount() { count++; }
-
Higher-order function:
A higher-order function is a function that takes one or more functions as arguments or returns a function as its result. Here is an example:
function multiplyBy(num) { return function (x) { return x * num; }; } const double = multiplyBy(2); console.log(double(5)); // Output: 10
-
Recursive function:
A recursive function is a function that calls itself until a certain condition is met. Here is an example:
function factorial(num) { if (num === 0 || num === 1) { return 1; } return num * factorial(num - 1); } console.log(factorial(5)); // Output: 120
-
Arrow function:
An arrow function is a more concise syntax for writing a function in JavaScript. Here is an example:
const add = (a, b) => { return a + b; }; console.log(add(2, 3)); // Output: 5
-
Generator function:
A generator function is a function that can pause its execution and return multiple values one at a time. Here is an example:
function* processNumbers(numbers) {
for (const num of numbers) {
// Perform some operation on num
const result = num * 2;
// Pause the iteration and yield the result
yield result;
}
}
function doSomething() {
console.log("Doing something...");
}
const numbers = [1, 2, 3, 4, 5];
const gen = processNumbers(numbers);
// Iterate over the generator and pause after each result
console.log(gen.next().value); // 2
doSomething();
console.log(gen.next().value); // 4
doSomething();
console.log(gen.next().value); // 6
doSomething();
console.log(gen.next().value); // 8
doSomething();
console.log(gen.next().value); // 10
-
In javaScript functions are considered first-class objects, which means they can be treated like any other value or object. This means that:
-
Functions can be assigned to variables:
const add = function (a, b) { return a + b; }; console.log(add(2, 3)); // 5
-
Functions can be returned as values from functions:
function multiply(x, y) { return x * y; } // multiply 3,4,5, by 2 multiply(2, 3); //6 multiply(2, 4); // 8 multiply(2, 5); // 10 // multiply 3,4,5, by 3 multiply(3, 3); // 9 multiply(3, 4); // 12 multiply(3, 5); // 15
function multiplyByTwo(x) { return x * 2; } function multiplyByThree(x) { return x * 3; } function multiplyByFour(x) { return x * 4; }
function makeMultiplier(factor) { return function (number) { return number * factor; }; } const multiplyByTwo = makeMultiplier(2); // multiply 3,4,5, by 2 console.log(multiplyByTwo(3)); // 6 console.log(multiplyByTwo(4)); // 8 console.log(multiplyByTwo(5)); // 10 const multiplyByThree = makeMultiplier(3); // multiply 3,4,5, by 3 console.log(multiplyByThree(3)); // 9 console.log(multiplyByThree(4)); // 12 console.log(multiplyByThree(5)); // 15
- When a function returns another function as its result, it is called a "
closure
". - A
closure
is a function that has access to variables in its outer (enclosing) scope, even after the outer function has returned.
- When a function returns another function as its result, it is called a "
-
Functions can be stored in data structures like arrays or objects:
const operations = { add: function (a, b) { return a + b; }, subtract: function (a, b) { return a - b; }, multiply: function (a, b) { return a * b; }, divide: function (a, b) { return a / b; }, }; console.log(operations.add(2, 3)); // 5 console.log(operations.divide(10, 2)); // 5
-
Functions can be passed as arguments to other functions:
function greeting(name) { console.log(`Hello, ${name}!`); } function greetingWrapper(fn, name) { console.log("Preparing to greet..."); fn(name); console.log("Greeting complete."); } greetingWrapper(greeting, "Alice"); // Output: // Preparing to greet... // Hello, Alice! // Greeting complete.
-
- JS is
single-threaded
programming language, which means it can only execute one task at a time. - This is because it has only one
call stack
, which is responsible for executing the code. - This means that if a function is currently executing, no other code can run until the call stack is clear.
- This can be a problem if a function takes a long time to execute, because it will block the call stack and prevent other code from running. This is known as
blocking code
. - Blocking code can be a problem in JavaScript because it can make the user interface unresponsive.
- For example, if a function takes a long time to execute, the user will not be able to interact with the page until the function is finished executing.
- This can be a problem if the function is doing something that is not related to the user interface, such as fetching data from a server or performing a complex calculation.
- To solve this problem, JavaScript developers often use
asynchronous programming techniques
, such ascallbacks
,promises
, andasync/await
, to allow the application to continue running while it waits for long-running tasks to complete. - This allows the user interface to remain responsive while the application is performing long-running tasks.
- A callback is a function that is passed as an argument to another function
- And is executed after some operation has been completed
-
Example to show the problem without callback function:
// breaking the DRY principle const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; console.log(nums); function copyArrayAndMultiplyByTwo(array) { const output = []; for (let i = 0; i < array.length; i++) { output.push(array[i] * 2); } return output; } const numbers1 = copyArrayAndMultiplyByTwo(nums); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] function copyArrayAndDivideByTwo(array) { const output = []; for (let i = 0; i < array.length; i++) { output.push(array[i] / 2); } return output; } const numbers2 = copyArrayAndDivideByTwo(nums); // [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5] function copyArrayAndAddTwo(array) { const output = []; for (let i = 0; i < array.length; i++) { output.push(array[i] + 2); } return output; } const numbers3 = copyArrayAndAddTwo(nums); // [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
-
The solution using callback function
// using the callback functions: function multiplyByTwo(number) { return number * 2; } function divideByTwo(number) { return number / 2; } function addTwo(number) { return number + 2; } function copyArray(array, instructions) { const newArray = []; array.forEach((element) => { newArray.push(instructions(element)); }); return newArray; } const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; console.log(numbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] const newNumbers = copyArray(numbers, multiplyByTwo); console.log(newNumbers); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] const newNumbers2 = copyArray(numbers, divideByTwo); console.log(newNumbers2); // [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5] const newNumbers3 = copyArray(numbers, addTwo); console.log(newNumbers3); // [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- You don't need to create a new function for each operation
- You can pass a annonymous/arrow function as an argument to another function
// Passing annonymous function as arguments to another function const newNumbers4 = copyArray(numbers, function (number) { return number + 2; }); console.log(newNumbers4); // [3, 4, 5, 6, 7, 8, 9, 10, 11, 12] // Passing arrow function as arguments to another function const newNumbers5 = copyArray(numbers, (number) => number + 2); console.log(newNumbers5); // [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
-
Example to show how to use callback function to handle events in the browser
const button = document.querySelector("button"); function handleClick(event) { console.log("Button clicked!"); } button.addEventListener("click", handleClick);
-
In asynchronous programming, a function may take some time to complete its operation. This can cause the program to pause or become unresponsive.
-
Callbacks provide a way to avoid this by allowing the program to continue executing while the asynchronous function is running in the background.
function getData(callback) { setTimeout(() => { const data = { name: "John Doe", age: 30 }; callback(data); }, 1000); } function processData(data) { console.log(`Name: ${data.name}, Age: ${data.age}`); } getData(processData); // Output after 1 second: Name: John Doe, Age: 30
-
The above example shows how callbacks can be used to handle asynchronous operations in JavaScript.
-
The
getData()
function takes a callback function as an argument and executes it after one second. -
The
processData()
function is passed as an argument to thegetData()
function and is executed after one second. -
The
processData()
function receives the data object as an argument and logs it to the console. -
The
getData()
function is called with theprocessData()
function as an argument. -
The
getData()
function executes theprocessData()
function after one second and passes the data object as an argument.
-
Higher order function is a function that takes a function as an argument
function copyArray(array, instructions) { const newArray = []; array.forEach((element) => { newArray.push(instructions(element)); }); return newArray; } function multiplyByTwo(number) { return number * 2; } const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // Using the higher-order function to pass a function as an argument const newNumbers = copyArray(numbers, multiplyByTwo); console.log(newNumbers); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-
Or a function that returns a function as a result
// example of a function that returns a function as a result: function createAdder(num) { return function (x) { return x + num; }; } const addTwo = createAdder(2); console.log(addTwo(10)); // 12 const addFive = createAdder(5); console.log(addFive(10)); // 15
Examples of built-in higher-order functions in JavaScript:
-
Array.map()
: Returns a new array with the same number of elements as the original array, where each element is transformed according to a function that is passed as an argument.const numbers = [1, 2, 3, 4, 5]; const doubledNumbers = numbers.map(function (num) { return num * 2; }); console.log(doubledNumbers); // Output: [2, 4, 6, 8, 10]
-
Array.filter()
: Returns a new array with only the elements that pass a test implemented by a function that is passed as an argument.const numbers = [1, 2, 3, 4, 5]; const evenNumbers = numbers.filter(function (num) { return num % 2 === 0; }); console.log(evenNumbers); // Output: [2, 4]
-
Array.reduce()
:
-
The goal of the
reduce
method is to reduce the array to a single value by applying a function to each element in that array (from left to right). -
The function that is passed to
reduce()
takes two arguments: anaccumulator
and thecurrent value
in the array. -
The function returns the updated
accumulator
, which is used as theaccumulator
for the next iteration. -
The function is called once for each element in the array.
const numbers = [1, 2, 3, 4, 5]; const sumOfNumbers = numbers.reduce(function (acc, num) { return acc + num; }, 0); console.log(sumOfNumbers); // Output: 15
-
These built-in higher-order functions make it easy to write concise and expressive code that manipulates arrays.
Promises
are used to handle asynchronous operations in JavaScript.- An
asynchronous operation
is one that doesn't block the execution of the program while it's running, but instead runs in the background andnotifies
the program when it's finished. - A
Promise
is an object that represents the eventual completion or failure of an asynchronous operation. Promises
returns an object to which you attach callbacks, instead of passing callbacks into a function.- A
Promise
is in one of these states:pending
: initial state, neither fulfilled nor rejected.fulfilled
: meaning that the operation was completed successfully.rejected
: meaning that the operation failed.
// example of creating a promise using the Promise constructor:
let promise = new Promise(function (resolve, reject) {
// executor (the producing code, "singer")
});
-
The function passed to
new Promise
is called theexecutor
. -
When new
Promise
is created, theexecutor
runs automatically. It contains the producing code which should eventually produce the result. -
Its arguments
resolve
andreject
are callbacks provided by JavaScript itself. -
When the
executor
obtains the result, it should call one of these callbacks:resolve(value)
— if the job finished successfully, with resultvalue
.reject(error)
— if an error occurred,error
is the error object.
-
So to summarize: the
executor
runs automatically and attempts to perform a job. -
When it is finished with the attempt, it calls
resolve
if it was successful orreject
if there was an error.const myPromise = new Promise((resolve, reject) => { // Do some async operation here... if (/* some condition */) { resolve(/* some value */); // Fulfill the promise } else { reject(/* some error */); // Reject the promise } }); myPromise.then(result => { // The promise was fulfilled }).catch(error => { // The promise was rejected });
-
The
promise
object returned by thenew Promise
constructor has these internal properties:-
state
— initially"pending"
, then changes to either"fulfilled"
whenresolve
is called or"rejected"
whenreject
is called. -
result
— initiallyundefined
, then changes tovalue
whenresolve(value)
called or toerror
whenreject(error)
is called. -
So the executor eventually moves promise to one of these states:
-
-
Here’s an example of a
promise
constructor and a simpleexecutor
function with “producing code” that takes time (viasetTimeout
):let promise = new Promise(function (resolve, reject) { // the function is executed automatically when the promise is constructed // after 5 second signal that the job is done with the result "done" setTimeout(() => resolve("done"), 5000); }); console.log(promise); // Promise { <pending> }
-
We can see two things by running the code above:
-
The
executor
is called automatically and immediately (bynew Promise
). -
The
executor
receives two arguments:resolve
andreject
. These functions are pre-defined by the JavaScript engine, so we don’t need to create them. We should only call one of them when ready.
After 5 seconds of “processing”, the
executor
callsresolve("done")
to produce the result. This changes thestate
of thepromise
object:- That was an example of a successful job completion, a
“fulfilled promise”
.
-
-
And now an example of the
executor
rejecting thepromise
with anerror
:let promise = new Promise(function (resolve, reject) { // after 5 seconds signal that the job is finished with an error setTimeout(() => reject(new Error("Whoops!")), 5000); }); console.log(promise); // Promise { <pending> }
- The call to
reject(...)
moves the promise object to "rejected" state:
- A
Promise
object serves as a link between theexecutor
(the “producing code”) and theconsuming functions
, which will receive theresult
orerror
. Consuming functions
can be registered (subscribed) using methods.then
,.catch
and.finally
.
-
The most important, fundamental one is .then.
-
The syntax is:
promise.then( function (result) { /* handle a successful result */ }, function (error) { /* handle an error */ } );
-
The first argument of
.then
is a function that runs when the promise isfulfilled
, and receives theresult
. -
The second argument of
.then
is a function that runs when the promise isrejected
, and receives theerror
. -
For instance, here’s a reaction to a
successfully
resolved promise:let promise = new Promise(function (resolve, reject) { setTimeout(() => resolve("done!"), 1000); }); // resolve runs the first function in .then promise.then( (result) => alert(result), // shows "done!" after 1 second (error) => alert(error) // doesn't run );
-
And in the case of a
rejection
, the second one:let promise = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // reject runs the second function in .then promise.then( (result) => alert(result), // doesn't run (error) => alert(error) // shows "Error: Whoops!" after 1 second );
-
If we’re interested only in successful completions, then we can provide only one function argument to
.then
:let promise = new Promise((resolve) => { setTimeout(() => resolve("done!"), 1000); }); promise.then(alert); // shows "done!" after 1 second
-
If we’re interested only in
errors
, then we can usenull
as the first argument:.then(null, errorHandlingFunction)
, which is exactly the same:let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // .catch(f) is the same as promise.then(null, f) // promise.then(null, alert); // shows "Error: Whoops!" after 1 second promise.catch(alert); // shows "Error: Whoops!" after 1 second
-
The call
.catch(f)
is a complete analog of.then(null, f)
, it’s just a shorthand.
-
The call
.finally(f)
is similar to.then(f, f)
in the sense thatf
always runs when the promise issettled
: be itresolve
orreject
. -
The idea of
finally
is to set up a handler for performing cleanup/finalizing after the previous operations are complete. -
E.g. stopping loading indicators, closing no longer needed connections, etc.
-
A
finally
handler “passes through” theresult
orerror
to the next suitable handler. -
For instance, here the
result
is passed throughfinally
tothen
:new Promise((resolve, reject) => { setTimeout(() => resolve("value"), 2000); }) .finally(() => alert("Promise ready")) // triggers first .then((result) => alert(result)); // <-- .then shows "value"
-
As you can see, the
value
returned by the firstpromise
is passed throughfinally
to the nextthen
. -
And here’s an example of an
error
, for us to see how it’s passed throughfinally
tocatch
:new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) // <-- .finally handles the error .catch((err) => alert(err)); // <-- .catch handles the error object
-
A
finally
handler has no arguments. -
In
finally
we don’t know whether thepromise
is successful or not. That’s all right, as our task is usually to perform“general”
finalizing procedures. -
That’s very convenient, because
finally
is not meant to process apromise result
. As said, it’s a place to dogeneric cleanup
, no matter what the outcome was. -
A
finally
handler also shouldn’t return anything. If it does, the returned value is silently ignored. -
The only
exception
to this rule is when afinally
handler throws anerror
. Then thiserror
goes to thenext handler
, instead of anyprevious outcome
.
-
In frontend programming,
promises
are often used fornetwork requests
. -
The
Fetch API
provides an interface for fetching resources. -
It is a more powerful and flexible replacement for
XMLHttpRequest
. -
For making a request and fetching a resource, use the
fetch()
method. -
It is a global method in Window object.
-
The
fetch()
method takes one mandatory argument, thepath
to the resource you want to fetch. -
The
fetch()
method does not directly return theJSON response body
but instead it returns apromise
that resolves with aResponse object
. -
The
Response object
, in turn, does not directly contain the actualJSON response
body but is instead a representation of the entireHTTP response
. -
So, to extract the
JSON body content
from theResponse object
, we use thejson()
method. -
The
json()
returns a secondpromise
thatresolves
with theresult
of parsing theresponse body text
asJSON
. -
json()
is a method on theresponse object
that returns apromise
to parse the body text asJSON
.fetch("https://jsonplaceholder.typicode.com/todos/1") // reads the remote data and parses it as JSON .then((response) => response.json()) .then((json) => console.log(json));
const URL = "https://dummyjson.com/products"; fetch(URL) // returns a promise .then((res) => res.json()) // returns a promise .then((json) => json) // returns a json object .then((data) => { console.log(data.products); return data.products; // for the next then() }) .then((products) => { console.log(products[0]); return products[0]; // for the next then() }) .then((firstProduct) => { console.log(firstProduct.title); });
let user = { name: "John", surname: "Smith", }; let response = await fetch("/article/fetch/post/user", { method: "POST", headers: { "Content-Type": "application/json;charset=utf-8", }, body: JSON.stringify(user), }); let result = await response.json(); alert(result.message);
-
Explanation of the code:
-
We first create an object called
user
with two properties,name
andsurname
.let user = { name: "John", surname: "Smith", };
-
We then use the
fetch()
function to make aPOST
request to a URL endpoint (/article/fetch/post/user
in this case) and pass in anoptions object
that specifies themethod
,headers
, andbody
of the request. -
In this case, we are sending the
user
object as the requestbody
after converting it to aJSON string
usingJSON.stringify()
.let response = await fetch("/article/fetch/post/user", { method: "POST", headers: { "Content-Type": "application/json;charset=utf-8", }, body: JSON.stringify(user), });
-
The
Content-Type header
is used to indicate the media type orformat
of the data being sent in thebody
of anHTTP request
. -
It informs the server about how to interpret and process the data in the
request body
. -
In the provided code, the
Content-Type header
is set to"application/json;charset=utf-8"
. This specifies that the body of the request contains data inJSON
format, and the character encoding used isUTF-8
. -
By setting the
Content-Type header
appropriately, you are providing a hint to theserver
on how to handle therequest body
. -
When the
server
receives therequest
, it checks theContent-Type header
to determine how to parse and interpret the incoming data. -
In this case, the
server
can expect the body to be inJSON
format and use appropriate parsing mechanisms to convert theJSON string
into anobject
or perform any necessary processing. -
In the context of the
Content-Type header
with"charset=utf-8"
, it indicates that the content of the request body is encoded using theUTF-8 character encoding
. By specifying this encoding, you ensure that theserver
correctly interprets and handles the text data encoded in UTF-8 format. -
The
JSON.stringify()
method is used in this code to convert theuser
object into a JSON string representation. -
When making an
HTTP request
, thebody
of the request typically contains the data that needs to be sent to the server. However, theHTTP protocol
itself only supports sendingtextual data
. -
JSON
(JavaScript Object Notation) is a popular data interchange format that is widely supported and understood by many programming languages and platforms. -
In order to include the
user
object as the body of theHTTP request
, it needs to be converted into a format that can be transmitted over the network. -
By using
JSON.stringify(user)
, theuser
object is serialized into aJSON string
, which is a textual representation of the object's data. -
This allows the data to be sent as the body content of the request.
let user = { name: "John", surname: "Smith", }; let jsonString = JSON.stringify(user); console.log(jsonString); // '{"name":"John","surname":"Smith"}'
-
On the
server side
, when the request is received, the server can thenparse
theJSON string
back into an object and access the individual properties of theuser
object. -
This is commonly done using
JSON parsing libraries
orbuilt-in functionality
provided by the server-side programming language or framework. -
We then use the
json()
method on theresponse
object to parse the response body as JSON and return a JavaScript object. We store the parsed object in theresult
variable usingawait
since thejson()
method returns aPromise
.let result = await response.json();
-
Finally, we display a message from the parsed JSON data using the
alert()
function. In this case, we assume that the response body has a property calledmessage
.alert(result.message);
-
-
There’s a special syntax to work with
promises
in a more comfortable fashion, called“async/await”
. It’s surprisingly easy to understand and use. -
The word
“async”
before a function means one simple thing: a function always returns apromise
. Other values are wrapped in a resolved promise automatically. -
let's check this function:
function getEmployeesData() { const employees = ["John", "Jane", "Jack"]; return new Promise((resolve, reject) => { if (employees.length > 0) { resolve("Employees exist"); } else { reject("Employees does not exist"); } }); } // call the function getEmployeesData().then( (reolveValue) => console.log(reolveValue), (rejectValue) => console.error(rejectValue) );
-
We can rewrite the previous function as:
function getEmployeesData() { const employees = ["John", "Jane", "Jack"]; if (employees.length > 0) { return Promise.resolve("Employees exist"); } else { return Promise.reject("Employees does not exist"); } } // call the function getEmployeesData().then( (reolveValue) => console.log(reolveValue), (rejectValue) => console.error(rejectValue) );
-
We can rewrite the previous function using
async
as:async function getEmployeesData() { const employees = ["John", "Jane", "Jack"]; if (employees.length > 0) { return "Employees exist"; } else { throw "Employees does not exist"; } } // call the function getEmployeesData().then( (reolveValue) => console.log(reolveValue), (rejectValue) => console.error(rejectValue) );
-
await
is a keyword that is used inside anasync
function. -
await
waits for thepromise
toresolve
and returns the result. -
We use the
await
keyword to wait for the response to come back before moving on to the next line of code -
await
only works insideasync
functions within regular JavaScript code, however it can be used on its own with JavaScriptmodules
. -
Example
function getEmployeesData() { console.log("Start of getEmployeesData()"); const URL = "https://dummyjson.com/products"; const response = fetch(URL); response .then((res) => res.json()) .then((json) => json.products) .then((products) => products[0]) .then((firstProduct) => firstProduct.title) .then((title) => console.log(title)); console.log("End of getEmployeesData()"); } getEmployeesData();
-
The above function can be rewritten using
async
andawait
as:async function getEmployeesData() { console.log("Start of getEmployeesData()"); const URL = "https://dummyjson.com/products"; const response = await fetch(URL); // wait for this promise to resolve before moving on to the next line of code. const json = await response.json(); // wait until the resolved promise is pasred to json const products = json.products; const firstProduct = products[0]; const title = firstProduct.title; console.log(title); console.log("End of getEmployeesData()"); } getEmployeesData();
-
The keyword
await
makes JavaScript wait until thatpromise
settles and returns its result. -
await
literallysuspends
the function execution until thepromise
settles, and thenresumes
it with thepromise
result. -
That doesn’t cost any CPU resources, because the
JavaScript
engine can do other jobs in the meantime: execute other scripts, handle events, etc. -
It’s just a more elegant syntax of getting the
promise
result thanpromise.then
. And, it’s easier to read and write. -
The above function can be rewritten using
try
catch
as:async function getEmployeesData() { console.log("Start of getEmployeesData()"); const URL = "https://dummyjson.com/products"; try { const response = await fetch(URL); const json = await response.json(); const products = json.products; const firstProduct = products[0]; const title = firstProduct.title; console.log(title); } catch (error) { console.log("error in catch", error); } console.log("End of getEmployeesData()"); }
-
In modern browsers,
await
on top level works just fine, when we’re inside amodule
. But if the code is not inside a module, thenawait
will produce anerror
. -
Example:
// we assume this code runs at top level, inside a module let response = await fetch("/article/promise-chaining/user.json"); let user = await response.json(); console.log(user);
async function fetchData(URL) { try { const response = await fetch(URL); // console.log(response); if (!response.ok) { console.log(response.status); throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error("Error:", error); } } try { const response = await fetchData("https://dummyjson.com/products"); const data = response?.products; console.log(data); } catch (error) { console.error("Error outer catch:", error); }
async function postJSON(URL, data) { try { const response = await fetch(URL, { method: "POST", // or 'PUT' headers: { "Content-Type": "application/json", }, body: JSON.stringify(data), }); const result = await response.json(); console.log("Success:", result); return result; } catch (error) { console.error("Error:", error); } } const URL = "https://example.com/profile"; const data = { username: "example" }; const response = await postJSON(URL, data);
- The call to
- callback hell is a situation where you have a lot of nested callbacks
loadScript("/my/script.js", function (script) {
loadScript("/my/script2.js", function (script) {
loadScript("/my/script3.js", function (script) {
// ...continue after all scripts are loaded
});
});
});
- This code uses callbacks to load three scripts in sequence. Each time a script is loaded, the next script is loaded in the callback function.
- This creates a "
pyramid of doom
" structure where the code becomes nested and hard to read. It also makes error handling difficult - To solve this, we can use
Promises
andasync/await
. Here's an example of how to refactor the code using Promises and async/await:
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = src;
script.onload = () => {
document.head.appendChild(script);
resolve(script);
};
script.onerror = () => reject(new Error(`Failed to load script ${src}`));
});
}
async function start() {
try {
const script1 = await loadScript("/my/script.js");
console.log(`${script1.src} loaded`);
const script2 = await loadScript("/my/script2.js");
console.log(`${script2.src} loaded`);
const script3 = await loadScript("/my/script3.js");
console.log(`${script3.src} loaded`);
// continue after all scripts are loaded
} catch (err) {
console.error(err);
}
}
start();
- This refactored code uses
Promises
to load the scripts, andasync/await
to wait for each Promise toresolve
before continuing with the next statement. - This approach makes the code much more readable and maintainable, and avoids the "
pyramid of doom
" structure of callback hell.