YouTube Courses - Learn Smarter

YouTube-Courses.site transforms YouTube videos and playlists into structured courses, making learning more efficient and organized.

Object Oriented JavaScript

Master Object-Oriented Programming (OOP) in JavaScript! Learn about constructors, prototypes, classes, and inheritance to write more structured and reusable code.



Introduction to Object-Oriented JavaScript

Welcome to the world of Object-Oriented JavaScript! This chapter will introduce you to the fundamental concept of objects in JavaScript and how they relate to object-oriented programming principles. You might have heard the phrase “everything in JavaScript is an object,” and while not strictly true, understanding this concept is crucial for effective JavaScript development. We will explore what objects are, their properties and methods, and how they are used in JavaScript.

Understanding Objects in JavaScript

Objects as Real-World Entities

Think of objects in JavaScript as similar to objects in the real world. Consider a car, for instance. A car has characteristics that describe it, such as its color, model, and registration number. These characteristics are known as properties.

Properties: Attributes or characteristics that describe an object. They hold data associated with the object.

Furthermore, a car can perform actions, like driving, reversing, and accelerating. These actions that an object can perform are called methods.

Methods: Functions associated with an object that define what actions the object can perform. They operate on the object’s data or properties.

Just like real-world objects, JavaScript objects also possess properties and methods. This analogy helps to conceptualize how objects function in programming.

Built-in Objects in JavaScript

JavaScript provides several built-in objects that are readily available for developers to use. Let’s explore a couple of these using the browser’s developer console.

Arrays as Objects

One common built-in object is the array.

Array: An ordered list of values, which can be of any data type. Arrays are used to store collections of items.

Let’s create an array of names in the console:

let names = ["Ryu", "Crystal", "Mario"];

If you inspect this names variable in the console, you’ll see that it is indeed an object. Arrays, being objects, possess both properties and methods.

  • Properties of Arrays:

    • length: This property tells you the number of elements in the array. For our names array, names.length would return 3.
  • Methods of Arrays:

    • sort(): This method rearranges the elements of the array in a specific order, typically alphabetically or numerically. For example, names.sort() would reorder our names array to ["Crystal", "Mario", "Ryu"].

    You can access properties using dot notation (e.g., names.length) and call methods also using dot notation followed by parentheses (e.g., names.sort()).

The Window Object

Another important built-in object in browser-based JavaScript is the window object.

Window Object: Represents the browser window in which the JavaScript code is running. It is the global object in browser environments and provides access to browser features and functionalities.

When JavaScript code runs in a web browser, it has access to this global window object. It’s often referred to as the “mother of all objects” because it’s the top-level object in the browser environment. Like arrays, the window object also has numerous properties and methods.

  • Properties of the Window Object:

    • innerWidth: This property reflects the width of the browser window’s viewport in pixels. You can access it using window.innerWidth. If you resize your browser window and then check window.innerWidth again, you’ll see the value has updated to reflect the new width.
  • Methods of the Window Object: (The transcript doesn’t explicitly demonstrate a method of the window object in detail, but it mentions their existence). The window object has methods for tasks like opening new windows, setting timers, and interacting with the browser’s history.

These examples illustrate how built-in JavaScript objects, like arrays and the window object, function as collections of properties and methods, mirroring the concept of objects in the real world.

Primitive Types vs. Objects

While many things in JavaScript are objects, it’s important to note that not everything is. Some fundamental data types in JavaScript are primitive types and are not objects themselves.

Primitive Types: Basic data types in JavaScript that are not objects and do not have properties or methods in their raw form. These include null, undefined, boolean, number, bigint, string, and symbol.

Examples of primitive types include:

  • null: Represents the intentional absence of a value.
  • Numbers (e.g., 10, 3.14).
  • Booleans (e.g., true, false).
  • Strings (e.g., "Mario").

Unlike objects, you might think you cannot directly access properties or methods on primitive types. For instance, if you declare a string variable:

let name = "Mario";

And try to inspect name in the console, you won’t see the expandable object structure with properties and methods that you saw with arrays or the window object.

However, JavaScript provides a convenient mechanism that allows primitive types to behave as if they were objects when needed.

The Concept of Primitive Wrappers

JavaScript can temporarily wrap primitive values in objects behind the scenes. This is known as primitive wrappers.

Primitive Wrappers: Temporary object representations of primitive values (like strings, numbers, or booleans) that JavaScript creates to allow access to object methods and properties on these primitive values.

For example, even though a string like "Mario" is a primitive type, you can still use the length property to find the number of characters in it:

name.length; // Returns 5

What’s happening here is that JavaScript implicitly creates a temporary String object wrapper around the primitive string "Mario" when you try to access the length property. This String object has properties and methods, including length. After accessing the property, the temporary wrapper object is discarded.

You can explicitly create a String object wrapper yourself using the String() constructor:

let name2 = new String("Ryu");

Now, if you inspect name2 in the console, you will see a String object, complete with properties (like length) and methods (available under the [[Prototype]] or __proto__ property in the console). This demonstrates that while "Ryu" as a primitive string isn’t an object, it can be wrapped in a String object to gain object-like behavior.

This automatic wrapping mechanism allows you to use object-oriented syntax (like dot notation to access properties and methods) with primitive types, making JavaScript more flexible and user-friendly.

Creating Your Own Objects: Introduction to Object-Oriented Programming

So far, we’ve explored built-in JavaScript objects and how primitive types can behave like objects. But the real power of object-oriented programming comes from the ability to create your own custom objects.

Imagine you want to represent a car in your JavaScript code. You could create an object that has properties like color, make, and registrationPlate, and methods like drive() and reverse(). This ability to model real-world entities and concepts as objects in code is the core of object-oriented programming (OOP).

Object-Oriented Programming (OOP): A programming paradigm that organizes code around “objects,” which are instances of classes and can contain data in the form of properties and code in the form of methods. OOP aims to improve code organization, reusability, and maintainability.

Object-oriented programming will be the central focus of the upcoming chapters. We will learn how to create our own objects, define their properties and methods, and explore how these objects can interact to build more complex and organized JavaScript applications.

Course Syllabus: What’s Next?

This series will guide you through the key concepts of object-oriented JavaScript. Here’s a preview of what we’ll be covering:

  • Object Literal Notation: We will start by learning how to create objects using a simple and direct syntax called object literal notation.
  • JavaScript Classes and Constructors: We’ll explore JavaScript classes, introduced in ECMAScript 6 (ES6), and constructors, which provide a more structured way to create objects, especially when you need to create multiple objects of the same type.

    ES6 (ECMAScript 2015): A major update to the JavaScript language standard, officially known as ECMAScript 2015. It introduced many new features, including classes, arrow functions, and let/const variables. Syntactic Sugar: Syntax in a programming language that is designed to make things easier to read or express. It does not add new functionality but makes the language “sweeter” for human use. In the context of JavaScript classes, they are considered syntactic sugar over the prototype-based inheritance. Constructors: Special methods within a class that are automatically called when a new object (instance) of that class is created. They are used to initialize the object’s properties.

  • Inheritance: We’ll delve into inheritance, a fundamental OOP concept that allows you to create new objects based on existing ones, inheriting their properties and methods and extending or modifying them.
  • Method Chaining: We’ll learn about method chaining, a technique that allows you to call multiple methods on an object in a single line of code, making your code more concise and readable.
  • Prototypes and Prototype Inheritance: Finally, we will go “under the hood” to understand the underlying mechanism of JavaScript’s object system: prototypes and prototype inheritance. This will provide a deeper understanding of how objects and inheritance actually work in JavaScript.

    Prototypes: Objects from which other objects inherit properties and methods in JavaScript. Every object in JavaScript has a prototype object. Prototype Inheritance: A mechanism in JavaScript where objects inherit properties and methods from their prototype objects. This forms the basis of inheritance in JavaScript.

This course is designed to provide a comprehensive understanding of object-oriented JavaScript. If you are new to JavaScript, it’s recommended to have a basic understanding of JavaScript fundamentals before starting this series. A beginner’s JavaScript playlist is available for those who need to brush up on the basics.

We look forward to embarking on this object-oriented JavaScript journey with you!


Introduction to Object-Oriented JavaScript: Encapsulation

This chapter introduces the fundamental concept of objects in JavaScript and how they can be used to organize and structure code more effectively. We will explore the idea of encapsulation and demonstrate how it helps to create manageable and maintainable code. All the code files for this series are available on the instructor’s GitHub page, linked in the video description. For specific lesson code, ensure you select the correct lesson branch from the branch dropdown menu on GitHub.

The Problem with Unorganized Code: Spaghetti Code

Let’s consider a scenario where we need to manage information for multiple users. Imagine we are building a system that requires storing user emails, names, and friend lists, along with functionalities like logging in and logging out. A common, but inefficient, approach might be to create separate variables for each piece of user information.

Initially, you might start by creating variables like user1Email, user1Name, user1Friends, user2Email, user2Name, and so on, as demonstrated in the transcript’s example. Similarly, functions like login, logout, and loginFriends might be defined separately.

