Arrays

Objects allow you to store keyed collections of values. That’s fine.

But quite often we find that we need an ordered collection, where we have a 1st, a 2nd, a 3rd element and so on. For example, we need that to store a list of something: users, goods, HTML elements etc.

It is not convenient to use an object here, because it provides no methods to manage the order of elements. We can’t insert a new property “between” the existing ones. Objects are just not meant for such use.

There exists a special data structure named Array, to store ordered collections.

Declaration

There are two syntaxes for creating an empty array:

let arr = new Array();
let arr = [];

Almost all the time, the second syntax is used. We can supply initial elements in the brackets:

let fruits = ["Apple", "Orange", "Plum"];

Array elements are numbered, starting with zero.

We can get an element by its number in square brackets:

let fruits = ["Apple", "Orange", "Plum"];

alert( fruits[0] ); // Apple
alert( fruits[1] ); // Orange
alert( fruits[2] ); // Plum

We can replace an element:

fruits[2] = 'Pear'; // now ["Apple", "Orange", "Pear"]

…Or add a new one to the array:

fruits[3] = 'Lemon'; // now ["Apple", "Orange", "Pear", "Lemon"]

The total count of the elements in the array is its length:

let fruits = ["Apple", "Orange", "Plum"];
alert( fruits.length ); // 3

We can also use alert to show the whole array.

let fruits = ["Apple", "Orange", "Plum"];
alert( fruits ); // Apple,Orange,Plum

An array can store elements of any type.

For instance:

// mix of values
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
// get the object at index 1 and then show its name
alert( arr[1].name ); // John
// get the function at index 3 and run it
arr[3](); // hello

Get last elements with “at”

Let’s say we want the last element of the array.

Some programming languages allow the use of negative indexes for the same purpose, like fruits[-1].

Although, in JavaScript it won’t work. The result will be undefined, because the index in square brackets is treated literally.

We can explicitly calculate the last element index and then access it: fruits[fruits.length - 1].

let fruits = ["Apple", "Orange", "Plum"];
alert( fruits[fruits.length-1] ); // Plum

A bit cumbersome, isn’t it? We need to write the variable name twice.

Luckily, there’s a shorter syntax: fruits.at(-1):

let fruits = ["Apple", "Orange", "Plum"];
// same as fruits[fruits.length-1]
alert( fruits.at(-1) ); // Plum

In other words, arr.at(i):

  • is exactly the same as arr[i], if i >= 0.
  • for negative values of i, it steps back from the end of the array.

Methods pop/push, shift/unshift

queue is one of the most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations:

  • push appends an element to the end.
  • shift get an element from the beginning, advancing the queue, so that the 2nd element becomes the 1st.

Arrays support both operations.

In practice we need it very often. For example, a queue of messages that need to be shown on-screen.

There’s another use case for arrays – the data structure named stack.

It supports two operations:

  • push adds an element to the end.
  • pop takes an element from the end.

So new elements are added or taken always from the “end”.

A stack is usually illustrated as a pack of cards: new cards are added to the top or taken from the top:

For stacks, the latest pushed item is received first, that’s also called LIFO (Last-In-First-Out) principle. For queues, we have FIFO (First-In-First-Out).

Arrays in JavaScript can work both as a queue and as a stack. They allow you to add/remove elements, both to/from the beginning or the end.

In computer science, the data structure that allows this, is called deque.

Methods that work with the end of the array:

pop

Extracts the last element of the array and returns it:

let fruits = ["Apple", "Orange", "Pear"];
alert( fruits.pop() ); // remove "Pear" and alert it
alert( fruits ); // Apple, Orange

Both fruits.pop() and fruits.at(-1) return the last element of the array, but fruits.pop() also modifies the array by removing it.

push

Append the element to the end of the array:

let fruits = ["Apple", "Orange"];
fruits.push("Pear");
alert( fruits ); // Apple, Orange, Pear

The call fruits.push(...) is equal to fruits[fruits.length] = ....

Methods that work with the beginning of the array:

shift

Extracts the first element of the array and returns it:

let fruits = ["Apple", "Orange", "Pear"];
alert( fruits.shift() ); // remove Apple and alert it
alert( fruits ); // Orange, Pear

unshift

Add the element to the beginning of the array:

let fruits = ["Orange", "Pear"];
fruits.unshift('Apple');
alert( fruits ); // Apple, Orange, Pear

Methods push and unshift can add multiple elements at once:

