Work with Objects, Classes, and Prototypical Inheritance
Learning Objectives
After completing this unit, you’ll be able to:
- Create objects using object literal notation and constructors.
- Assign properties and functions to objects.
- Identify the role of prototypes in JavaScript object inheritance.
- Describe JavaScript class syntax.
- Describe the role of inheritance and object literal notation in Lightning Web Components.
There are plenty of ways to describe JavaScript as a language. No matter which definition you choose, everyone can agree on the importance of the JavaScript concept of an object. The more you understand JavaScript objects and how they work, the better you’ll be able to write effective JavaScript.
A few notes on objects before we get started.
- Objects don’t have classes the way an Apex, Java, or C# developer might think of them.
- Each object inherits from another object.
- Objects are mutable.
- Objects get their own variable context when they’re created.
Creating Objects
Syntactically speaking, there are several ways to create an object in JavaScript. But no matter how you create an object, it’s actually abstracting an underlying API called Object.create()
.
In some instances there’s good reason to use Object.create()
directly, but we won’t cover that here. Instead, let’s look at more common ways to create objects.
Object Literal Notation
The first object creation syntax is referred to as object literal notation. This is a simple declarative way to declare and assign an object all at once. The object is then assigned immediately as part of the same statement.
const bike = { gears: 10, currentGear: 3, changeGear: function(direction, changeBy) { if (direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } } } console.log(bike.gears); // 10 console.log(bike.currentGear); //3 bike.changeGear('up', 1); console.log(bike.currentGear); //4
Object literal notation is declarative in nature. The object bike
in this example has three members: the gears
and currentGear
properties and the changeGear
function. To reference those members once the object has been created, use dot notation.
Literal objects are great for one-off objects. But if you want to create two or more objects of the same type, they are not practical. For that, you need repeatable logic to create new objects.
New Objects with Constructors
Another way to create objects is to use a constructor. A constructor is a function that contains instructions for establishing the properties of an object when that object is created and assigned. This has an advantage over object literal as you can create many instances of objects that have the same properties.
function Bike(gears, startGear) { this.gears = gears; this.currentGear = startGear; } Bike.prototype.changeGear = function(direction,changeBy){ if(direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } } const bike = new Bike(10, 3); console.log(bike.gears); // 10 console.log(bike.currentGear); //3 bike.changeGear('up', 1); console.log(bike.currentGear); //4
In this exampleBike
is a normal JavaScript function that defines the object. We follow JavaScript convention and capitalize the first word to signal that this function is a constructor. The new
keyword is critical. Without new
, the this
pointer will not point to the object you expect and causes unintended behavior. We revisit this
when we cover context in a later unit.
Notice the assignment of the changeGear
function is done using something called prototype
. This ensures the function is defined once and shared by all instances created from this constructor. We cover use of prototypes and inheritance later in this unit.
Syntax-wise, object literal notation and constructors are pretty different. But in each case you still end up with a new object created in memory, with the variable bike
as a pointer to that object. With the constructor, you can make lots of Bike
objects with the same properties and functions.
Assigning Properties and Functions to Objects
If you inferred from the bike
examples above that there are two possible types of members in an object—properties and functions—you would be correct.
Properties come in three basic shapes.
- Primitives
- Objects
- Arrays
At the time of the writing of this module, there are seven primitive types in JavaScript: string, number, Boolean, null
, undefined
, symbol, and bigint. Primitive types are immutable. When a variable is a primitive type, it’s passed by value when assigned. That is to say, each time a primitive is assigned, a copy of the value is made and assigned to the new variable.
Pretty much anything that isn’t a primitive in JavaScript is an object. In object literal notation, object properties are denoted by curly brackets.
Arrays themselves are also implemented as objects in JavaScript. Arrays can be created with the Array()
constructor function or with literal notation denoted by square brackets.
Functions have their own unit in this module, so we won’t talk about them here, but based on the above, let’s take another pass at defining a more complex bike
object using object literal notation.
const bike = { frontGearIndex: 0, rearGearIndex: 0, transmission: { frontGearTeeth: [30,45], rearGearTeeth: [11,13,15,17,19,21,24,28,32,36] }, calculateGearRatio: function() { let front = this.transmission.frontGearTeeth[this.frontGearIndex], rear = this.transmission.rearGearTeeth[this.rearGearIndex]; return (front / rear); }, changeGear: function(frontOrRear, newValue) { if (frontOrRear === 'front') { this.frontGearIndex = newValue; } else { this.rearGearIndex = newValue; } } };
Referencing Properties by Bracket Syntax
Referencing an object member is most commonly done with dot notation. For instance, in the previous example, we reference the object’s properties and functions as follows.
bike.frontGearIndex bike.transmission.frontGearTeeth bike.calculateGearRatio()
In dot notation, there are strict rules for the names of properties. However, JavaScript also allows for another syntax called bracket notation. The members above would be referenced as follows in bracket notation.
bike["frontGearIndex"] bike["transmission"]["frontGearTeeth"] bike["calculateGearRatio"]()
While it requires more typing, bracket notation has two benefits. You can name your property or function anything you want, and because it’s a string, you can pass a property or function name through a variable and call it.
Let's see this at work by reimagining the changeGear
function. We now use four functions to define shifting the front and rear gears either up or down. In the changeGear
function, we construct the name of the function to call based on String parameters and then call it.
changeGear: function(frontOrRear, upOrDown) { let shiftFunction = frontOrRear + upOrDown; this[shiftFunction](); }, frontUp: function(){ this.frontGearIndex += 1; }, frontDown: function(){ this.frontGearIndex -= 1; }, rearUp: function(){ this.rearGearIndex += 1; }, rearDown: function(){ this.rearGearIndex -= 1; }
Add these to our bike object, and we can see them at work.
console.log(bike.calculateGearRatio()); // 2.727272727 //Calls the frontUp() function bike.changeGear("front", "Up"); console.log(bike.calculateGearRatio()); // 4.090909091 //calls the rearUp() function bike.changeGear("rear", "Up"); console.log(bike.calculateGearRatio()); // 3.461538461
Object Mutability
Apart from the different syntaxes for defining objects, there’s another key principle of JavaScript objects: mutability.
Objects in JavaScript are mutable, which means that if you want to modify the shape of an object, you can.
Let’s take the bike
object we created. We could, for instance, add a new property or function.
bike.isTandem = true; bike.popAWheelie = function() { … };
Even though you may not have access to the code where the object is initially defined, you can modify the shape of your object once it’s in memory. The important point, though, is that only one instance of the object changes. Let’s look back at our Bike
constructor:
const bike1 = new Bike(); const bike2 = new Bike(); bike1.isTandem = true; console.log(bike1.isTandem); // true console.log(bike2.isTandem); // undefined
If you want several objects to share the same properties or methods, then there’s an inheritance model. Let’s look at that.
Objects and Inheritance
Despite not having classes as defined by classical languages, JavaScript still has an inheritance model, called prototype inheritance.
A prototype is, in fact, another object. It sits in memory, and defines properties or functions that other objects inherit if they share the same prototype.
Traditionally in JavaScript objects share the same prototype by sharing the same constructor function. Remember the Bike
constructor. We assign the changeGear
function to something called prototype
.
function Bike(gears, startGear) { this.gears = gears; this.currentGear = startGear; } Bike.prototype.changeGear = function(direction, changeBy) { if (direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } }
This way every object created from Bike
inherits the changeGear
function.
You can also implement multilevel inheritance with prototypes. It is referred to as a prototype chain. Implementing a prototype chain using constructor functions is complex, and requires a fair amount of boilerplate code. It’s also beyond the scope of this module. What you need to know is that in order to address prototype chain complexity, ECMA put a standard in place for a more straightforward syntax to implement inheritance: the class
syntax.
Classes and JavaScript
If you read the word "class" and got a warm fuzzy feeling, thinking that you’d be looking at something that creates true class-based inheritance, prepare to be disappointed. Still, the class
keyword in JavaScript is a nice bit of syntactic sugar to address the complexities of prototype inheritance using constructor functions. Under the covers, the engine is still using Object.create
and there is still no class (in the object-oriented sense), just that in-memory prototype object that is the actual source of inheritance.
The good news is that it does read a lot more like code from Java or C#, with a few JavaScript-specific things to take into account.
While we won’t delve into too many details of JavaScript Classes here, for academic purposes it is good to see a version of the bike object implemented with class syntax.
class Bike { constructor(gears, startGear){ this.gears = gears; this.currentGear = startGear; } changeGear(direction, changeBy) { if (direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } } } const bike = new Bike(10, 5); console.log(bike.currentGear); // 5 bike.changeGear('up', 2); console.log(bike.currentGear); // 7
As you can see, the syntax looks a lot more like a class in Java or Apex. A clear difference is that the constructor function is always named constructor
. An important feature is that functions and attributes automatically belong to the prototype chain without having to directly reference Object.prototype. This also simplifies creating multilevel prototype inheritance.
Lightning Web Components and Objects
Several parts of this unit are relevant to developing Lightning Web Components, including some of the syntax we discussed, and prototype chains.
Classes and Lightning Web Components
Lightning Web components take advantage of many of the modern improvements to JavaScript, most notably, the use of class syntax. Components are typically defined by a JavaScript class that extends another class called LightningElement. Here’s what it looks like:
import { LightningElement } from lwc; export default class MyComponent extends LightningElement { myProperty; myFunction() { console.log(this.myProperty); } }
The functionality of a Lightning web component is defined in a JavaScript class. This example also uses some syntax we haven’t addressed yet regarding modules (import
and export
).
On Object Literals
In some examples in this module, for the purposes of learning how objects work, we declare functions inside object literals. Note that this is not a recommended practice in modern JavaScript. Object literals are a great way to create ad hoc data structures to pass data between functional parts of a JavaScript program, but you should avoid defining a function in an object literal.
Resources
- JavaScript Object
- Working with Objects
- Trailhead: Build a Bear-Tracking App with Lightning Web Components
- Lightning Web Components Developer Guide: Define a Component