However, as the application grows and the number of users and functionalities increases, this approach quickly becomes problematic. This style of coding can lead to what is commonly known as spaghetti code.

Spaghetti code: A pejorative phrase for source code that has a complex and tangled control structure, especially using many GOTO statements, jumps, exceptions, or threads. It is named such because program flow is conceptually like a bowl of spaghetti, that is tangled and twisted. In the context of this discussion, it refers to code that is disorganized, difficult to read, and hard to maintain due to lack of structure and organization.

In spaghetti code, everything is scattered, and there’s no clear structure. Information and functionalities related to a single entity, like a user, are not grouped together. This lack of centralization and containment makes the code unmanageable and difficult to debug or update.

Centralization: In programming, centralization refers to the practice of organizing related data and functionalities together in one place, making it easier to manage and understand.

Containment: In the context of object-oriented programming, containment refers to the practice of bundling related data (properties) and behaviors (methods) within an object, effectively encapsulating them.

Introduction to Objects: A Solution to Spaghetti Code

To overcome the challenges of spaghetti code and create more organized and maintainable applications, we can use objects. Objects allow us to group related data and functionalities together into a single unit.

Object: In programming, an object is a collection of data (properties) and functions (methods) that operate on that data. Objects are fundamental building blocks in object-oriented programming, allowing for the representation of real-world entities and concepts in code.

Instead of having multiple separate variables for each user attribute, we can create a single user object that holds all the information related to that user. This approach significantly improves code organization and readability.

Creating an Object Literal

In JavaScript, one of the simplest ways to create an object is using an object literal.

Object literal: A notation for describing and creating objects in JavaScript. It uses curly braces {} to define an object and key-value pairs to define its properties.

To create an object literal, we use curly braces {}. Let’s create an object to represent a user:

let user1 = {}; // This creates an empty object

This code snippet declares a variable user1 and assigns it an empty object. Currently, this object doesn’t contain any information.

Adding Properties to an Object

Objects are designed to hold data, which we refer to as properties.

Properties: Characteristics or attributes of an object. In JavaScript objects, properties are key-value pairs, where the key is a string (or Symbol) and the value can be any JavaScript data type (including other objects and functions).

To add properties to our user1 object, we can define them within the curly braces of the object literal. For example, to add an email and a name property:

let user1 = {
  email: '[email protected]',
  name: 'Ryu'
};

In this example, we have added two properties to the user1 object: email with the value '[email protected]' and name with the value 'Ryu'.

Encapsulation: Bundling Data and Behavior

The process of grouping related properties together within an object is a key concept called encapsulation.

Encapsulation: The bundling of data (properties) and methods (functions that operate on that data) that operate on that data within a single unit (object). Encapsulation helps in organizing code, hiding internal implementation details, and preventing unintended external access and modification of data.

Encapsulation is like enclosing everything related to a user within a “capsule,” the object. This makes our code more organized and logical because all user-related information is contained in one place. If we need to access or modify any user-specific data, we know it will be within the user object.

Accessing Object Properties

To access a property of an object, we can use dot notation. For example, to access the name property of the user1 object and log it to the console:

console.log(user1.name); // Output: Ryu

This line of code accesses the name property of the user1 object and prints its value, “Ryu,” to the console.

Adding Methods to Objects: Functionality

Objects not only store data but can also contain functionalities, known as methods.

Methods: Functions that are associated with an object and operate on the object’s data (properties). Methods define the behaviors or actions that an object can perform.

Methods are functions that are properties of an object. Let’s add login and logout functionalities to our user1 object.

let user1 = {
  email: '[email protected]',
  name: 'Ryu',
  login: function() {
    console.log(this.email, 'has logged in');
  },
  logout: function() {
    console.log(this.email, 'has logged out');
  }
};

Here, we have added two methods, login and logout, to the user1 object. Inside these methods, we use the keyword this.

The this Keyword

The keyword this in JavaScript refers to the current object context.

this keyword: In JavaScript, this is a special keyword that refers to the object that is currently executing the code. Its value depends on the context in which it is used. Inside an object method, this generally refers to the object itself.

In the context of these object methods, this refers to the user1 object itself. Therefore, this.email within the login and logout methods refers to the email property of the user1 object.

When these methods are called, they will log a message to the console including the user’s email address.

ES6 Method Syntax

Modern JavaScript (ECMAScript 2015 or ES6 and later) provides a shorthand syntax for defining methods within objects, making the code cleaner and more concise. Instead of writing login: function() {}, we can simply write login() {}.

Using ES6 method syntax, our user1 object becomes:

let user1 = {
  email: '[email protected]',
  name: 'Ryu',
  login() {
    console.log(this.email, 'has logged in');
  },
  logout() {
    console.log(this.email, 'has logged out');
  }
};

This is functionally equivalent to the previous version but is a more compact and preferred syntax.

Calling Object Methods

To call a method of an object, we use dot notation, similar to accessing properties, but we also include parentheses () to invoke the function.

user1.login();  // Output: [email protected] has logged in
user1.logout(); // Output: [email protected] has logged out

These lines of code call the login and logout methods of the user1 object, respectively, executing the code within those methods and producing the corresponding console output.

Running JavaScript Code in a Browser

To see the output of our JavaScript code, we can run it in a web browser. The transcript mentions using Live Server, a helpful tool for development.

Live Server: A development web server that automatically reloads the browser whenever you save changes to your code files. It is often used with text editors like VS Code to provide a live preview of web pages during development. It’s typically installed as an extension or package within the code editor.

Live Server, often available as a package or extension in code editors like VS Code, allows you to quickly view your HTML and JavaScript code in a browser and automatically refresh the page whenever you make changes to your files.

Package: In software development, a package is a collection of modules, libraries, or files that are bundled together for distribution and installation. In the context of VS Code extensions, a package refers to an extension that adds new features or functionalities to the editor.

To use Live Server (if installed), you can typically right-click on your HTML file (index.html as mentioned in the transcript) in your code editor and select “Open with Live Server.” This will open your HTML file in a browser, and any JavaScript code linked to that HTML file will be executed. The console output (from console.log statements) can be viewed by opening the browser’s developer tools (usually by right-clicking on the webpage and selecting “Inspect” or “Inspect Element” and then navigating to the “Console” tab).

Conclusion

This chapter has introduced the concept of objects in JavaScript and demonstrated how to create object literals with properties and methods. We explored the importance of encapsulation in organizing code and making it more manageable. By using objects, we can move away from disorganized “spaghetti code” and create structured, object-oriented JavaScript applications. The next step will be to explore how to update object properties and access them from outside the object.


Understanding Objects in Programming

This chapter explores the concept of objects in programming, focusing on how to create, access, and modify them. We will cover different methods for interacting with objects and discuss best practices for object creation and management.

Introduction to Objects

In programming, an object is a fundamental concept used to represent real-world entities or abstract ideas. Objects are self-contained units that bundle together data and functionality.

Object: In programming, an object is a self-contained unit that combines data (properties) and actions (methods). It is a fundamental building block for structuring code and modeling real-world entities.

The transcript begins by discussing how an object can encapsulate everything related to a user.

  • We have this object now encapsulating everything it means to be this user in one single place.
  • This is a good first step in organizing data and functionality.

Encapsulating: In object-oriented programming, encapsulation refers to bundling data (attributes or properties) and methods (functions or behaviors) that operate on that data into a single unit, or object. It helps in hiding the internal implementation details of an object and exposing only necessary interfaces.

Accessing Object Properties and Methods

Once an object is created, we need ways to access its data (properties) and actions (methods). The transcript highlights two primary methods: dot notation and square bracket notation.

Properties: Properties are characteristics or attributes associated with an object. They hold data that describes the state of the object. Methods: Methods are functions associated with an object that define the actions or operations that can be performed on or by the object.

Dot Notation

Dot notation is a straightforward and commonly used method for accessing properties and methods of an object.

Dot Notation: A syntax used to access properties or methods of an object by placing a dot (.) after the object’s name, followed by the name of the property or method.

  • We can access these properties and methods by dot notation.
  • For example, if user1 is an object variable, we can access the name property using user1.name.
  • Similarly, we can call a method like login using user1.login(). (Although login is mentioned in the transcript, it’s not explicitly implemented in the examples provided, the example given in the transcript is user 1 dot log in.)

Square Bracket Notation

Square bracket notation provides an alternative way to access object properties. This method is particularly useful when property names are not known in advance or are dynamically determined.

  • We can also access properties using square brackets.
  • Instead of user1.email, we can use user1['email'].
  • It is crucial to enclose the property name within a string inside the square brackets.

String: In programming, a string is a sequence of characters, such as letters, numbers, and symbols. It is used to represent text. In many programming languages, strings are enclosed in quotation marks.

  • For example, user1['email'] (with ‘email’ as a string) is valid, but user1[email] (without quotes) is incorrect as it would look for a variable named email.

Modifying Object Properties