let fruits = ["Apple"];
fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");
// ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
alert( fruits );

Objects

As we know from the chapter Data types, there are eight data types in JavaScript. Seven of them are called “primitive”, because their values contain only a single thing (be it a string or a number or whatever).

In contrast, objects are used to store keyed collections of various data and more complex entities. In JavaScript, objects penetrate almost every aspect of the language. So we must understand them first before going in-depth anywhere else.

An object can be created with figure brackets {…} with an optional list of properties. A property is a “key: value” pair, where key is a string (also called a “property name”), and value can be anything.

We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by the key. It’s easy to find a file by its name or add/remove a file.

An empty object (“empty cabinet”) can be created using one of two syntaxes:

let user = new Object(); // "object constructor" syntax
let user = {};  // "object literal" syntax

Usually, the figure brackets {...} are used. That declaration is called an object literal.

Literals and properties

We can immediately put some properties into {...} as “key: value” pairs:

let user = {     // an object
  name: "John",  // by key "name" store value "John"
  age: 30        // by key "age" store value 30
};

A property has a key (also known as “name” or “identifier”) before the colon ":" and a value to the right of it.

In the user object, there are two properties:

  1. The first property has the name "name" and the value "John".
  2. The second one has the name "age" and the value 30.

The resulting user object can be imagined as a cabinet with two signed files labeled “name” and “age”.

We can add, remove and read files from it at any time.

Property values are accessible using the dot notation:

// get property values of the object:
alert( user.name ); // John
alert( user.age ); // 30

The value can be of any type. Let’s add a boolean one:

user.isAdmin = true

To remove a property, we can use the delete operator:

delete user.age;

We can also use multiword property names, but then they must be quoted:

let user = {
  name: "John",
  age: 30,
  "likes birds": true  // multiword property name must be quoted
};

The last property in the list may end with a comma:

let user = {
  name: "John",
  age: 30,
}

That is called a “trailing” or “hanging” comma. Makes it easier to add/remove/move around properties, because all lines become alike.

Square brackets

For multiword properties, the dot access doesn’t work:

// this would give a syntax error
user.likes birds = true

JavaScript doesn’t understand that. It thinks that we address user.likes, and then gives a syntax error when comes across unexpected birds.

The dot requires the key to be a valid variable identifier. That implies: contains no spaces, doesn’t start with a digit and doesn’t include special characters ($ and _ are allowed).

There’s an alternative “square bracket notation” that works with any string:

let user = {};

// set
user["likes birds"] = true;

// get
alert(user["likes birds"]); // true

// delete
delete user["likes birds"];

Now everything is fine. Please note that the string inside the brackets is properly quoted (any type of quotes will do).

Square brackets also provide a way to obtain the property name as the result of any expression – as opposed to a literal string – like from a variable as follows:

let key = "likes birds";

// same as user["likes birds"] = true;
user[key] = true;

Here, the variable key may be calculated at run-time or depend on the user input. And then we use it to access the property. That gives us a great deal of flexibility.

For instance:

let user = {
  name: "John",
  age: 30
};

let key = prompt("What do you want to know about the user?", "name");

// access by variable
alert( user[key] ); // John (if enter "name")

The dot notation cannot be used in a similar way:

let user = {
  name: "John",
  age: 30
};

let key = "name";
alert( user.key ) // undefined

Computed properties

We can use square brackets in an object literal, when creating an object. That’s called computed properties.

For instance:

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // the name of the property is taken from the variable fruit
};
alert( bag.apple ); // 5 if fruit="apple"

The meaning of a computed property is simple: [fruit] means that the property name should be taken from fruit.

So, if a visitor enters "apple"bag will become {apple: 5}.

Essentially, that works the same as:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// take property name from the fruit variable
bag[fruit] = 5;

…But looks nicer.

We can use more complex expressions inside square brackets:

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

Square brackets are much more powerful than dot notation. They allow any property names and variables. But they are also more cumbersome to write.

So most of the time, when property names are known and simple, the dot is used. And if we need something more complex, then we switch to square brackets.

Property value shorthand

In real code, we often use existing variables as values for property names.

For instance:

function makeUser(name, age) {
  return {
    name: name,
    age: age,
    // ...other properties
  };
}

let user = makeUser("John", 30);
alert(user.name); // John

variable is so common, that there’s a special property value shorthand to make it shorter.

Instead of name:name we can just write name, like this:

function makeUser(name, age) {
  return {
    name, // same as name: name
    age,  // same as age: age
    // ...
  };
}

