Objects and Object-Oriented Programming - Learning JavaScript (2016)

Learning JavaScript (2016)

Chapter 9. Objects and Object-Oriented Programming

We covered the basics of objects in Chapter 3, but now it’s time to take a deeper look at objects in JavaScript.

Like arrays, objects in JavaScript are containers (also called aggregate or complex data types). Objects have two primary differences from arrays:

§ Arrays contain values, indexed numerically; objects contain properties, indexed by string or symbol.

§ Arrays are ordered (arr[0] always comes before arr[1]); objects are not (you can’t guarantee obj.a comes before obj.b).

These differences are pretty esoteric (but important), so let’s think about the property (no pun intended) that makes objects really special. A property consists of a key (a string or symbol) and a value. What makes objects special is that you can access properties by their key.

Property Enumeration

In general, if you want to list out the contents of the container (called enumeration), you probably want an array, not an object. But objects are containers, and do support property enumeration; you just need to be aware of the special complexities involved.

The first thing you need to remember about property enumeration is that order isn’t guaranteed. You might do some testing and find that you get properties out in the order in which you put them in, and that may be true for many implementations most of the time. However, JavaScript explicitly offers no guarantee on this, and implementations may change at any time for reasons of efficiency. So don’t be lulled into a false sense of security by anecdotal testing: never assume a given order of enumeration for properties.

With that warning out of the way, let’s now consider the primary ways to enumerate an object’s properties.

for...in

The traditional way to enumerate the properties of an object is for...in. Consider an object that has some string properties and a lone symbol property:

const SYM = Symbol();

const o = { a: 1, b: 2, c: 3, [SYM]: 4 };

for(let prop in o) {

if(!o.hasOwnProperty(prop)) continue;

console.log(`${prop}: ${o[prop]}`);

}

This seems pretty straightforward…except you are probably reasonably wondering what hasOwnProperty does. This addresses a danger of the for...in loop that won’t be clear until later in this chapter: inherited properties. In this example, you could omit it, and it wouldn’t make a difference. However, if you’re enumerating the properties of other types of objects—especially objects that originated elsewhere—you may find properties you didn’t expect. I encourage you to make it a habit to use hasOwnProperty. We’ll soon learn why it’s important, and you’ll have the knowledge to determine when it’s safe (or desirable) to omit.

Note that the for...in loop doesn’t include properties with symbol keys.

WARNING

While it’s possible to use for...in to iterate over an array, it’s generally considered a bad idea. I recommend using a regular for loop or forEach for arrays.

Object.keys

Object.keys gives us a way to get all of the enumerable string properties of an object as an array:

const SYM = Symbol();

const o = { a: 1, b: 2, c: 3, [SYM]: 4 };

Object.keys(o).forEach(prop => console.log(`${prop}: ${o[prop]}`));

This example produces the same result as a for...in loop (and we don’t have to check hasOwnProperty). It’s handy whenever you need the property keys of an object as an array. For example, it makes it easy to list all the properties of an object that start with the letter x:

const o = { apple: 1, xochitl: 2, balloon: 3, guitar: 4, xylophone: 5, };

Object.keys(o)

.filter(prop => prop.match(/^x/))

.forEach(prop => console.log(`${prop}: ${o[prop]}`));

Object-Oriented Programming

Object-oriented programming (OOP) is an old paradigm in computer science. Some of the concepts we now know as OOP begin to appear in the 1950s, but it wasn’t until the introduction of Simula 67 and then Smalltalk that a recognizable form of OOP emerged.

The basic idea is simple and intuitive: an object is a logically related collection of data and functionality. It’s designed to reflect our natural understanding of the world. A car is an object that has data (make, model, number of doors, VIN, etc.) and functionality (accelerate, shift, open doors, turn on headlights, etc.). Furthermore, OOP makes it possible to think about things abstractly (a car) and concretely (a specific car).

Before we dive in, let’s cover the basic vocabulary of OOP. A class refers to a generic thing (a car). An instance (or object instance) refers to a specific thing (a specific car, such as “My Car”). A piece of functionality (accelerate) is called a method. A piece of functionality that is related to the class, but doesn’t refer to a specific instance, is called a class method (for example, “create new VIN” might be a class method: it doesn’t yet refer to a specific new car, and certainly we don’t expect a specific car to have the knowledge or ability to create a new, valid VIN). When an instance is first created, its constructor runs. The constructor initializes the object instance.