Object properties are not fixed and can be updated after the object’s creation. Both dot notation and square bracket notation can be used to modify property values.

Updating Properties using Dot Notation

  • To update a property using dot notation, we can assign a new value to it.
  • For instance, to change the name property of user1 to “Yoshi”, we can write user1.name = 'Yoshi'.
  • This directly modifies the value associated with the name property within the user1 object.

Updating Properties using Square Bracket Notation

  • Square bracket notation can also be used for updating properties.
  • To change the name property of user1 to “Mario” using square bracket notation, we can write user1['name'] = 'Mario'.
  • This achieves the same result as dot notation but uses the square bracket syntax.

Dynamic Property Access with Square Brackets

The transcript highlights a key advantage of square bracket notation: dynamic property access.

Dynamic: In programming, dynamic refers to actions or processes that occur during the runtime of a program, rather than at compile time. In this context, dynamic property access means the property being accessed is determined by a variable whose value can change during program execution.

  • Square bracket notation is particularly useful when the property you want to access is determined by a variable.
  • Consider a variable prop that holds a property name as a string, like prop = 'name'.
  • We can then use user1[prop] to access the name property.
  • If the value of prop changes to 'email', then user1[prop] will access the email property.
  • This dynamic access is not possible with dot notation, as user1.prop would literally look for a property named “prop” on the object, not the property name stored in the prop variable.

Adding New Properties and Methods to Objects

Objects are mutable, meaning we can add new properties and methods to them even after they are initially created.

Adding Properties

  • We can add a new property to an object by simply assigning a value to a non-existent property name using either dot or square bracket notation.
  • For example, to add an age property to user1 and set it to 25, we can use user1.age = 25.
  • After this assignment, the user1 object will now include an age property.

Adding Methods

  • Similarly, we can add new methods to an object by assigning a function to a new property name.
  • For example, to add a logInfo method to user1, we can write:
    user1.logInfo = function() {
        console.log("User info logged");
    };
  • Now, user1.logInfo() can be called to execute the assigned function.

Best Practices for Object Structure

The transcript expresses a preference for defining all object properties and methods directly within the object literal definition.

Object Literal: A way to create objects in JavaScript using curly braces {} to define properties and methods directly within the object’s declaration. It’s a concise syntax for creating and initializing objects.

  • The speaker prefers to define all expected properties and methods at the time of object creation within the object literal.
  • Even if a property’s value is not immediately known, it’s recommended to include it in the initial definition, perhaps with a default value like null or 0.
  • This approach enhances code readability and maintainability by keeping all object definitions in one place.
  • While dynamically adding properties and methods is possible, it might make the object structure less transparent and harder to manage in larger projects.

Creating Multiple Objects: The Need for Abstraction

The transcript raises a crucial point about creating multiple objects of the same type.

  • Imagine needing to create several user objects (user1, user2, user3, etc.), each with similar properties but different values.
  • Manually creating each object by repeating the property and method definitions becomes inefficient and prone to errors.
  • This repetition highlights the need for a more streamlined way to create multiple instances of the same object type.

Instances: In object-oriented programming, an instance is a specific occurrence of an object created from a class or object literal. Each instance has its own unique set of property values but shares the same structure and methods defined by its blueprint.

  • To address this, the transcript introduces the concept of classes as a feature in ES6 (ECMAScript 2015).

ES6 (ECMAScript 2015): ES6, also known as ECMAScript 2015, is a major update to the JavaScript language standard. It introduced many new features, including classes, arrow functions, let and const keywords, and more, significantly enhancing JavaScript’s capabilities and syntax. Classes: In object-oriented programming, a class is a blueprint for creating objects. It defines the properties (data) and methods (behavior) that objects of that class will have. Classes provide a structured way to create multiple objects with similar characteristics.

  • Classes provide a more efficient and organized way to create multiple objects of the same kind, avoiding the redundancy of repeatedly writing object literals.
  • The next step, as indicated in the transcript, would be to explore how classes in ES6 can be used to create multiple user objects more effectively.

Understanding Object Creation in JavaScript: From Object Literals to Classes

This chapter explores different methods of creating objects in JavaScript, focusing on the transition from simple object literals to the more structured approach using classes. We will examine the benefits of using classes, especially when dealing with the need to create multiple objects of the same type.

The Challenge of Repetitive Object Creation

Initially, when working with JavaScript objects, you might create individual objects directly. For example, to represent users, you might create several user objects like so:

// Example of creating multiple user objects (as described in the transcript)
// (Note: This is conceptual and not actual code from the transcript, but illustrates the point)
const user1 = {
  // properties and methods for user 1
};

const user2 = {
  // properties and methods for user 2
};

// ... and so on

This approach, while functional for a small number of objects, quickly becomes inefficient and cumbersome when you need to create many objects with similar structures and functionalities. The transcript highlights this issue:

…but notice we’re repeating ourselves a lot we’ve rewritten the login and logout functions several times these properties several times and quickly this could get out of hand…

The problem is the repetition of code. Each time you create a new user object using this method, you are essentially rewriting the same properties and methods. This leads to:

  • Increased code redundancy: Writing the same code multiple times makes your codebase longer and harder to maintain.
  • Potential for errors: Repetitive coding increases the risk of making mistakes and inconsistencies between objects.
  • Difficult maintenance: If you need to change a property or method, you have to update it in every single object definition, which is time-consuming and error-prone.

Introducing a More Efficient Approach: Classes

To address the problem of repetitive object creation, JavaScript offers a more efficient and organized method using classes.

Class: In object-oriented programming, a class is a blueprint for creating objects. It defines the properties and methods that objects of that class will have.

Classes allow you to define a template for creating objects, ensuring consistency and reducing code duplication. The transcript introduces the concept of classes as a solution:

…what if instead I wanted an easier way to create a user object what if I wanted to do something like this I could say VAR user four is equal to a new user and then just pass in some parameters over here like so…

This demonstrates the core idea of using a class: creating new objects (instances) based on a predefined structure.

Classes as Blueprints

Think of a class as a blueprint for creating objects. The transcript uses an analogy to explain this concept:

…you can think of a class in JavaScript as a bit like a blueprint that describes a particular object in a non specific way for example a class that describes a car would have a color property and every car would have that property it would have a color so we define those properties in our class for the car our blueprint for the car but we don’t say what color the car is because that is specific to each individual instance of that class…

Just like a blueprint for a house specifies the general structure and features (rooms, doors, windows) without dictating specific details like paint color or furniture, a JavaScript class defines the general properties and methods that objects of that class will possess.

Instances of a Class

When you use a class to create an actual object, you are creating an instance of that class.

Instance: An instance is a specific object created from a class. It possesses the properties and methods defined by the class but can have its own unique data values for those properties.

Continuing the car analogy:

…when we create a new instance of this class we’re creating a new car object and then at that point where we create it we pass to the class a parameter which is the color of that car we can say okay we want to use the car class to create a red car or a blue car or a green car or a purple car and so forth…

Each car you create from the “car class” blueprint is an instance of the car class. They all share the same basic structure (properties like color, model, etc., and methods like start, stop, etc.), but each instance can have different values for these properties (e.g., one car instance might be red, another blue).

Similarly, with a User class, you can create multiple user instances:

…in the same way we could have a class for a user so a user has an email address a name maybe a status to say whether they’re online or offline that could be true or false they also have a login method and a logout method now whatever we create a new user we’re going to use this class and we’re going to pass in the values of these different things right here so we could create user one and pass in these values and user 2 and pass in these values…

Each user instance created from the User class will have the same properties (email, name, status) and methods (login, logout), but each instance will hold different values for these properties, representing different individual users.

Class Syntax in JavaScript (ES6)

The transcript mentions that classes were introduced in ES6 (ECMAScript 2015), bringing a more familiar syntax for object-oriented programming to JavaScript.

ES6 (ECMAScript 2015): A major update to the JavaScript language standard, ECMAScript. It introduced significant new features including classes, arrow functions, let and const keywords, and more.

…but with the release of es6 we gained a little syntactic sugar so that we can solve this problem using classes as well now under the hood classes essentially do the same thing as working with prototypes in JavaScript but some people think that classes are nicer or easier to work with…

Syntactic sugar refers to syntax that makes the language easier to read and write, without adding new functionality under the hood. In the context of classes in JavaScript, it means that classes provide a more class-based syntax on top of JavaScript’s existing prototype-based inheritance mechanism.

Prototype: In JavaScript, prototypes are the mechanism by which objects inherit features from one another. Every object in JavaScript has a prototype object, from which it inherits properties and methods.

While classes are built on top of prototypes, they offer a more conventional syntax for developers familiar with class-based languages.

Creating a Basic Class

The transcript demonstrates how to create a basic empty class in JavaScript:

…the way we do this is by saying first of all the keyword class that says we want to create a class then the name of the class itself now Convention says that we start this with an uppercase letter so I’ll call it user with a capital u then we open and close our curly braces and this right here this is now an empty user class…