We can use both normal properties and shorthands in the same object:

let user = {
  name,  // same as name:name
  age: 30
};

Property names limitations

As we already know, a variable cannot have a name equal to one of the language-reserved words like “for”, “let”, “return” etc.

But for an object property, there’s no such restriction:

// these properties are all right
let obj = {
  for: 1,
  let: 2,
  return: 3
};

alert( obj.for + obj.let + obj.return );  // 6

In short, there are no limitations on property names. They can be any strings or symbols (a special type for identifiers, to be covered later).

Other types are automatically converted to strings.

For instance, a number 0 becomes a string "0" when used as a property key:

let obj = {
  0: "test" // same as "0": "test"
};

// both alerts access the same property (the number 0 is converted to string "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (same property)

Property existence test, “in” operator

A notable feature of objects in JavaScript, compared to many other languages, is that it’s possible to access any property. There will be no error if the property doesn’t exist!

Reading a non-existing property just returns undefined. So we can easily test whether the property exists:

let user = {};
alert( user.noSuchProperty === undefined ); // true means "no such property"

There’s also a special operator "in" for that.

The syntax is:

"key" in object

For instance:

let user = { name: "John", age: 30 };
alert( "age" in user ); // true, user.age exists
alert( "blabla" in user ); // false, user.blabla doesn't exist

Please note that on the left side of in there must be a property name. That’s usually a quoted string.

If we omit quotes, that means a variable should contain the actual name to be tested. For instance:

let user = { age: 30 };

let key = "age";
alert( key in user ); // true, property "age" exists

Why does the in operator exist? Isn’t it enough to compare against undefined?

Well, most of the time the comparison with undefined works fine. But there’s a special case when it fails, but "in" works correctly.

It’s when an object property exists, but stores undefined:

let obj = {
  test: undefined
};
alert( obj.test ); // it's undefined, so - no such property?
alert( "test" in obj ); // true, the property does exist!

In the code above, the property obj.test technically exists. So the in operator works right.

Situations like this happen very rarely, because undefined should not be explicitly assigned. We mostly use null for “unknown” or “empty” values. So the in operator is an exotic guest in the code.

The “for..in” loop

To walk over all keys of an object, there exists a special form of the loop: for..in. This is a completely different thing from the for(;;) construct that we studied before.

The syntax:

for (key in object) {
  // executes the body for each key among object properties
}

For instance, let’s output all properties of user:

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // values for the keys
  alert( user[key] ); // John, 30, true
}

Note that all “for” constructs allow us to declare the looping variable inside the loop, like let key here.

Also, we could use another variable name here instead of key. For instance, "for (let prop in obj)" is also widely used.

Object methods, “this”

“this” in methods

It’s common that an object method needs to access the information stored in the object to do its job.

For instance, the code inside user.sayHi() may need the name of the user.

To access the object, a method can use the this keyword.

The value of this is the object “before dot”, the one used to call the method.

For instance:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" is the "current object"
    alert(this.name);
  }

};

user.sayHi(); // John

Here during the execution of user.sayHi(), the value of this will be user.

Technically, it’s also possible to access the object without this, by referencing it via the outer variable:

let user = {
  name: "John",
  age: 30,
  sayHi() {
    alert(user.name); // "user" instead of "this"
  }
};

“this” is not bound

In JavaScript, keyword this behaves unlike most other programming languages. It can be used in any function, even if it’s not a method of an object.

There’s no syntax error in the following example:

function sayHi() {
  alert( this.name );
}

The value of this is evaluated during the run-time, depending on the context.

For instance, here the same function is assigned to two different objects and has different “this” in the calls:

let user = { name: "John" };
let admin = { name: "Admin" };
function sayHi() {
  alert( this.name );
}
// use the same function in two objects
user.f = sayHi;
admin.f = sayHi;
// these calls have different this
// "this" inside the function is the object "before the dot"
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)
admin['f'](); // Admin (dot or square brackets access the method – doesn't matter)

Arrow functions have no “this”

Arrow functions are special: they don’t have their “own” this. If we reference this from such a function, it’s taken from the outer “normal” function.

For instance, here arrow() uses this from the outer user.sayHi() method:

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

That’s a special feature of arrow functions, it’s useful when we actually do not want to have a separate this, but rather to take it from the outer context. Later in the chapter Arrow functions revisited we’ll go more deeply into arrow functions.