OOP also gives us a framework for hierarchically categorizing classes. For example, there could be a more generic vehicle class. A vehicle may have a range (the distance it can go without refueling or recharging), but unlike a car, it might not have wheels (a boat is an example of a vehicle that probably doesn’t have wheels). We say that vehicle is a superclass of car, and that car is a subclass of vehicle. The vehicle class may have multiple subclasses: cars, boats, planes, motorcycles, bicycles, and so on. And subclasses may, in turn, have additional subclasses. For example, the boat subclass may have further subclasses of sailboat, rowboat, canoe, tugboat, motorboat, and so on.

We’ll use the example of a car throughout this chapter, as it’s a real-world object we can probably all relate to (even if we don’t participate in the car culture).

Class and Instance Creation

Prior to ES6, creating a class in JavaScript was a fussy, unintuitive affair. ES6 introduces some convenient new syntax for creating classes:

class Car {

constructor() {

}

}

This establishes a new class called Car. No instances (specific cars) have been created yet, but we have the ability to do so now. To create a specific car, we use the new keyword:

const car1 = new Car();

const car2 = new Car();

We now have two instances of class Car. Before we make the Car class more sophisticated, let’s consider the instanceof operator, which can tell you if a given object is an instance of a given class:

car1 instanceof Car // true

car1 instanceof Array // false

From this we can see that car1 is an instance of Car, not Array.

Let’s make the Car class a little more interesting. We’ll give it some data (make, model), and some functionality (shift):

class Car {

constructor(make, model) {

this.make = make;

this.model = model;

this.userGears = ['P', 'N', 'R', 'D'];

this.userGear = this.userGears[0];

}

shift(gear) {

if(this.userGears.indexOf(gear) < 0)

throw new Error(`Invalid gear: ${gear}`);

this.userGear = gear;

}

}

Here the this keyword is used for its intended purpose: to refer to the instance the method was invoked on. You can think of it as a placeholder: when you’re writing your class—which is abstract—the this keyword is a placeholder for a specific instance, which will be known by the time the method is invoked. This constructor allows us to specify the make and model of the car when we create it, and it also sets up some defaults: the valid gears (userGears) and the current gear (gear), which we initialize to the first valid gear. (I chose to call these user gears because if this car has an automatic transmission, when the car is in drive, there will be an actual mechanical gear, which may be different.) In addition to the constructor—which is called implicitly when we create a new object—we also created a method shift, which allows us to change to a valid gear. Let’s see it in action:

const car1 = new Car("Tesla", "Model S");

const car2 = new Car("Mazda", "3i");

car1.shift('D');

car2.shift('R');

In this example, when we invoke car1.shift('D'), this is bound to car1. Similarly, in car2.shift('R'), it’s bound to car2. We can verify that car1 is in drive (D) and car2 is in reverse (R):

> car1.userGear // "D"

> car2.userGear // "R"

Dynamic Properties

It may seem very clever that the shift method of our Car class prevents us from inadvertently selecting an invalid gear. However, this protection is limited because there’s nothing to stop you from setting it directly: car1.userGear = 'X'. Most OO languages go to great lengths to provide mechanisms to protect against this kind of abuse by allowing you to specify the access level of methods and properties. JavaScript has no such mechanism, which is a frequent criticism of the language.

Dynamic properties1 can help mitigate this weakness. They have the semantics of a property with the functionality of a method. Let’s modify our Car class to take advantage of that:

class Car {

constructor(make, model) {

this.make = make;

this.model = model;

this._userGears = ['P', 'N', 'R', 'D'];

this._userGear = this._userGears[0];

}

get userGear() { return this._userGear; }

set userGear(value) {

if(this._userGears.indexOf(value) < 0)

throw new Error(`Invalid gear: ${value}`);

this._userGear = vaule;

}

shift(gear) { this.userGear = gear; }

}

The astute reader will note that we haven’t eliminated the problem as we can still set _userGear directly: car1._userGear = 'X'. In this example, we’re using “poor man’s access restriction”—prefixing properties we consider private with an underscore. This is protection by convention only, allowing you to quickly spot code that’s accessing properties that it shouldn’t be.