Keyword: A keyword is a reserved word in a programming language that has a special meaning and cannot be used as a variable name or identifier. class is a keyword in JavaScript used to define classes.

Convention: In programming, conventions are sets of style guidelines and best practices that are widely adopted by the community to improve code readability, maintainability, and consistency. Using PascalCase (starting class names with an uppercase letter) is a common naming convention for classes in many languages, including JavaScript.

Here’s the code snippet for creating an empty User class:

class User {

}

This code declares a class named User. The class keyword initiates the class definition, followed by the class name (User), and then curly braces {} to enclose the class body. At this stage, this User class is empty, but it is now ready to be extended with properties and methods in subsequent steps.

Next Steps

The transcript concludes by setting the stage for adding properties and methods to the newly created User class in the following sections. This will further illustrate how classes function as blueprints for creating objects with defined characteristics and behaviors.


Understanding Classes and Constructors in Object-Oriented Programming

This chapter introduces the fundamental concepts of classes and constructors in object-oriented programming. We will explore how classes serve as blueprints for creating objects and how constructor functions play a crucial role in initializing these objects with specific properties.

Introduction to Classes

In object-oriented programming, a class acts as a blueprint or template for creating objects. It defines the properties and behaviors that objects of that class will possess. Think of a class as a general category, like “User,” which can have various specific instances, such as individual user profiles with unique details.

Class: A blueprint for creating objects. It defines the properties (data) and methods (behaviors) that objects of that class will have.

In the context of the transcript, we are defining a User class. Currently, this class is defined but empty, meaning it has no properties or behaviors yet.

// Class definition (initially empty)
class User {

}

Constructor Functions: The Object Builders

The first essential component within a class is the constructor function. This special function is automatically executed whenever a new object of the class is created. Its primary responsibility is to initialize the object, setting up its initial state and properties.

Constructor Function: A special method within a class that is automatically called when a new object of that class is created. Its purpose is to initialize the object’s properties.

To define a constructor function within our User class, we use the keyword constructor:

class User {
  constructor() {
    // Constructor logic will go here
  }
}

This constructor function will be invoked every time we create a new User object.

Creating Objects with the new Keyword

To create an actual instance of the User class, we use the new keyword followed by the class name and parentheses. This process is known as instantiation, and the resulting entity is called an object or an instance of the class.

Object (Instance): A specific realization of a class. It is created based on the blueprint defined by the class and holds its own unique set of data for the properties defined in the class.

For example, to create a new User object and store it in a variable named user1, we would write:

var user1 = new User();

Here, the new keyword plays a pivotal role in the object creation process. Let’s break down what happens behind the scenes when we use new.

The Role of the new Keyword

The new keyword performs three key actions when creating an object:

  • Creates a New Empty Object: First, new generates a brand new, empty object in memory. This object is initially devoid of any properties.

  • Sets this to the New Object: Inside the class, particularly within the constructor function, the keyword this is used to refer to the current object being created. The new keyword ensures that this is set to point to the newly created empty object. This allows us to work with and modify this specific object within the class.

    this Keyword: In JavaScript, this is a keyword that refers to the current object. Its value depends on the context in which it is used. Within a class constructor, this refers to the newly created object instance.

  • Calls the Constructor Function: Finally, new executes the constructor function defined within the class. This is where we can add properties to the newly created object using the this keyword.

Adding Properties to Objects in the Constructor

To give our User objects meaningful data, we need to add properties. Properties are characteristics or attributes of an object, like a user’s email or name.

Property: A characteristic or attribute of an object, representing data associated with that object. Properties are essentially key-value pairs within an object.

Inside the constructor function, we can use the this keyword to define and assign values to properties of the new object. For instance, to add an email property to our User objects, we might initially try:

class User {
  constructor() {
    this.email = "[email protected]"; // Hardcoded email
    this.name = "Ryu";           // Hardcoded name
  }
}

var user1 = new User();
var user2 = new User();

console.log(user1);
console.log(user2);

However, this approach has a significant limitation. Every User object created using this class will have the same email (“[email protected]”) and name (“Ryu”), because these values are hardcoded within the constructor. This defeats the purpose of creating distinct user objects with unique information.

Passing Arguments to the Constructor for Dynamic Properties

To create User objects with different properties, we need a way to provide specific values when creating each object. This is achieved by using arguments in the constructor function.

Argument: A value passed to a function (or constructor) when it is called. Arguments allow us to provide input to functions, making them more flexible and reusable.

We can modify our constructor to accept arguments representing the email and name:

class User {
  constructor(email, name) { // Constructor now accepts email and name arguments
    this.email = email;     // Assign the passed email to the object's email property
    this.name = name;       // Assign the passed name to the object's name property
  }
}

Now, when we create a new User object using new User(), we can pass in the desired email and name as arguments:

var user1 = new User("[email protected]", "Ryu"); // Passing arguments for user1
var user2 = new User("[email protected]", "Yoshi"); // Passing arguments for user2

console.log(user1);
console.log(user2);

In this enhanced constructor:

  • constructor(email, name): We define the constructor to accept two arguments, email and name. These act as placeholders for the values we will provide when creating a new User.
  • this.email = email;: Inside the constructor, this.email creates a property named email on the new object, and we assign the value of the email argument (passed during object creation) to this property.
  • this.name = name;: Similarly, this.name creates a property named name on the new object and assigns the value of the name argument to it.

By passing different arguments when creating user1 and user2, we ensure that each object has its own unique email and name properties.

Conclusion

This chapter has laid the groundwork for understanding classes and constructors in object-oriented programming. We learned that classes serve as templates, and constructor functions within classes are vital for creating and initializing objects. The new keyword is the mechanism for object creation, and the this keyword enables us to define and manipulate properties of the object being constructed. By utilizing arguments in the constructor, we can create diverse instances of a class, each with its own set of property values, making our code more dynamic and reusable. This foundation is crucial for building more complex and organized programs using object-oriented principles.


Defining Methods within JavaScript Classes: Enhancing User Objects

This chapter explores how to define methods within JavaScript classes to add functionalities to objects created from those classes. Building upon the concept of defining properties for user objects, we will now integrate methods such as login and logout to create more robust and interactive user representations.

Recap: User Class and Properties

In our previous discussions, we established a User class and successfully assigned properties like email and name to it. This allowed us to create user objects with specific attributes.

class User {
  constructor(email, name) {
    this.email = email;
    this.name = name;
  }
}

This code snippet demonstrates the basic structure of a User class with a constructor that initializes the email and name properties for each new user object.

Introducing Methods to the User Class

Recall that in earlier code, user objects not only possessed properties but also actions they could perform, represented as methods like login and logout. To incorporate these actions into our User class, we need to define methods within the class definition.

Methods in classes are defined outside the constructor function but still within the class block. It’s crucial to note that unlike object literals, we do not use commas to separate properties and methods within a class definition.

Object Literal: A way to create objects in JavaScript using curly braces {} and defining properties as key-value pairs.

Defining the login Method

Let’s start by defining the login method for our User class. This method will simulate a user logging into a system.

class User {
  constructor(email, name) {
    this.email = email;
    this.name = name;
  }
  login() {
    console.log(this.email, 'just logged in');
  }
}

In this code:

  • We define the login() method directly within the User class, after the constructor.
  • Inside the login() method, we use console.log() to display a message indicating that the user has logged in.
  • The keyword this is used to access the email property of the specific user object that is calling the login() method.

this keyword: In JavaScript, this refers to the current object. Its value depends on the context in which it is used. Within a class method, this typically refers to the instance of the class (the object being created).

Defining the logout Method

Similarly, we can define a logout method for the User class to represent a user logging out.

class User {
  constructor(email, name) {
    this.email = email;
    this.name = name;
  }
  login() {
    console.log(this.email, 'just logged in');
  }
  logout() {
    console.log(this.email, 'just logged out');
  }
}

The logout() method is structured similarly to the login() method, displaying a message indicating the user has logged out, again using this.email to access the email property of the current user object.

Creating User Objects and Invoking Methods

Now that we have defined methods within our User class, let’s see how to create user objects and utilize these methods.

We use the new keyword followed by the class name and constructor arguments to create new instances of the User class.

Instance: In object-oriented programming, an instance is a specific realization of an object. It is created from a class and has its own set of values for the properties defined in the class.

const user1 = new User('[email protected]', 'Mario');
const user2 = new User('[email protected]', 'Yoshi');

These lines create two User objects, user1 and user2, each with their respective email and name properties.

To invoke the methods associated with these user objects, we use dot notation followed by the method name and parentheses.

user1.login(); // Invokes the login method on user1
user2.logout(); // Invokes the logout method on user2

Executing this code will produce the following output in the console:

[email protected] just logged in
[email protected] just logged out

This demonstrates that the login() method is executed for user1 and the logout() method is executed for user2, confirming that our methods are correctly associated with the user objects.

Examining Object Properties and Methods

When we inspect a user object in the console, we can observe its properties and methods. For example, typing user1 in the console and expanding its properties, we can see the email and name properties. Furthermore, within the __proto__ (prototype) property of the object, we can find the defined methods: login and logout.