Summary

  • Functions that are stored in object properties are called “methods”.
  • Methods allow objects to “act” like object.doSomething().
  • Methods can reference the object as this.

The value of this is defined at run-time.

  • When a function is declared, it may use this, but that this has no value until the function is called.
  • A function can be copied between objects.
  • When a function is called in the “method” syntax: object.method(), the value of this during the call is object.

Please note that arrow functions are special: they have no this. When this is accessed inside an arrow function, it is taken from outside.

Key Points

  1. In Global Scope the value of this is window.
  2. In function the value of this is window
  3. In Method the value of this is object
  4. function inside the method es5(deceleration function) the value of this is window.
  5. function inside the method es6(expression function) the value of this is object.
  6. the value of this in constructor function is blank object.
  7. In event listener value of this will be according to the element.

call/apply/bind

call

There’s a special built-in function method func.call(context, …args) that allows to call a function explicitly setting this.

The syntax is:

func.call(context, arg1, arg2, ...)

It runs func providing the first argument as this, and the next as the arguments.

To put it simply, these two calls do almost the same:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

They both call func with arguments 12 and 3. The only difference is that func.call also sets this to obj.

As an example, in the code below we call sayHi in the context of different objects: sayHi.call(user) runs sayHi providing this=user, and the next line sets this=admin:

