18.4 Object-Oriented Programming in JavaScript
To better understand the principle of objects in JavaScript, it’s probably best to start by learning how to create your own objects. Once you understand the underlying concept, you won’t have any more problems with the predefined objects or the browser objects. Of course, it should be noted here that this is only an introduction to object-oriented programming (OOP) in JavaScript.
18.4.1 What Exactly Are Objects?
Simply put, objects in JavaScript are basically nothing more than complex and compound variables with properties and methods. The properties of an object are also called attributes or properties and the methods are sometimes also referred to as object methods. The object provides you with access to all properties and methods. Based on what we’ve learned so far about JavaScript in this book, we can also say that properties are the data and methods are the functions of an object. Take a look at the following JavaScript code as an example:
function print(usr) {
console.log("Nickname : " + usr[0]);
console.log("Age : " + usr[1]);
let admin = usr[2] ? "Yes" : "No";
console.log("Admin rights : " + admin);
}
let user = [
"pronix74", // Nickname
46, // Age
false // Admin rights
];
print(user);
In this example, the variables and the function for the output were used separately. The disadvantage of such somewhat unstructured scripts is that they use a loose collection of global variables and functions. Such scripts are difficult to adapt or extend later. As the amount of data and functions grows, it will become confusing over time.
With very little effort, you can create or encapsulate a template for an object with properties and methods from these global variables and functions, and you only need one global object through which the rest of the data and functions can then be accessed. The following adapted example is intended to show you how easy it is to apply OOP in JavaScript:
let user = {
nickname: "pronix74", // Nickname
age: 46, // Age
admin: false, // admin rights
print: function() { // "function()" is optional
console.log("Nickname : " + this.nickname);
console.log("Age : " + this.age);
console.log("Admin : " + this.isAdmin());
},
isAdmin: function() {
return this.admin ? "Yes" : "No";
}
};
user.print();
Listing 18.18 /examples/chapter018/18_4_1/script.js
In this example, you can see one of several ways to create an object with JavaScript using the object literal notation. Here, you can see that objects in JavaScript are implemented using key-value pairs. Key and value are separated by a colon. You can use the key to access the corresponding values. A value itself can in turn be a literal, a function, or some other object. The key is the identifier for the property or method of the object.
Everything after the assignment to let user between the curly brackets is the content of the object. In the example, there are three data properties, that is, nickname, alter, and admin, and two methods, print and isAdmin. In JavaScript, methods are also introduced with the keyword function. Since ES6, however, the keyword can also be omitted. You must separate the individual properties and methods of an object with commas, and the value of a property or method must be written after a colon.
The Keyword “this”
I’ll describe the keyword this separately later on. Within the methods print() and isAdmin(), it represents the object on which the method is executed. Without this, access within the methods to the properties wouldn’t work.
18.4.2 Creating Objects via Constructor Functions
Alternatively, you can define a constructor function and create a new object using the new keyword. Unlike the object literal notation, the constructor function allows you to create any number of copies of the object. In JavaScript itself, there’s no constructor for creating instances, as are available in other OOP languages, which is why a function is used as a constructor function here. Basically, the appearance of a constructor function doesn’t differ from a normal function. To distinguish it from a normal function, it’s recommended to capitalize the first letter in constructor functions.
Here’s a constructor function from which you can subsequently create any number of objects:
function User(nickname, age, admin) {
this.nickname = nickname;
this.alter = age;
this.admin = admin;
this.printUser = function() {
console.log("Nickname : " + this.nickname);
console.log("Age : " + this.age);
console.log("Admin : " + this.isAdmin());
}
this.isAdmin = function() {
return this.admin ? "Yes" : "No";
}
};
let user01 = new User("pronix74", 46, false);
let user02 = new User("halwa66", 52, true);
user01.printUser();
user02.printUser();
Listing 18.19 /examples/chapter018/18_4_2/script.js
To turn a function into a constructor function, you must first call it using the keyword new. The function then creates a new object and returns it. You don’t need to use return for a constructor function because the new object gets returned implicitly. The properties and methods within the constructor function are accessed via the keyword this.
18.4.3 Creating Objects via the Class Syntax
Because objects in particular take some getting used to for those switching from another object-oriented language to JavaScript, ECMAScript 6 (2015) introduced a class syntax that’s quite similar to that of Java or C++, for example. If you come from these programming languages, you’ve implemented such object types using classes. JavaScript doesn’t know classes, but uses constructor functions and prototypes for this purpose.
For this reason, the class keyword was introduced to define the “class”, while the function keyword (optional since ES6 anyway) is no longer needed for the methods.
Here’s the User example in a class syntax:
class User {
constructor(nickname, age, admin) {
this.nickname = nickname;
this.alter = age;
this.admin = admin;
}
print() {
console.log("Nickname : " + this.nickname);
console.log("Age : " + this.age);
console.log("Admin : " + this.isAdmin());
}
isAdmin() {
return this.admin ? "Yes" : "No";
}
};
let user01 = new User("pronix74", 46, false);
let user02 = new User("halwa66", 52, true);
user01.print();
user02.print();
Listing 18.20 /examples/chapter018/18_4_3/script.js
The constructor() method is called implicitly when you create a new object instance of the corresponding class. Basically, the constructor() method corresponds to the constructor function from the previous section. Again, no return is required because the object instance is returned implicitly. Apart from that, creating a new object works the same way as with the constructor function with new, which implicitly calls the constructor() method of the class.
18.4.4 Accessing the Object Properties and Methods: Setters and Getters
To access object properties and methods, we usually use the dot notation, as you’ve already seen in the examples before. Consider this example:
...
let user01 = new User("pronix74", 46, false);
// Dot notation:
user01.print();
console.log(user01.nickname); // Output: pronix74
Besides the dot notation, you can also write the property and method in square brackets between single or double quotes. Here’s the alternative option:
...
let user01 = new User("pronix74", 46, false);
// Bracket notation:
user01["print"]();
console.log(user01["nickname"]); // Output: pronix74
You can use the dot notation or the bracket notation as you like. The only thing I’d recommend is that you keep it consistent. I personally prefer the dot notation. Nevertheless, there are individual cases in which you must use the bracket notation. This happens when properties or methods contain a hyphen, for example. Without the square bracket notation an error would occur because the minus sign would be interpreted as a subtraction operator.
In this way, you could now also change the properties of an object directly. If you want to change the nickname, for example, this could be done as follows:
user01.nickname = "woafu1974";
user01.age = "I won't tell";
user01.admin = "root";
You can already see from the example that it would now also be possible to enter any nonsense. In OOP, setter methods are usually used to change individual properties, while getter methods are used to retrieve individual properties. Especially with the setter methods, you can then still check the passed value for its validity.
JavaScript provides the keywords set (for setter methods) and get (for getter methods). You must place the keyword before the method. For example, to change the nickname, you must use set. To retrieve individual properties, on the other hand, you would use get. However, you aren’t forced to implement both versions. Here’s the example with the setter and getter methods in use:
class User {
constructor(nickname, age, admin) {
this._nickname = nickname;
this._age = age;
this._admin = admin;
}
isAdmin() {
return this._admin ? "Yes" : "No";
}
set nickname(name) {
if (typeof name === "string") {
this._nickname = name;
} else {
console.log("Error: no string!")
}
}
get nickname() {
return this._nickname;
}
set age(age) {
if (typeof age === "number") {
this._age = age;
} else {
console.log("Error: Not an integer!")
}
}
get age() {
return this._age;
}
set admin(adm) {
if (typeof adm === "boolean") {
this._admin = adm;
} else {
console.log("Error: true oder false!")
}
}
get admin() {
return this._admin;
}
};
let user01 = new User("pronix74", 46, false);
// Setter methods in use
user01.nickname = "woafu1974"; // -> set nickname("woafu1974")
user01.age = 47; // -> set age(47)
user01.admin = true; // -> set admin(true)
// Getter methods in use
console.log(user01.nickname); // -> get nickname()
console.log(user01.age); // -> get age()
console.log(user01.admin); // -> get admin();
Listing 18.21 /examples/chapter018/18_4/script.js
To avoid naming conflicts between the properties and methods here, it’s common practice to have the properties begin with an underscore. Thanks to this data encapsulation, you now also have the properties better protected against access from outside, although it’s still possible to address the properties directly from outside with an underscore (e.g., user01._nickname).
18.4.5 The Keyword “this”
You’ve already used the keyword this quite a bit, so I’ll briefly go into some more detail here. In simple terms, this is a kind of property which, when called, is assigned the value of the object with which it’s called. In other words, it’s virtually a reference to itself. this is therefore an implicit parameter that is automatically available.
this is a keyword whose value is resolved and usually references an object. However, the actual value of this again depends on the execution context in which it was called.
In the following example, this is written in a class, a global function, and the global environment. Within the class, this refers to the instantiated SimpleClass object itself. For the global function, the execution context is undefined in strict mode. Without strict mode, it would be the window object when the JavaScript is executed in the web browser. When using this globally, on the other hand, the global window object is used by default, but only if the program is run in a web browser.
...
class SimpleClass {
simple() {
console.log(this); // Output: SimpleClass
}
};
function simpleFunction() {
console.log(this); // Output: Window (in browser)
// Output: undefined (with Node.js)
}
let val1 = new SimpleClass();
val1.simple();
simpleFunction();
console.log(this); // Output in web browser: window
Listing 18.22 /examples/chapter018/18_4_5/script.js
this is thus a reference to the owner of the object, which is necessary because there can be and are more instances of an object. Especially within methods of a created object, this is very important because only in this way can you make sure that with several instances of the same objects also the stored properties of the individual instance can be accessed. After all, each individual instance of an object has its own properties and doesn’t share them with any other instance.