Prototype (or __proto__): In JavaScript, prototypes are the mechanism by which objects inherit features from one another. Every object in JavaScript has a prototype, which is itself an object. When you try to access a property of an object, and the object doesn’t have that property, JavaScript looks up the prototype chain until it finds the property or reaches the end of the chain.

This __proto__ property is crucial because it shows that the methods are not directly stored on each individual user object instance itself, but rather they are defined once in the class and made available to all instances through the prototype chain. This is a key efficiency benefit of using classes over object literals for creating multiple objects with shared functionalities.

Efficiency and Encapsulation through Classes

Defining methods within a class is significantly more efficient compared to defining methods separately for each object, as would be the case with object literal notation. With classes, we define the methods once within the class definition, and all instances of that class automatically inherit these methods through the prototype.

Encapsulation: In object-oriented programming, encapsulation refers to the bundling of data (properties) and methods that operate on that data within a single unit (an object). It also involves restricting direct access to some of the object’s components, which can be achieved through access modifiers (though not explicitly demonstrated in this basic example). Encapsulation helps to organize code, prevent accidental modification of data, and improve code maintainability.

By encapsulating both properties and methods within the User class, we create a cohesive and well-structured representation of a user. This approach promotes code reusability, maintainability, and a more organized way of structuring JavaScript applications.

Next Steps: Method Chaining

Having established how to define methods within classes and create instances with these methods, we can further explore advanced techniques. In the upcoming chapter, we will delve into a concept called “method chaining,” which allows for more concise and expressive code when working with object methods.


Method Chaining in Object-Oriented Programming

This chapter explores the concept of method chaining in object-oriented programming, using JavaScript as the primary example language. Method chaining is a powerful technique that enhances code readability and conciseness by allowing multiple method calls on an object to be chained together in a single statement. We will delve into understanding how method chaining works, the underlying mechanisms that enable it, and the benefits it offers in software development.

Introduction to Method Chaining

In object-oriented programming, objects encapsulate data (properties) and behavior (methods). Often, we need to perform a sequence of operations on an object. Traditionally, this involves calling methods on separate lines, which can sometimes lead to verbose and less readable code. Method chaining provides an elegant alternative, allowing you to call multiple methods sequentially on the same object in a single, fluent line of code.

Consider a scenario where you have a User object with methods like login(), updateScore(), and logout(). Without method chaining, you might call these methods like this:

user1.login();
user1.updateScore();
user1.logout();

Method chaining aims to condense this into a more streamlined approach:

user1.login().updateScore().logout();

This chapter will explain how to implement method chaining and why the initial attempt might not work as expected.

Understanding the Initial Challenge: undefined Return Values

Let’s examine a basic User class and its methods to understand the problem method chaining addresses.

class User {
  constructor(email, name) {
    this.email = email;
    this.name = name;
    this.score = 0; // Initial score for every user
  }

  login() {
    console.log(this.email, 'just logged in');
  }

  logout() {
    console.log(this.email, 'just logged out');
  }

  updateScore() {
    this.score++;
    console.log(this.email, 'score is now', this.score);
  }
}

const user1 = new User('[email protected]', 'Ryu');
const user2 = new User('[email protected]', 'Yoshi');

In this code, we have a User class with a constructor to initialize user properties like email, name, and score. It also includes methods for login(), logout(), and updateScore().

If we attempt to chain methods directly in their current form:

user1.login().logout(); // Attempting method chaining

We encounter an error: TypeError: Cannot read property 'logout' of undefined.

This error occurs because, by default, methods in JavaScript (and many other languages) that don’t explicitly return a value implicitly return undefined.

Undefined: In JavaScript, undefined is a primitive value automatically assigned to variables that have been declared but not yet assigned a value. Functions also return undefined if they do not have an explicit return statement.

When user1.login() is called, it executes its code (logging the login message) but returns undefined. Therefore, the subsequent part of the chain, .logout(), is being called on undefined, which is not an object and does not have a logout method, resulting in the error.

The Solution: Returning this for Method Chaining

To enable method chaining, we need each method in the chain to return the object itself after performing its operation. In object-oriented programming, this refers to the current instance of the object.

Instance: In object-oriented programming, an instance is a specific, concrete realization of a class. It’s an object that is created from a class blueprint. For example, user1 and user2 are instances of the User class.

By returning this from each method, we ensure that the next method in the chain is called on the same object instance. Let’s modify the User class methods to return this:

class User {
  // ... (constructor remains the same)

  login() {
    console.log(this.email, 'just logged in');
    return this; // Return the User instance
  }

  logout() {
    console.log(this.email, 'just logged out');
    return this; // Return the User instance
  }

  updateScore() {
    this.score++;
    console.log(this.email, 'score is now', this.score);
    return this; // Return the User instance
  }
}

Now, each method (login, logout, updateScore) explicitly returns this. Let’s re-examine the method chaining example:

user1.login().updateScore().updateScore().logout();

Step-by-step execution:

  1. user1.login(): The login() method is called on user1. It logs the login message to the console and then returns this, which is the user1 object itself.

  2. .updateScore() (first call): Because login() returned user1, the next method .updateScore() is called on user1. It increments user1’s score, logs the updated score, and then returns this (again, user1).

  3. .updateScore() (second call): Again, because the previous updateScore() returned user1, the next .updateScore() is called on user1. It increments the score again, logs the updated score, and returns this.

  4. .logout(): Finally, the logout() method is called on user1 (returned from the previous updateScore() call). It logs the logout message and returns this.

Because each method returns the object instance, we can successfully chain multiple method calls together.

Demonstration and Output

Let’s execute the chained method calls and observe the output:

user1.login().updateScore().updateScore().logout();
user2.updateScore().updateScore();

Console Output:

[email protected] just logged in
[email protected] score is now 1
[email protected] score is now 2
[email protected] just logged out
[email protected] score is now 1
[email protected] score is now 2

As you can see, the methods are executed in the order they are chained, and each operation is performed on the intended object instance. user1 logs in, has its score updated twice, and then logs out. user2 has its score updated twice. Each user maintains its own independent score property, demonstrating that method chaining operates on the same object throughout the chain.

Benefits of Method Chaining

Method chaining offers several advantages:

  • Improved Readability: Chained method calls can make code more concise and easier to read, especially when performing a sequence of operations on an object. It reflects a more fluent and natural flow of actions.
  • Reduced Verbosity: Method chaining reduces the need for intermediate variables and multiple lines of code, leading to less verbose code.
  • Enhanced Code Flow: Chaining visually represents the sequential execution of operations on an object, making the code flow clearer and more intuitive.
  • Fluent Interface Design: Method chaining is a key aspect of creating fluent interfaces or APIs (Application Programming Interfaces). Fluent interfaces are designed to be easy to read and use, often mimicking natural language.

Method: In object-oriented programming, a method is a function that is associated with an object. Methods define the behaviors or actions that an object can perform.

Property: In object-oriented programming, a property is a characteristic or attribute of an object. Properties hold data that describes the state of an object.

Object: In object-oriented programming, an object is a self-contained unit that combines data (properties) and behavior (methods). Objects are instances of classes.

Class: In object-oriented programming, a class is a blueprint or template for creating objects. It defines the properties and methods that objects of that class will have.

Conclusion

Method chaining is a valuable technique in object-oriented programming that enhances code clarity and conciseness. By ensuring that methods return the object instance (this), we enable fluent and readable code that efficiently performs sequences of operations. Understanding and utilizing method chaining can significantly improve the design and maintainability of your object-oriented applications. Practice implementing method chaining in your classes to fully grasp its benefits and integrate it into your programming style.


Chapter: Introduction to Inheritance in Object-Oriented Programming

1. Understanding Inheritance: Building Upon Existing Structures

In software development, particularly within the paradigm of Object-Oriented Programming (OOP), we often encounter situations where different types of objects share common characteristics but also possess unique features. Imagine building a system for a website with various user roles. You might have standard users, administrators, moderators, and so on. While each role is distinct, they all share fundamental user attributes, such as a name, email address, and the ability to log in.

Instead of creating completely separate code structures for each user type, OOP offers a powerful mechanism called inheritance. Inheritance allows us to create new classes that inherit properties and behaviors from existing classes. This promotes code reusability, reduces redundancy, and establishes a clear hierarchy between different types of objects.

Class: In object-oriented programming, a class is a blueprint for creating objects. It defines the properties (data) and methods (behaviors) that objects of that class will have.

Object (Instance): An instance is a specific, concrete realization of a class. It’s a unique entity created based on the class blueprint, holding specific values for the properties defined by the class.

2. Scenario: Users and Administrators

Let’s consider a practical example. Suppose we are developing a website that requires user accounts. We can start by defining a User class. This class will encapsulate the common attributes and actions that all users on our website will have.

// Example User Class (Conceptual - based on transcript's context)
class User {
  constructor(email, name) {
    this.email = email;
    this.name = name;
    this.score = 0;
  }