function sayHi() {
  alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// use call to pass different objects as "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

apply

Instead of func.call(this, ...arguments) we could use func.apply(this, arguments).

The syntax of built-in method func.apply is:

func.apply(context, args)

It runs the func setting this=context and using an array-like object args as the list of arguments.

The only syntax difference between call and apply is that call expects a list of arguments, while apply takes an array-like object with them.

So these two calls are almost equivalent:

func.call(context, ...args);
func.apply(context, args);

Example:

function func(a, b, c) {
  console.log(this, a, b, c);
}
func.call(obj, [1, 2, 3]);

bind()

The bind() method of Function instances creates a new function that, when called, calls this function with its this keyword set to the provided value, and a given sequence of arguments preceding any provided when the new function is called.

It does not call function. It just return another function that we can store in variable to call it

function func() {
  console.log(this);
}
let n = func.bind("ahmad");

n();

Prototypal inheritance

In JavaScript, objects have a special hidden property [[Prototype]] (as named in the specification), that is either null or references another object. That object is called “a prototype”:

When we read a property from object, and it’s missing, JavaScript automatically takes it from the prototype. In programming, this is called “prototypal inheritance”. And soon we’ll study many examples of such inheritance, as well as cooler language features built upon it.

The property [[Prototype]] is internal and hidden, but there are many ways to set it.

Prototypal inheritance in JavaScript is a mechanism that allows objects to inherit properties and methods from other objects. Instead of using classes like in classical inheritance (as in Java or C++), JavaScript uses prototypes.

function makeHuman(name,age){
  this.name = name;
  this.age = age;
}

makeHuman.prototype.greet = 12;

makeHuman.prototype.func = function(){
  console.log(this.name);
}

let human1 = new makeHuman("ahmad",20);
let human2 = new makeHuman("ali",30);

How It Works:

  • Every JavaScript object has an internal property called [[Prototype]], which points to another object.

  • When you try to access a property or method on an object, JavaScript first looks for it on the object itself.

  • If the property/method is not found, JavaScript looks up the prototype chain until it either finds the property or reaches null.

The value of “this”

An interesting question may arise in the example above: what’s the value of this inside set fullName(value)? Where are the properties this.name and this.surname written: into user or admin?

The answer is simple: this is not affected by prototypes at all.

No matter where the method is found: in an object or its prototype. In a method call, this is always the object before the dot.

So, the setter call admin.fullName= uses admin as this, not user.

That is actually a super-important thing, because we may have a big object with many methods, and have objects that inherit from it. And when the inheriting objects run the inherited methods, they will modify only their own states, not the state of the big object.

Closures in JS

A closure in JavaScript is a function that remembers the variables from its lexical scope, even when the function is executed outside that scope.

How It Works:

When a function is created inside another function, it captures the outer function’s variables, even after the outer function has finished executing.

function counter() {
    let count = 0;
    return function() {
        count++;
        console.log(count);
    };
}
const increment = counter();
increment(); // 1
increment(); // 2

What is an Event Listener in JavaScript?

An event listener is a method that allows you to wait for a specific event (like a click, keypress, hover, etc.) and run a function when the event occurs.

✅ Example of addEventListener()

document.getElementById("btn").addEventListener("click", function() {
 console.log("Button Clicked!");
});
  • Here, addEventListener("click", function) listens for a click event on the button with id="btn".
  • When clicked, it logs "Button Clicked!".

Common Events

EventDescription
clickWhen an element is clicked
mouseoverWhen the mouse hovers over an element
keydownWhen a key is pressed
submitWhen a form is submitted

What is querySelector in JavaScript?

querySelector() is a method used to select an element from the DOM using CSS selectors.

Example of querySelector()

const heading = document.querySelector("h1"); // Selects the first `<h1>` element
heading.style.color = "red"; // Changes its color to red

Selecting Elements

document.querySelector("#myId");    // Selects an element by ID
document.querySelector(".myClass"); // Selects the first element with this class
document.querySelector("p");        // Selects the first <p> element

Event Delegation in JavaScript

Event Delegation is a pattern that allows you to handle events efficiently by attaching a single event listener to a parent element instead of adding multiple listeners to child elements.

🤔 Why Use Event Delegation?

Better Performance → Avoids adding multiple event listeners.
Handles Dynamic Elements → Works even for elements added later via JavaScript.
Less Memory Usage → Reduces the number of event listeners.

Example

let parent = document.querySelector("#parent");
parent.addEventListener("click", function (ev) {
  if (ev.target.id === "play") {
    console.log("Play Song");
  } else if (ev.target.id === "pause") {
    console.log("Pause Song");
  }
});

When Should You Use Event Delegation?

  • Handling clicks on lists, tables, menus, or dynamically added elements.
  • Handling form inputs when adding fields dynamically.
  • Improving performance for elements inside large containers.

Higher-Order Functions in JavaScript

A Higher-Order Function (HOF) is a function that takes another function as an argument or returns a function.

function func1(f1) {  // func1 takes a function as an argument (HOF property)
  f1();  // Calls the passed function
  return function () {  // Returns a new function (HOF property)
    console.log("returned fun");
  };
}

fun = function () {
  console.log("Passed Function");
};

let returned = func1(fun); // Call func1 with 'fun' function

returned(); // Call the returned function

Built-in Higher-Order Functions in JavaScript

MethodDescription
map()Transforms an array by applying a function to each element.
filter()Filters an array based on a condition.
reduce()Reduces an array to a single value.
forEach()Iterates over an array but does not return a new one.

Handling Errors with `try…catch

The try...catch block allows you to handle errors gracefully. The try...catch statement is used to handle errors, and throw is used to create custom errors.

Basic Example of try...catch

try {
    let x = y + 5;  // ❌ ReferenceError: y is not defined
} catch (error) {
    console.log("An error occurred:", error.message);
}

🔹 If an error occurs inside try, execution moves to catch, preventing a crash.

Using throw to Create Custom Error

function divide(a, b) {
    if (b === 0) {
        throw new Error("Cannot divide by zero!");  // 🚀 Custom error
    }
    return a / b;
}

try {
    console.log(divide(10, 0)); // ❌ Throws an error
} catch (error) {
    console.log("Error:", error.message);
}

🔹 throw allows custom error messages instead of generic JavaScript errors.

finally Block (Always Executes)

try {
    console.log("Trying something...");
    throw new Error("Something went wrong!");
} catch (error) {
    console.log("Caught Error:", error.message);
} finally {
    console.log("This runs no matter what!");
}

Custom Events in JavaScript

A Custom Event in JavaScript is an event that you manually create and dispatch using the CustomEvent constructor. This allows communication between different parts of your application.

Creating and Dispatching a Custom Event

The CustomEvent constructor takes two arguments: 1️⃣ Event Name (string)
2️⃣ Event Options (optional, includes detail for extra data)

let ev = new Event("newEv");

document.querySelector("button").addEventListener("newEv", function () {
  console.log("Custom Event");
});
document.querySelector("button").dispatchEvent(ev);

Full Example: Custom Event with Data

// Create and dispatch custom event
const customEvent = new CustomEvent("userLoggedIn", {
   detail: { username: "ahmad9059", role: "admin" }
});

document.addEventListener("userLoggedIn", function (event) {
   console.log(`User: ${event.detail.username}, Role: ${event.detail.role}`);
});

// Dispatch the event
document.dispatchEvent(customEvent);

When to Use Custom Events?

  1. For component communication (e.g., sending data between different UI components).
  2. When working with user interactions that don’t have built-in events.
  3. For event-driven architectures where actions trigger custom behavior.