If you really need to enforce privacy, you can use an instance of WeakMap (see Chapter 10) that’s protected by scope (if we don’t use WeakMap, our private properties will never go out of scope, even if the instances they refer to do). Here’s how we can modify our Car class to make the underlying current gear property truly private:

const Car = (function() {

const carProps = new WeakMap();

class Car {

constructor(make, model) {

this.make = make;

this.model = model;

this._userGears = ['P', 'N', 'R', 'D'];

carProps.set(this, { userGear: this._userGears[0] });

}

get userGear() { return carProps.get(this).userGear; }

set userGear(value) {

if(this._userGears.indexOf(value) < 0)

throw new Error(`Invalid gear: ${value}`);

carProps.get(this).userGear = value;

}

shift(gear) { this.userGear = gear; }

}

return Car;

})();

Here we’re using an immediately invoked function expression (see Chapter 13) to ensconce our WeakMap in a closure that can’t be accessed by the outside world. That WeakMap can then safely store any properties that we don’t want accessed outside of the class.

Another approach is to use symbols as property names; they also provide a measure of protection from accidental use, but the symbol properties in a class can be accessed, meaning even this approach can be circumvented.

Classes Are Functions

Prior to the class keyword introduced in ES6, to create a class you would create a function that served as the class constructor. While the class syntax is much more intuitive and straightforward, under the hood the nature of classes in JavaScript hasn’t changed (class just adds some syntactic sugar), so it’s important to understand how a class is represented in JavaScript.

A class is really just a function. In ES5, we would have started our Car class like this:

function Car(make, model) {

this.make = make;

this.model = model;

this._userGears = ['P', 'N', 'R', 'D'];

this._userGear = this.userGears[0];

}

We can still do this in ES6: the outcome is exactly the same (we’ll get to methods in a bit). We can verify that by trying it both ways:

class Es6Car {} // we'll omit the constructor for brevity

function Es5Car {}

> typeof Es6Car // "function"

> typeof Es5Car // "function"

So nothing’s really new in ES6; we just have some handy new syntax.

The Prototype

When you refer to methods that are available on instances of a class, you are referring to prototype methods. For example, when talking about the shift method that’s available on Car instances, you’re referring to a prototype method, and you will often see it writtenCar.prototype.shift. (Similarly, the forEach function of Array would be written Array.prototype.forEach.) Now it’s time to actually learn what the prototype is, and how JavaScript performs dynamic dispatch using the prototype chain.

NOTE