  login() {
    console.log(`${this.name} logged in`);
  }

  logout() {
    console.log(`${this.name} logged out`);
  }

  updateScore(points) {
    this.score += points;
    console.log(`${this.name}'s score updated to ${this.score}`);
  }
}

// Creating instances of the User class
const user1 = new User('[email protected]', 'Mario');
const user2 = new User('[email protected]', 'Luigi');

// Demonstrating methods
user1.login(); // Mario logged in
user2.updateScore(10); // Luigi's score updated to 10

As demonstrated above, we can create individual instances of the User class, each representing a unique user with their own email, name, and score. These users can then utilize the defined methods such as login, logout, and updateScore. Furthermore, we could perform method chaining, calling multiple methods on the same object sequentially.

Instance: An instance is a specific, concrete realization of a class. It’s a unique entity created based on the class blueprint, holding specific values for the properties defined by the class.

Method: A method is a function that is associated with an object. It defines actions that an object can perform or operations that can be performed on an object’s data.

Method Chaining: Method chaining is a programming technique where multiple methods are called on the same object in a sequence in a single line of code. Each method call returns an object, allowing the next method to be called immediately on the result.

3. Introducing Administrators: A Specialized User Type

Now, let’s introduce a new requirement: administrators. Administrators are special users who have all the capabilities of regular users, but also possess additional administrative privileges. For instance, an administrator might have the ability to delete user accounts.

We could simply modify the existing User class to include administrator-specific functionalities. However, this approach is not ideal. We don’t want every user to have administrative powers. A better solution is to create a new class specifically for administrators.

4. The Power of Inheritance: Creating the Admin Class

Instead of rewriting all the code from the User class for our Admin class and then adding the administrative features, we can leverage inheritance. We can create an Admin class that inherits from the User class. This means the Admin class will automatically possess all the properties and methods of the User class. Then, we can simply add the extra functionalities specific to administrators to the Admin class.

Inheritance: Inheritance is a fundamental principle in object-oriented programming where a new class (subclass or derived class) inherits properties and methods from an existing class (superclass or base class). This promotes code reuse and establishes a hierarchical relationship between classes.

5. Implementing Inheritance with extends

In JavaScript (and many other object-oriented languages), we use the extends keyword to establish inheritance. Let’s create our Admin class that inherits from the User class:

class Admin extends User {
  // Admin class definition will go here
}

The extends User clause signifies that Admin is a subclass of User. This simple line of code establishes the inheritance relationship.

Extends: In the context of class inheritance, extends is a keyword used in many object-oriented programming languages (including JavaScript) to indicate that a class is inheriting from another class. It establishes an “is-a” relationship, where the subclass “is a” type of the superclass.

6. Automatic Inheritance of Properties and Methods

By using extends, the Admin class automatically inherits the constructor, properties (like email, name, score), and methods (like login, logout, updateScore) from the User class. This means when we create a new Admin instance, it will automatically have all of these characteristics and functionalities.

Constructor: A constructor is a special method within a class that is automatically called when a new object (instance) of that class is created. Its primary purpose is to initialize the object’s properties and set up the initial state of the object.

In fact, if we don’t define a constructor in the Admin class, it will automatically use the constructor of the User class. This is exactly what we observe in the transcript example.

7. Adding Specialized Methods: The deleteUser Function

Now, let’s add a method specific to administrators: the ability to delete users. We will define a deleteUser method within the Admin class. To implement this, we first need a way to store our users. Let’s assume we have an array called users that holds instances of the User class.

let users = [user1, user2]; // Array to store user instances

class Admin extends User {
  deleteUser(userToDelete) {
    // Functionality to delete a user from the users array
    this.users = users.filter(currentUser => {
      return currentUser.email !== userToDelete.email;
    });
    users = this.users; // Update the global users array (for demonstration)
  }
}

In the deleteUser method, we use the JavaScript filter() method to create a new array containing only the users whose email does not match the email of the user we want to delete (userToDelete). This effectively removes the specified user from the array.

Function: In programming, a function is a block of organized, reusable code that is used to perform a single, related action. Functions are essential for modularizing code and making it more readable and maintainable.

Array: An array is a data structure that stores a collection of elements of the same or different data types in a contiguous memory location. Elements in an array are ordered and can be accessed using their index.

Filter Method: The filter() method is a built-in JavaScript array method that creates a new array containing only the elements from the original array that pass a certain condition. This condition is defined by a function that is provided as an argument to the filter() method.

Argument: In programming, an argument is a value that is passed to a function (or method) when it is called. Arguments provide input to the function, allowing it to operate on specific data.

Parameter: A parameter is a variable in the declaration of a function. It acts as a placeholder for an argument that will be passed to the function when it is called.

The filter() method utilizes an ES6 arrow function for concise syntax. The arrow function currentUser => { return currentUser.email !== userToDelete.email; } takes each currentUser from the users array as a parameter and returns true if the currentUser.email is not equal to userToDelete.email, and false otherwise. Only users for which the function returns true are kept in the new filtered array.

ES6 Arrow Function: An ES6 arrow function is a more concise syntax for writing function expressions in JavaScript, introduced in ECMAScript 2015 (ES6). They offer a shorter way to define functions, especially for simple, single-expression functions.

8. Demonstrating Inheritance and Specialized Functionality

Let’s create an Admin instance and demonstrate the deleteUser method in action:

const adminUser = new Admin('[email protected]', 'Shawn');

console.log("Initial Users Array:", users); // Output: Initial Users Array: [User, User]

adminUser.deleteUser(user2); // Attempt to delete user2 (Luigi)

console.log("Users Array After Deletion:", users); // Output: Users Array After Deletion: [User] (Luigi is removed)

// Demonstrate inherited method
adminUser.login(); // Shawn logged in

// Attempting to use deleteUser on a regular User instance (will result in error)
// user1.deleteUser(user2); // Error: user1.deleteUser is not a function

As you can see, the adminUser instance, created from the Admin class, successfully deleted user2 from the users array using the deleteUser method. Furthermore, adminUser can still use the inherited login() method from the User class. However, a regular user1 instance does not have the deleteUser method, demonstrating that this functionality is exclusive to administrators.

9. Prototypal Inheritance (Behind the Scenes)

While not explicitly detailed in the transcript, it’s important to understand that JavaScript uses prototypal inheritance. When the transcript mentions “proto” and “proto again”, it is referring to the prototype chain. In essence, when you access a property or method on an object, JavaScript first looks at the object itself. If it’s not found, it then looks up the prototype chain. In our Admin example, the prototype chain of an Admin instance would lead to the Admin class prototype, then to the User class prototype, and so on. This is how inheritance is implemented under the hood in JavaScript. A detailed explanation of prototypes is a more advanced topic but is the underlying mechanism enabling inheritance.

Prototype (Proto): In JavaScript, prototypes are the mechanism by which objects inherit features from one another. Every object in JavaScript has a prototype, which is itself an object. When you try to access a property of an object, and the object itself doesn’t have that property, JavaScript will look up the prototype chain to find the property.

10. Benefits of Inheritance and Code Reusability

Inheritance offers several key advantages in object-oriented programming:

  • Code Reusability: Avoids code duplication by inheriting common properties and methods from a superclass. We didn’t have to rewrite the login, logout, and updateScore methods for the Admin class.
  • Maintainability: Changes to the superclass (e.g., User) are automatically reflected in all subclasses (e.g., Admin), reducing maintenance effort.
  • Extensibility: Easily extend existing classes with new functionalities without modifying the original class, as demonstrated by adding deleteUser to Admin.
  • Organization and Clarity: Creates a clear hierarchical structure, making code easier to understand and manage.

11. Conclusion

Inheritance is a powerful tool in object-oriented programming that promotes code reusability, maintainability, and extensibility. By understanding and utilizing inheritance, developers can create more organized, efficient, and robust software systems. In this chapter, we explored the concept of inheritance using a practical example of User and Admin classes in JavaScript, demonstrating how to create specialized classes that build upon existing ones, inheriting common functionalities and adding unique features as needed.


Understanding JavaScript Prototypes: Emulating Classes Before ES6

Introduction: Classes as Syntactic Sugar

In earlier lessons, we explored the concept of classes in JavaScript, utilizing the ES6 class syntax to create objects. However, it’s crucial to understand that JavaScript’s approach to classes is somewhat unique. Underneath the familiar class keyword, JavaScript operates using a mechanism called the prototype model.

Syntactic Sugar: This term refers to syntax within a programming language that is designed to make things easier to read or to express, but does not add new functionality. It’s an alternative way to write something that could already be written in another way. In this context, ES6 classes are syntactic sugar because they provide a class-like syntax on top of JavaScript’s prototype-based inheritance.

Essentially, ES6 classes are a layer of abstraction, or “syntactic sugar,” built upon the underlying JavaScript prototype model. Before the introduction of the class keyword in ES6, developers emulated classes directly using this prototype model. Even when using the class keyword today, JavaScript internally still leverages the prototype model to achieve class-like behavior.

