Hybrid

JS Prototypes, Classes, and Modules

Module Still Under Development

# JavaScript Classes

For more details about the class syntax and prototype chain in JavaScript see the week 12.1 notes from MAD9014 (opens new window).

Here are the basics.

The class keyword is simply a different way of creating an Object in JavaScript.

class Deadpool extends HTMLElement {
  constructor() {
    super();
  }
}

let ryanReynolds = new Deadpool();
1
2
3
4
5
6
7

The class can be created on its own or as an extension of some other type of object. Let's take these two classes as examples.

class A {
  constructor() {
    //creates an object of type `A`
  }
}

class B extends X {
  constructor() {
    super();
    //creates an object of type `B`
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

Calling the constructor from each class will create an object of that specific type. With the extends keyword we are just changing the prototype chain for the object. In the example above, The prototype of A will be A.prototype and the next step up the prototype chain will be Object.prototype.

The prototype of B will be B.prototype and the next step up the prototype chain will be X.prototype. If the X object was a class made like A, then the next step up the chain from X.prototype will be Object.prototype and above Object.prototype will always be null.

The constructor() function inside the class will run when you call the class with the new keyword.

The super() method call will run the constructor of whatever class you are extending and add any properties from HTMLElement into Deadpool. By extending HTMLElement we are defining the prototype chain and saying that Deadpool.prototype is connected to HTMLElement.prototype.

If you were to do this with traditional JavaScript functions and prototype syntax, it would look like this:

function Deadpool() {
  //this is like the constructor() function
}

Object.setPrototypeOf(Deadpool.prototype, HTMLElement.prototype);

let ryanReynolds = new Deadpool();
1
2
3
4
5
6
7

The class keyword in JavaScript is often called syntactic sugar because it still calls the standard JavaScript methods and builds the prototypes in the background. It is not using class-based inheritance. It is still using prototype-based delegation.

# The Prototype Chain

Every object that you create in JavaScript gets created by a function. If you are creating an array then it is created by the function called Array. If you create a plain object then it gets built with a function called Object. Even when you are creating array and object literals, the function gets used behind the scenes to create the objects.

Every ordinary function that you create is actually created with a function called Function. Asynchronous functions are created with a function called AsyncFunction.

Each function has a property called prototype. The prototype is a place where shared properties and methods can be stored. They will be shared with every object created by the same function. For example, Array.prototype.sort is the name and location of the sort method that can be used by every array.

If you want, you can create your own shared methods and add them to the various prototype objects. Here is an example where we are adding a method called blah to every array.

Array.prototype.blah = function () {
  //this is the array calling this method
  this.forEach((item, index) => {
    this[index] = 'blah';
  });
};

//now all arrays have a method called `blah`
let names = ['Sam', 'Dean', 'Cas', 'Crowley'];
names.blah();
console.log(names); //outputs ['blah', 'blah', 'blah', 'blah']
1
2
3
4
5
6
7
8
9
10
11

When you call a method on any object, the JavaScript interpreter understands that it needs to walk up the prototype chain looking for the method.

All the built-in objects in JavaScript have a prototype and that prototype knows what the next step up the prototype chain will be.

Eg: Array.prototype => Object.prototype => null

When you are creating your own prototypes you can use Object.setPrototypeOf() to define the parent object whose prototype will be the next step up the chain.

However, when you are creating your own object using the class syntax, then you just need to add extends followed by the name of the parent object type that will have the prototype which is the next step in the chain.

# JavaScript Class Methods

So, we now know that we can add an instance method to an JavaScript object by putting it inside the prototype object. But how do we do that in code?

class MyObject {
  constructor() {
    //this is called by `new MyObject()`
  }

  talk() {
    //this is an instance method and
    //will be stored inside MyObject.prototype
  }
  walk() {
    //this is an instance method and
    //will be stored inside MyObject.prototype
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Once the instance methods are created you can call them from any object instance. Instance is the keyword here.

Every time you create an object of type MyObject, you are creating an instance of MyObject.

let obj1 = new MyObject(); //create an instance
let obj2 = new MyObject(); //create an instance
let obj3 = new MyObject(); //create an instance

obj1.talk(); //call an instance method
obj2.walk();
obj3.walk();
1
2
3
4
5
6
7

There are also instance properties. These are the variables that you declare inside the class, but not inside any of the functions.

class MyObject {
  name;
  createdDate;

  constructor(_name){
    //this is called by `new MyObject()`
    this.createdDate = Date.now();
    this.name = _name;
  }
}

let obj1 = new MyObject('Bob');
1
2
3
4
5
6
7
8
9
10
11
12

In the above example we have TWO instance variables, also called instance members. Inside the constructor we assign the values to those two variables from inside the constructor function. We use the keyword this to refer to the object instance that is currently being created by the constructor function.

# static methods and properties

Sometimes you want to create a method or property value that can be shared across ALL instances or accessed without creating or referencing an instance.

As an example, let's say we want to keep track of how many instances of an object have been created. We can't put an instance variable inside of every object instance and then update every object every time the constructor is called. Instead we create a single variable and attach it to the class, instead of the instances.

class MyObject {
  name;
  createdDate;
  static objectCount = 0;

  constructor(_name) {
    //this is called by `new MyObject()`
    this.createdDate = Date.now();
    this.name = _name;
    //increment the static variable
    MyObject.objectCount++;
  }

  static howMany() {
    //return the value of the variable
    return MyObject.objectCount;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Each time the constructor is called we update the value in the static variable. Static means that it is saved inside the class, not created as a value inside each instance.

In the example above, we also have a static method that can give us the current value of the static objectCount.

MyObject.howMany(); //returns the current value regardless of how many times new MyObject has been called.
1

# Public and Private modifiers

One of the newer features in JavaScript classes is the concept of private members.

By default, in JavaScript, access to variables is controlled by scope. A variable declared in the global scope can be seen everywhere in your code. If a variable is declared inside a {} block, like inside a function or loop, then it can only be seen inside that block. Code in a lower scope (like inside a function) can see variables that are in a higher containing scope.

Modules have added a new level of scope. Variables inside of a module are, by default, only visible inside the module, based on their scope within the module. If you want something in the module's global scope to be accessible outside of the module then it must be exported.

Now, with the class syntax, we can create objects that contain variables and functions inside the class which can only be seen by the code inside the class.

class MyObject {
  #secret = 'can only be seen from inside the class';
  static #objectCount = 0; //static but also private

  constructor() {
    this.#secret = 'private instance value';
  }

  static howMany() {
    //this is the only way to get the value of MyObject.objectCount
    return MyObject.#objectCount;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

In the example above you would not be able to access the value of this.#secret or MyObject.#objectCount from outside the class. If you tried, you would get an error as if the variables did not exist.

We did update the howMany() method though. It is allowed to access private members. So, it can access the value and return it to the code outside the class.

# Class Extending HTMLElement

The first step in creating a Web Component is to create a class that extends HTMLElement. HTMLElement is the base Object for all HTML in the DOM. It holds all the global default properties like id.

class CoolComponent extends HTMLElement {
  constructor() {
    super();
  }
}
1
2
3
4
5

This will create a CoolComponent object that can hold properties, methods, events, event listeners, and more.

If this code were written in a file called coolcomponent.js then any webpage that wants to access it, needs to load the script as a module. The JavaScript file does not need to import anything, just run the script.

So, in your HTML you will just add a script tag with the type="module" attribute.

<script src="./components/coolcomponent.js" type="module"></script>
1

Web Components can be loaded from external servers too. They don't have to be saved locally. If you have multiple local web components that you are using, a recommended approach is to create a folder called components and save them all in that folder.

# Modules

As shown in the previous sample, you can add the attribute type="module" to your <script> element to load the script as an ES Module.

Once you have made your script into a module, it now has the ability to use the keywords import or export.

With web components you will want to treat the component as a module that can be imported into your main JavaScript file. As it is imported you can register your component. At the bottom of every component file you will have a line like this, which will be the one line that runs.

window.customElements.define('your-component', YourComponent);
1

Now, you don't need to add a script tag for every component to your HTML. As long as your main script is loaded into the HTML with the type="module" attribute, then it will be able to load any modules you need.

<script src="./js/main.js" type="module"></script>
1

Inside the main.js file we import the components, and then register them to be useful in the HTML.

import './my-first-component.js';
import './my-second-component.js';
import './my-third-component.js';
1
2
3

Note that you don't have to import the component script files into the HTML. The process of importing the scripts into the main.js file will run the customElements.define method and make your custom web components available to the HTML.

However, if you do want to add a script tag for each component instead, you can. Just remember to add the type="module" attribute to each script tag.

# CSS Property Value Composition

Every HTML element has default values for all of it's CSS properties. However, most of these are probably not what you expect. For example, all elements start off as display: inline.

The the browser has it's own stylesheet which gets applied. This is known as the user-agent stylesheet. If you open the developer tools in Chrome and you look at the styles panel, you will see the values provided by the user-agent stylesheet.

On top of that will be applied your own CSS stylesheets. Within your own CSS you can target individual properties and remove values that had been assigned and revert back to the values from the standard or the user-agent. Watch this value to learn more.

On top of the layers of styling already defined there is also style encapsulation, called scoped styling, that is implemented through the Shadow DOM. Read more about scoped styling here (opens new window).

As part of the latest CSS standards there is a new way of controlling how the specificity of your styles is applied. It is known as CSS Layers. Basically you can add layer names to your style blocks and these will control the order that the styles are applied. Within each block, normal CSS specificity applies.

@layer special, notspecial;

@layer notspecial {
  p {
    color: gold; /* applied second */
  }
}
@layer special {
  p {
    color: rebeccapurple; /* applied first */
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

Read more about CSSLayerStatementRule here (opens new window) and the CSSLayerBlockRule here (opens new window)

Last Updated: 1/10/2024, 9:10:52 PM