Using a number sign (#) has emerged as a popular convention for describing prototype methods. For example, you will often see Car.prototype.shift written simply as Car#shift.

Every function has a special property called prototype. (You can vary this for any function f by typing f.prototype at the console.) For regular functions, the prototype isn’t used, but it’s critically important for functions that act as object constructors.

NOTE

By convention, object constructors (aka classes) are always named with a capital letter—for example, Car. This convention is not enforced in any way, but many linters will warn you if you try to name a function with a capital letter, or an object constructor with a lowercase letter.

A function’s prototype property becomes important when you create a new instance with the new keyword: the newly created object has access to its constructor’s prototype object. The object instance stores this in its __proto__ property.

WARNING

The __proto__ property is considered part of JavaScript’s plumbing—as is any property that’s surrounded by double underscores. You can do some very, very wicked things with these properties. Occasionally, there is a clever and valid use for them, but until you have a thorough understanding of JavaScript, I highly recommend you look at but don’t touch these properties.

What’s important about the prototype is a mechanism called dynamic dispatch (“dispatch” is another word for method invocation). When you attempt to access a property or method on an object, if it doesn’t exist, JavaScript checks the object’s prototype to see if it exists there. Because all instances of a given class share the same prototype, if there is a property or method on the prototype, all instances of that class have access to that property or method.

TIP

Setting data properties in a class’s prototype is generally not done. All instances share that property’s value, but if that value is set on any instance, it’s set on that instance—not on the prototype, which can lead to confusion and bugs. If you need instances to have initial data values, it’s better to set them in the constructor.

Note that defining a method or property on an instance will override the version in the prototype; remember that JavaScript first checks the instance before checking the prototype. Let’s see all of this in action:

// class Car as defined previously, with shift method

const car1 = new Car();

const car2 = new Car();

car1.shift === Car.prototype.shift; // true

car1.shift('D');

car1.shift('d'); // error

car1.userGear; // 'D'

car1.shift === car2.shift // true

car1.shift = function(gear) { this.userGear = gear.toUpperCase(); }

car1.shift === Car.prototype.shift; // false

car1.shift === car2.shift; // false

car1.shift('d');

car1.userGear; // 'D'

This example clearly demonstrates the way JavaScript performs dynamic dispatch. Initially, the object car1 doesn’t have a method shift, but when you call car1.shift('D'), JavaScript looks at the prototype for car1 and finds a method of that name. When we replace shift with our own home-grown version, both car1 and its prototype have a method of this name. When we invoke car1.shift('d'), we are now invoking the method on car1, not its prototype.

Most of the time, you won’t have to understand the mechanics of the prototype chain and dynamic dispatch, but every once in a while, you’ll run into a problem that will require you to dig deeper—and then it’s good to know the details about what’s going on.

Static Methods

So far, the methods we’ve considered are instance methods. That is, they are designed to be useful against a specific instance. There are also static methods (or class methods), which do not apply to a specific instance. In a static method, this is bound to the class itself, but it’s generally considered best practice to use the name of the class instead of this.

Static methods are used to perform generic tasks that are related to the class, but not any specific instance. We’ll use the example of car VINs (vehicle identification numbers). It doesn’t make sense for an individual car to be able to generate its own VIN: what would stop a car from using the same VIN as another car? However, assigning a VIN is an abstract concept that is related to the idea of cars in general; hence, it’s a candidate to be a static method. Also, static methods are often used for methods that operate on multiple vehicles. For example, we may wish to have a method called areSimilar that returns true if two cars have the same make and model and areSame if two cars have the same VIN. Let’s see these static methods implemented for Car:

class Car {

static getNextVin() {

return Car.nextVin++; // we could also use this.nextVin++

// but referring to Car emphasizes

// that this is a static method

}

constructor(make, model) {

this.make = make;

this.model = model;

this.vin = Car.getNextVin();

}

static areSimilar(car1, car2) {

return car1.make===car2.make && car1.model===car2.model;

}

static areSame(car1, car2) {

return car1.vin===car2.vin;

}

}

Car.nextVin = 0;

const car1 = new Car("Tesla", "S");

const car2 = new Car("Mazda", "3");

const car3 = new Car("Mazda", "3");

car1.vin; // 0

car2.vin; // 1

car3.vin // 2

Car.areSimilar(car1, car2); // false

Car.areSimilar(car2, car3); // true

Car.areSame(car2, car3); // false

Car.areSame(car2, car2); // true

Inheritance

In understanding the prototype, we’ve already seen an inheritance of a sort: when you create an instance of a class, it inherits whatever functionality is in the class’s prototype. It doesn’t stop there, though: if a method isn’t found on an object’s prototype, it checks the prototype’s prototype. In this way, a prototype chain is established. JavaScript will walk up the prototype chain until it finds a prototype that satisfies the request. If it can find no such prototype, it will finally error out.

Where this comes in handy is being able to create class hierarchies. We’ve already discussed how a car is generically a type of vehicle. The prototype chain allows us to assign functionality where it’s most appropriate. For example, a car might have a method called deployAirbags. We could consider this a method of a generic vehicle, but have you ever been on a boat with an airbag? On the other hand, almost all vehicles can carry passengers, so a vehicle might have an addPassenger method (which could throw an error if the passenger capacity is exceeded). Let’s see how this scenario is written in JavaScript:

class Vehicle {

constructor() {

this.passengers = [];

console.log("Vehicle created");

}

addPassenger(p) {

this.passengers.push(p);

}

}

class Car extends Vehicle {

constructor() {

super();

console.log("Car created");

}

deployAirbags() {

console.log("BWOOSH!");

}

}

The first new thing we see is the extends keyword; this syntax marks Car as a subclass of Vehicle. The second thing that you haven’t seen before is the call to super(). This is a special function in JavaScript that invokes the superclass’s constructor. This is required for subclasses; you will get an error if you omit it.

Let’s see this example in action:

const v = new Vehicle();

v.addPassenger("Frank");

v.addPassenger("Judy");

v.passengers; // ["Frank", "Judy"]

const c = new Car();

c.addPassenger("Alice");

c.addPassenger("Cameron");

c.passengers; // ["Alice", "Cameron"]

v.deployAirbags(); // error

c.deployAirbags(); // "BWOOSH!"

Note that we can call deployAirbags on c, but not on v. In other words, inheritance works only one way. Instances of the Car class can access all methods of the Vehicle class, but not the other way around.

Polymorphism

The intimidating word polymorphism is OO parlance for treating an instance as a member of not only its own class, but also any superclasses. In many OO languages, polymorphism is something special that OOP brings to the table. In JavaScript, which is not typed, any object can be used anywhere (though that doesn’t guarantee correct results), so in a way, JavaScript has the ultimate polymorphism.

Very often in JavaScript, the code you write employs some form of duck typing. This technique comes from the expression “if it walks like a duck, and quacks like a duck…it’s probably a duck.” To use our Car example, if you have an object that has a deployAirbags method, you might reasonably conclude that it’s an instance of Car. That may or may not be true, but it’s a pretty strong hint.

JavaScript does provide the instanceof operator, which will tell you if an object is an instance of a given class. It can be fooled, but as long as you leave the prototype and __proto__ properties alone, it can be relied upon:

class Motorcycle extends Vehicle {}

const c = new Car();

const m = new Motorcyle();

c instanceof Car; // true

c instanceof Vehicle; // true

m instanceof Car; // false

m instanceof Motorcycle; // true

m instanceof Vehicle; // true

NOTE

All objects in JavaScript are instances of the root class Object. That is, for any object o, o instanceof Object will be true (unless you explicitly set its __proto__ property, which you should avoid). This has little practical consequence to you; it’s primarily to provide some important methods that all objects must have, such as toString, which is covered later in this chapter.

Enumerating Object Properties, Revisited

We’ve already seen how we can enumerate the properties in an object with for...in. Now that we understand prototypal inheritance, we can fully appreciate the use of hasOwnProperty when enumerating the proprties of an object. For an object obj and a property x,obj.hasOwnProperty(x) will return true if obj has the property x, and false if the property isn’t defined or is defined in the prototype chain.

If you use ES6 classes as they’re intended to be used, data properties will always be defined on instances, not in the prototype chain. However, because there’s nothing to prevent adding properties directly to the prototype, it’s always best to use hasOwnProperty to be sure. Consider this example:

class Super {

constructor() {

this.name = 'Super';

this.isSuper = true;

}

}

// this is valid, but not a good idea...

Super.prototype.sneaky = 'not recommended!';

class Sub extends Super {

constructor() {

super();

this.name = 'Sub';

this.isSub = true;

}

}

const obj = new Sub();

for(let p in obj) {

console.log(`${p}: ${obj[p]}` +

(obj.hasOwnProperty(p) ? '' : ' (inherited)'));

}

If you run this program, you will see:

name: Sub

isSuper: true

isSub: true

sneaky: not recommended! (inherited)

The properties name, isSuper, and isSub are all defined on the instance, not in the prototype chain (note that properties declared in the superclass constructor are defined on the subclass instance as well). The property sneaky, on the other hand, was manually added to the superclass’s prototype.

You can avoid this issue altogether by using Object.keys, which includes only properties defined on the prototype.

String Representation

Every object ultimately inherits from Object, so the methods available on Object are by default available for all objects. One of those methods is toString, whose purpose is to provide a default string representation of an object. The default behavior of toString is to return "[object Object]", which isn’t particularly useful.

Having a toString method that says something descriptive about an object can be useful for debugging, allowing you to get important information about the object at a glance. For example, we might modify our Car class from earlier in this chapter to have a toString method that returns the make, model, and VIN:

class Car {

toString() {

return `${this.make} ${this.model}: ${this.vin}`;

}

//...

Now calling toString on a Car instance gives some identifying information about the object.

Multiple Inheritance, Mixins, and Interfaces

Some OO languages support something called multiple inheritance, where one class can have two direct superclasses (as opposed to having a superclass that in turn has a superclass). Multiple inheritance introduces the risk of collisions. That is, if something inherits from two parents, and both parents have a greet method, which does the subclass inherit from? Many languages prefer single inheritance to avoid this thorny problem.

However, when we consider real-world problems, multiple inheritance often makes sense. For example, cars might inherit from both vehicles and “insurable” (you can insure a car or a house, but a house is clearly not a vehicle). Languages that don’t support multiple inheritance often introduce the concept of an interface to get around this problem. A class (Car) can inherit from only one parent (Vehicle), but it can have multiple interfaces (Insurable, Container, etc.).

JavaScript is an interesting hybrid. It is technically a single inheritance language because the prototype chain does not look for multiple parents, but it does provide ways that are sometimes superior to either multiple inheritance or interfaces (and sometimes inferior).

The primary mechanism for the problem of multiple inheritance is the concept of the mixin. A mixin refers to functionality that’s “mixed in” as needed. Because JavaScript is an untyped, extremely permissive language, you can mix in almost any functionality to any object at any time.

Let’s create an “insurable” mixin that we could apply to cars. We’ll keep this simple. In addition to the insurable mixin, we’ll create a class called InsurancePolicy. An insurable mixin needs the methods addInsurancePolicy, getInsurancePolicy, and (for convenience)isInsured. Let’s see how that would work:

class InsurancePolicy() {}

function makeInsurable(o) {

o.addInsurancePolicy = function(p) { this.insurancePolicy = p; }

o.getInsurancePolicy = function() { return this.insurancePolicy; }

o.isInsured = function() { return !!this.insurancePolicy; }

}

Now we can make any object insurable. So with Car, what are we going to make insurable? Your first thought might be this:

makeInsurable(Car);

But you’d be in for a rude surprise:

const car1 = new Car();

car1.addInsurancePolicy(new InsurancePolicy()); // error

If you’re thinking “of course; addInsurancePolicy isn’t in the prototype chain,” go to the head of the class. It does no good to make Car insurable. It also doesn’t make sense: the abstract concept of a car isn’t insurable, but specific cars are. So our next take might be this:

const car1 = new Car();

makeInsurable(car1);

car1.addInsurancePolicy(new InsurancePolicy()); // works

This works, but now we have to remember to call makeInsurable on every car we make. We could add this call in the Car constructor, but now we’re duplicating this functionality for every car created. Fortunately, the solution is easy:

makeInsurable(Car.prototype);

const car1 = new Car();

car1.addInsurancePolicy(new InsurancePolicy()); // works

Now it’s as if our methods have always been part of class Car. And, from JavaScript’s perspective, they are. From the development perspective, we’ve made it easier to maintain these two important classes. The automotive engineering group manages and develops the Car class, and the insurance group manages the InsurancePolicy class and the makeInsurable mixin. Granted, there’s still room for the two groups to interfere with each other, but it’s better than having everyone working on one giant Car class.

Mixins don’t eliminate the problem of collisions: if the insurance group were to create a method called shift in their mixin for some reason, it would break Car. Also, we can’t use instanceof to identify objects that are insurable: the best we can do is duck typing (if it has a method calledaddInsurancePolicy, it must be insurable).

We can ameliorate some of these problems with symbols. Let’s say the insurance group is constantly adding very generic methods that are inadvertently trampling Car methods. You could ask them to use symbols for all of their keys. Their mixin would then look like this:

class InsurancePolicy() {}

const ADD_POLICY = Symbol();

const GET_POLICY = Symbol();

const IS_INSURED = Symbol();

const _POLICY = Symbol();

function makeInsurable(o) {

o[ADD_POLICY] = function(p) { this[_POLICY] = p; }

o[GET_POLICY] = function() { return this[_POLICY]; }

o[IS_INSURED] = function() { return !!this[_POLICY]; }

}

Because symbols are unique, this ensures that the mixin will never interfere with existing Car functionality. It makes it a little more awkward to use, but it’s much safer. A middle-ground approach might have been to use regular strings for methods, but symbols (such as _POLICY) for data properties.

Conclusion

Object-oriented programming is a tremendously popular paradigm, and for good reason. For many real-world problems, it encourages organization and encapsulation of code in a way that makes it easy to maintain, debug, and fix. JavaScript’s implementation of OOP has many critics—some even go so far as to say that it doesn’t even meet the definition of an OO language (usually because of the lack of data access controls). There is some merit to that argument, but once you get accustomed to JavaScript’s take on OOP, you’ll find it’s actually quite flexible and powerful. And it allows you to do things that other OO languages would balk at.

1Dynamic properties are more correctly called accessor properties, which we’ll learn more about in Chapter 21.