Why Learn the Prototype Model?

You might wonder, “If we have the class keyword, why should I bother learning about the prototype model?” While it’s possible to develop in JavaScript using only the class syntax, understanding the prototype model offers significant advantages:

  • Increased Flexibility: Knowledge of prototypes grants you greater control and flexibility when working with objects.
  • Enhanced Debugging Skills: Understanding the underlying mechanism aids in debugging code, especially when encountering complex object interactions.
  • Compatibility with Legacy Code: You may encounter code written by other developers that utilizes the prototype model directly, particularly in older JavaScript codebases. Familiarity with prototypes ensures you can understand and maintain such code.
  • Deeper Understanding of JavaScript: Learning the prototype model provides a more comprehensive and profound understanding of how JavaScript objects truly function.

Therefore, while using classes directly is often sufficient, exploring the prototype model is essential for becoming a more proficient and well-rounded JavaScript developer. It empowers you to understand the core mechanics of object creation and manipulation in JavaScript.

Emulating Classes with Constructor Functions

Before ES6 classes, JavaScript developers used constructor functions to create objects and simulate class-like behavior. Let’s explore how this was done and how it relates to the modern class syntax.

Constructor Function: In JavaScript, a constructor function is a function designed to be used with the new keyword to create and initialize objects. It sets up the initial state of an object and is often used to define properties and methods for objects of a particular “type.”

To illustrate, we will recreate the functionality of a User class without using the class keyword. We will start by defining a constructor function named User (with a capital ‘U’ by convention, to indicate it’s a constructor).

function User(email, name) {
  // Constructor function body
}

This User function will serve as our constructor. Just like the constructor method within a class, this function will be responsible for setting up the properties of new User objects.

Creating Objects with the new Keyword

To create new objects based on our User constructor function, we utilize the new keyword, just as we do with classes.

var user1 = new User('[email protected]', 'Mario');
var user2 = new User('[email protected]', 'Yoshi');

The new keyword performs several crucial actions:

  1. Creates a New Empty Object: It first creates a brand new, empty JavaScript object.
  2. Sets the this Context: It then sets the this keyword inside the constructor function to point to this newly created empty object.
  3. Calls the Constructor Function: It then calls the User constructor function, passing in the provided arguments ('[email protected]', 'Mario' for user1, and similarly for user2).

Within the User constructor function, the this keyword now refers to the newly created object. We can use this to attach properties to this object.

Defining Object Properties in the Constructor

Inside the User constructor function, we can define properties for each User object using the this keyword. Let’s add email, name, and online properties:

function User(email, name) {
  this.email = email;
  this.name = name;
  this.online = false; // Default online status is false
}

In this code:

  • this.email = email; assigns the value of the email parameter passed to the constructor to the email property of the newly created object.
  • this.name = name; does the same for the name parameter and the name property.
  • this.online = false; sets the online property to false for every new User object by default.

Now, when we create user1 and user2 using new User(...), each object will have these properties initialized.

Adding Methods to Objects

We can also add methods to objects created with constructor functions. One way is to define methods directly within the constructor function, also using the this keyword. Let’s add a login method:

function User(email, name) {
  this.email = email;
  this.name = name;
  this.online = false;
  this.login = function() {
    console.log(this.email + ' has logged in');
  };
}

Here, this.login = function() { ... }; defines a function and assigns it as the login method of the object being constructed. Inside the login method, this.email refers to the email property of the specific User object on which the method is called.

Testing the Constructor and Methods

Let’s test our User constructor function and the login method:

var user1 = new User('[email protected]', 'Mario');
var user2 = new User('[email protected]', 'Yoshi');

console.log(user1);
user2.login();

When we run this code, we will observe the following:

  • console.log(user1); will output the user1 object with its properties (email, name, online, and login method) to the console.
  • user2.login(); will call the login method on the user2 object, resulting in the output “[email protected] has logged in” in the console.

This demonstrates that we have successfully emulated a class-like structure using a constructor function, creating objects with properties and methods. While this approach works, it’s not the most efficient way to handle methods in JavaScript prototypes. In the next section, we will explore the more conventional and efficient method of attaching methods using the prototype property of constructor functions.

Prototype Property: In JavaScript, every function automatically has a prototype property that is an object. When a function is used as a constructor with the new keyword, the newly created object inherits properties and methods from the constructor function’s prototype object. This is the foundation of prototypal inheritance in JavaScript.


Understanding Prototypes in JavaScript: An Alternative to Class Emulation

This chapter delves into the concept of prototypes in JavaScript, offering an alternative approach to method attachment compared to directly embedding methods within constructor functions, as discussed in previous contexts. We will explore the prototype property and its significance in object-oriented programming in JavaScript.

Constructor Functions and Method Attachment: A Recap

In JavaScript, constructor functions serve as blueprints for creating objects. Previously, we examined how to emulate classes using these constructor functions and attach methods directly to them. For instance, consider a User constructor function:

function User(email, name) {
  this.email = email;
  this.name = name;
  this.online = false;
  this.login = function() { // Method attached directly
    this.online = true;
    console.log(`${this.email} has logged in`);
  }
}

const user1 = new User('[email protected]', 'Mario');
const user2 = new User('[email protected]', 'Luigi');

In this approach, every User object created possesses its own copy of the login method. While functional, JavaScript offers a more efficient mechanism for method sharing through prototypes.

Introducing the Prototype Property

JavaScript objects possess a special internal property, often referred to as __proto__ (or simply “proto” for brevity), which plays a crucial role in method resolution. Let’s examine this using an array example.

const nums = [1, 2, 3, 4, 5];
console.log(nums);

Upon inspecting nums in the browser’s console, you’ll observe a __proto__ property. Expanding this property reveals a collection of methods associated with arrays, such as sort, push, pop, and many others.

__proto__ (or “proto”): A property on objects that points to the prototype of its constructor function. It’s the mechanism for prototype inheritance, allowing objects to access properties and methods defined on their prototype.

These methods within the __proto__ are not directly defined on the nums array instance itself but are inherited from its prototype. Remarkably, to use these methods, we don’t need to explicitly access the __proto__ property. JavaScript automatically handles this process.

nums.sort(); // We can directly call 'sort' on the 'nums' array
console.log(nums); // Output: [1, 2, 3, 4, 5] (already sorted, no change in this example)

This automatic lookup and execution of methods from the prototype is known as proxying.

Proxying: In the context of prototypes, proxying refers to JavaScript’s behavior of automatically searching for methods and properties in an object’s prototype chain if they are not found directly on the object itself.

Prototypes and Object Types

The concept of prototypes extends beyond arrays. Every object type in JavaScript, including built-in types like Date and RegExp, as well as custom object types we create, has a prototype. You can think of a prototype as a blueprint or a “map” specific to an object type, containing shared functionalities (methods) for all objects of that type.

Prototype: In JavaScript, a prototype is an object associated with every function and object by default. For functions, the prototype property is a blueprint for objects created using that function as a constructor. For objects, __proto__ points to its constructor’s prototype, enabling inheritance.

For any object created of a particular type, its __proto__ property will point to the corresponding prototype object. This connection enables the object to access and utilize the methods defined within that prototype.

Consider our User constructor function. Even without explicitly defining a prototype, it inherently possesses one. However, in our initial example, methods like login were attached directly to each User instance, not to the prototype.

Distinguishing Between Constructor Function prototype and Instance __proto__

It is crucial to differentiate between the prototype property of a constructor function and the __proto__ property of an object instance.

  • Constructor Function’s prototype: Accessible via User.prototype (where User is the constructor function). This is where we define methods that we want to be shared among all instances of User.
  • Object Instance’s __proto__: Accessible via user1.__proto__ (where user1 is an instance of User). This property points to the prototype object of the User constructor function. It is the mechanism through which instances inherit properties and methods.

It’s important to note that you cannot directly access the prototype property on an instance of an object (e.g., user1.prototype is undefined). The prototype property belongs to the constructor function itself.

Attaching Methods to the Prototype

To leverage the benefits of prototypes, we can attach methods to the prototype property of our User constructor function instead of directly within the constructor.

function User(email, name) {
  this.email = email;
  this.name = name;
  this.online = false;
}

User.prototype.login = function() {
  this.online = true;
  console.log(`${this.email} has logged in`);
};

User.prototype.logout = function() {
  this.online = false;
  console.log(`${this.email} has logged out`);
};

const user3 = new User('[email protected]', 'Yoshi');
const user4 = new User('[email protected]', 'Peach');

user3.login(); // Works!
user4.logout(); // Works!

console.log(user3); // Inspect user3 in the console

In this revised approach:

  1. We define the login and logout methods as functions and assign them to User.prototype.login and User.prototype.logout, respectively.
  2. Inside these methods, the this keyword refers to the instance of the User object upon which the method is called (e.g., user3 when user3.login() is invoked).

this keyword: In JavaScript, within a method or function, this refers to the object that is currently executing the function or method. Its value is determined by how the function is called.

Now, when we inspect user3 in the console, we observe that the login and logout methods are no longer directly attached to the user3 object itself. Instead, they reside within the __proto__ property, pointing to the User.prototype. Yet, we can still call these methods directly on user3 (e.g., user3.login()) due to JavaScript’s prototype proxying.

Advantages of Using Prototypes for Methods

Employing prototypes for method attachment offers several advantages:

  • Memory Efficiency: Methods are defined once on the prototype and shared by all instances. This avoids redundant method copies in each object, saving memory, especially when creating numerous objects.
  • Prototype Inheritance: Prototypes are fundamental to JavaScript’s inheritance model. By using prototypes, we pave the way for implementing prototype inheritance, which allows creating specialized object types that inherit properties and methods from more general types. This will be explored in detail in subsequent discussions.

Prototype inheritance: A mechanism in JavaScript that allows objects to inherit properties and methods from other objects (their prototypes). It’s a core concept for code reuse and establishing relationships between object types.

Prototypes and the class Keyword

It is worth noting that when using the class keyword in modern JavaScript to define classes, the underlying mechanism still relies on prototypes. The class syntax provides a more syntactically convenient way to work with prototypes and constructor functions, abstracting away some of the manual prototype manipulation. However, understanding prototypes remains crucial for comprehending JavaScript’s object model and inheritance.

In the next chapter, we will delve deeper into prototype inheritance and explore how it enables code reuse and the creation of hierarchical object structures.


Understanding Prototype Inheritance in JavaScript: A Deep Dive

Introduction

This chapter explores the concept of prototype inheritance in JavaScript, a fundamental mechanism for code reuse and object-oriented programming. We will transition from the idea of classes, briefly touched upon in previous lessons, to understanding how similar inheritance patterns can be achieved using constructor functions and prototypes.

The goal is to create an Admin “class” (using constructor functions) that inherits properties and methods from a User “class”. This demonstrates how to extend functionality and build upon existing code structures without directly using ES6 classes, providing a deeper understanding of JavaScript’s underlying inheritance model.

Creating the Admin “Class” (Constructor Function)

We begin by defining a constructor function for our Admin “class,” mirroring the structure of the existing User “class”.

function Admin() {
  // Constructor logic will be added here
}

This Admin function, when invoked with the new keyword, will act as a constructor, creating new admin objects.

Constructor Function: In JavaScript, a constructor function is used to create objects. When called with the new keyword, it initializes a new object and sets the this keyword to refer to that new object instance.

To create a new Admin object instance, we can use the following syntax:

let admin = new Admin();

However, at this stage, our Admin constructor is empty and doesn’t yet inherit anything from the User “class.”

Inheriting Properties from the User “Class”

We want our Admin objects to possess the same properties as User objects (email, name, and online status) and be initialized in a similar way. Instead of rewriting the initialization logic, we can leverage the existing User constructor.

To achieve this, we will call the User function within the Admin constructor using the apply method.

apply() method: The apply() method is a function available to all JavaScript functions. It allows you to call a function with a specified this value and arguments provided as an array (or array-like object).

Here’s how we modify the Admin constructor:

function Admin(email, name) {
  User.apply(this, arguments); // Call the User constructor, setting 'this' context to the new Admin object
  this.role = 'super admin'; // Add admin-specific property
}

Explanation:

  • User.apply(this, arguments);: This line is crucial for inheritance.

    • User.apply: We are calling the User function.
    • (this, arguments): We are using apply to control the this context and pass arguments.
      • this: The first argument to apply sets the this value inside the User function to be the newly created Admin object. This ensures that when User sets properties like this.email and this.name, they are set on the Admin object, not on the User constructor itself.
      • arguments: The second argument is an array-like object containing all the arguments passed to the Admin constructor (email and name in this case). apply then passes these arguments to the User function.
  • this.role = 'super admin';: After inheriting the User properties, we add a property specific to Admin objects, demonstrating how to extend the inherited structure.

Now, when we create an Admin object, it will inherit the email, name, and online properties from the User constructor and also have the role property:

let admin = new Admin('[email protected]', 'Shawn');
console.log(admin);

This will output an Admin object with email, name, online (inherited from User), and role properties.

The Rest Parameter (...args) for Flexible Arguments

To make the Admin constructor more flexible and handle a variable number of arguments passed to the User constructor, we can use the rest parameter syntax.

Rest Parameter: The rest parameter (...) allows a function to accept an indefinite number of arguments as an array. It is placed as the last parameter in a function definition and gathers all remaining arguments into a single array.

Modify the Admin constructor to use the rest parameter:

function Admin(...args) {
  User.apply(this, args);
  this.role = 'super admin';
}

Explanation:

  • ...args: This rest parameter in the Admin function definition collects all arguments passed to Admin into an array named args.
  • User.apply(this, args);: We now pass the args array directly to User.apply. This works because apply expects an array (or array-like object) of arguments as its second parameter.

This approach makes the Admin constructor more adaptable if the User constructor were to accept more parameters in the future.

Inheriting Methods (Prototype Inheritance)

Currently, our Admin objects inherit properties but not methods like login and logout that are defined on the User.prototype. To inherit these methods, we need to establish prototype inheritance.

Prototype: In JavaScript, every object has a prototype. It’s another object that the original object inherits properties and methods from. For constructor functions, the prototype property of the function is used to set the prototype of objects created with that constructor.

Object.create(): Object.create() is a method that creates a new object with a specified prototype object.

We set the Admin.prototype to inherit from User.prototype using Object.create():

Admin.prototype = Object.create(User.prototype);

Explanation:

  • Admin.prototype = Object.create(User.prototype);: This line establishes the prototype chain.
    • Admin.prototype: We are setting the prototype of the Admin constructor function.
    • Object.create(User.prototype): We create a new object whose prototype is User.prototype. This new object becomes the prototype of Admin.

Now, any object created using the Admin constructor will inherit methods from User.prototype through its prototype chain. We can verify this by attempting to call the login method on an Admin object:

admin.login(); // This will now work because Admin.prototype inherits from User.prototype

Adding Admin-Specific Methods

We can extend the functionality of Admin objects by adding methods directly to Admin.prototype. These methods will be available only to Admin objects and not to User objects.

Let’s add a deleteUser method to the Admin prototype:

Admin.prototype.deleteUser = function(u) {
  users = users.filter(user => user.email !== u.email);
};

Explanation:

  • Admin.prototype.deleteUser = function(u) { ... };: We are adding a new method called deleteUser to the Admin.prototype.
  • function(u): This method takes a user object (u) as a parameter, representing the user to be deleted.
  • users = users.filter(user => user.email !== u.email);: This line uses the filter method to create a new users array containing only users whose email does not match the email of the user to be deleted (u). This effectively removes the specified user from the users array.

filter() method: The filter() method is an array method in JavaScript that creates a new array containing only the elements from the original array that pass a certain test (provided as a function).

ES6 Arrow Function: The syntax user => user.email !== u.email is an ES6 arrow function, a concise way to write anonymous functions. In this case, it’s equivalent to function(user) { return user.email !== u.email; }.

Now, Admin objects have the deleteUser method in addition to the inherited login and logout methods, while User objects only have login and logout.

admin.deleteUser(user2); // Only admins can delete users

Prototype Chain

When we look at the prototype of an Admin object in the browser’s developer console, we see a chain of prototypes:

  • The [[Prototype]] (often displayed as __proto__ or [[Prototype]] in browser consoles or represented as .__proto__ in code) of an Admin object points to Admin.prototype.
  • Admin.prototype itself points to User.prototype because of Admin.prototype = Object.create(User.prototype);.
  • User.prototype in turn points to Object.prototype, the base prototype for most JavaScript objects.

Prototype Chain: The prototype chain is the mechanism in JavaScript that enables inheritance. When you try to access a property or method of an object, JavaScript first looks for it directly on the object. If not found, it then searches the object’s prototype. This process continues up the prototype chain until the property or method is found, or the end of the chain (usually Object.prototype) is reached.

[[Prototype]] (or __proto__ or .proto in console): This is an internal property of every object in JavaScript that points to its prototype. It is the mechanism that implements the prototype chain. While __proto__ was standardized, direct access is generally discouraged in favor of methods like Object.getPrototypeOf() and Object.setPrototypeOf(). Browser developer tools often display it as .proto.

This chain allows Admin objects to inherit properties and methods from both User.prototype and Object.prototype, illustrating the power and flexibility of prototype inheritance in JavaScript.

Conclusion

This chapter demonstrated how to implement inheritance in JavaScript using constructor functions and prototypes, mirroring the behavior of classes. By understanding the concepts of constructor functions, apply, prototypes, Object.create, and the prototype chain, you gain a deeper understanding of JavaScript’s object-oriented nature and how inheritance is achieved under the hood. This knowledge is valuable for both understanding existing JavaScript code and for building more complex and maintainable applications. While ES6 classes provide a more syntactically sugar-coated way to work with inheritance, understanding prototypes remains crucial for a complete grasp of JavaScript.