Object-Oriented JavaScript: Classes
In this new series on JavaScript development, we’ll be taking a deep look into the workings of JavaScript and how to implement object-oriented JavaScript and JavaScript classes, the infamous JavaScript prototype, and arrays and for loops.
Object-Oriented JavaScript
JavaScript is an object-oriented programming language as everything in JavaScript is an object. However, it is different from other object-oriented languages like Java or C++ in that it does not have a class-based structure. Instead, JavaScript uses a prototype-based structure to define its objects. We’ll discuss exactly what it means to be prototype-based in the next article.
In this article we’ll be discussing how to implement OOP in JavaScript, namely how to create a JavaScript class. For a refresher of OOP principles, take a look at this series on Gamedevtuts+.
JavaScript Objects
To start off, let’s discuss how to create an object. In JavaScript, there are a two ways you can define an object. The first way is by using an object literal and the other is by using a constructor function.
// Object declared using object literal
var shape = {
height: 10,
width: 10,
area: function() {
return this.height * this.width;
}
}
console.log(shape.area()); // 100
// Object declared using constructor function
function Shape() {
this.height = 10;
this.width = 10;
this.area = function() {
return this.height * this.width;
};
};
var shape = new Shape();
console.log(shape.area()); // 100
Although the object literal and constructor function methods are similar, they are fundamentally different. An object literal creates an object that can be immediately used without first having to use the new
keyword. An object literal is also known as a singleton. However, an object literal cannot implement the basic OOP principles of encapsulation and inheritance.
By using a constructor function, we are able to implement object-oriented design in JavaScript. A constructor function is essentially a class.
Scope
Before we begin talking about JavaScript classes, we first must talk about scope. Scope is the context to which an entity (variable, function, or object) is accessible, or visible, to other entities. There are a few types of scope, but the main ones are global scope and local scope.
The global scope is the top most context level and is defined by the window
object. Everything starts in the global scope and any entity in the global scope can be accessed anywhere else in the code. Local scope refers to any entity that is not defined in the global scope. A locally scoped entity is only accessible inside of its own scope and cannot be accessed in any other scope (including the global scope).
var global = 4; // A globally scoped variable
function globalFunction() { // A globally scoped function
var local = 3; // A locally scoped variable
console.log(local + ", " + global);
}
globalFunction(); // 3, 4
console.log(global); // 4
console.log(local); // ReferenceError: local is not defined
The current scope can always be accessed by using the this
object, which points to the object that has the current scope. When within the global scope, the this
object will point to the Window
object. Scope inside of an object or function is a bit more complicated. Depending on how or where the function is defined, the scope may change from the global scope to the local scope.
console.log(this); // Window object
fn = new function() {
console.log(this); // Window object
}
function Shape() {
console.log(this); // Shape object
this.area = function() {
console.log(this); // Shape object
};
};
When declaring an entity, you can declare it either using the var
or the this
keyword. An entity declared using the var
keyword becomes an entity of the current scope (i.e. locally scoped). An entity declared using the this
keyword becomes a property of the object which has the current scope. What this means is that the entity can be accessed outside of the current scope, but you must go through the object first (even inside the object itself).
function Shape() {
var height = 10;
this.width = 2;
this.getArea = function() {
console.log(height * this.width); // width must be accessed by calling this
}
};
var shape = new Shape();
console.log(shape.height); // undefined
console.log(shape.width); // 2
shape.getArea(); // 20
Knowing how scope works is important to the implementation of object-oriented programming as it allows us to create public and private entities.
JavaScript Classes
Now that we understand scope, we can discuss how to implement JavaScript classes. Because an entity’s scope is different based on how it is declared, an object can have entities which are locally scoped (inaccessible outside of the object) and entities which are properties of the object (accessible by first accessing the object). This is how we can create private and public entities.
function Shape() {
// Private entities
var height = 10;
width = 2;
calcArea = function() {
return height * width;
}
// Public entities
this.perimeter = 24;
this.getArea = function() {
console.log(calcArea());
}
this.getAreaAndPerimeter = function() {
this.getArea();
console.log(this.perimeter);
}
};
var shape = new Shape();
shape.getArea(); // 20;
shape.calcArea(); // TypeError: Object # has no method 'calcArea'
shape.getAreaAndPerimeter(); // 20 24;
Constructor functions can also have parameters just like any other function. Parameters are locally scoped to a function. It is important to note that entity names must be unique when they are within the same scope. Entities that are part of different scopes can share the same name without overriding one another.
function Shape(height, width) { // height and width are locally scoped variables
// public variables
this.height = height;
this.width = width;
this.getArea1 = function() {
console.log(this.height * this.width); // This accesses the public variables
}
this.getArea2 = function() {
console.log(height * width); // This accesses the parameter values
}
};
var shape = new Shape(10, 2);
shape.getArea1(); // 20;
shape.getArea2(); // 20;
Any parameter that is not passed as an argument to a function will be automatically given the value of undefined
. If you want to have default values for a parameter, you can use the ||
operation to test whether or not the parameter has a value. If the value has been set, it will evaluate to true and return the parameter, otherwise it will return the default value.
function Shape(height, width) {
this.height = height || 5;
this.width = width || 3;
this.getArea = function() {
console.log(this.height * this.width);
}
};
var shape = new Shape(10, 2);
var shape2 = new Shape();
shape.getArea(); // 20;
shape2.getArea(); // 15;
Unlike class-based languages that allow for function overloading based on the number of parameters the function has, JavaScript cannot overload functions. Functions, like variables, must have unique names. If two entities share the same name in the same scope, the entity declared later in the code will override the other.
function Shape() {
this.height = 10;
this.width = 2;
this.getArea = function() {
console.log(1);
}
this.getArea = function(width) {
console.log(2);
}
this.getArea = function(width, height) {
console.log(3);
}
};
var shape = new Shape(10, 2);
shape.getArea(); // 3;
shape.getArea(10); // 3;
shape.getArea(10, 2); // 3;
It is possible to have a function with a variable number of arguments. Functions like Math.max()
and Math.min()
are such functions. To access all arguments passed to a function, you can use the arguments
object. The arguments
object is basically an array of all the arguments passed to a function. Although it looks like an array, it technically isn’t, so be careful how you use it. Array or not, it is still great for using more arguments than defined in the function.
function Shape() {
this.height = 10;
this.width = 2;
this.getArea = function() {
console.log(arguments);
}
};
var shape = new Shape();
shape.getArea(); // [];
shape.getArea(10); // [10];
shape.getArea(10, 2); // [10, 2];
Adding new Entities
Now lets say you have already created the constructor function and derived an object from it, but needed to add a new variable to the object later on in the code. JavaScript allows objects to add and remove entities at runtime. Entities added in this way become properties of that object and can be accessed just like entities declared with the this
keyword.
function Shape() {
this.height = 10;
this.width = 2;
};
var shape = new Shape();
// Some time later in the code
shape.newEntity = 1;
console.log(shape.newEntity); // 1
However, entities added to an object in this manner will not be added to all objects which derive from the same constructor function. If you tried to add a new entity to the constructor function itself, even newly derived objects will not have the new entity.
function Shape() {
this.height = 10;
this.width = 2;
};
var shape1 = new Shape();
var shape2 = new Shape();
shape1.newEntity = 1;
console.log(shape1.newEntity); // 1
console.log(shape2.newEntity); // undefined
Shape.newEntity = 1;
console.log(shape2.newEntity); // undefined
var shape3 = new Shape();
console.log(shape3.newEntity); // undefined
How then do we add a new entity to the constructor function itself? To do that, we need to learn about the prototype object. Because of how complex and confusing the prototype object can be, we’ll learn about it in the next article.
Conclusion
Even though JavaScript is not a class-based language, it can still mimic class-based behavior by using constructor functions. This allows us to use OOP principles such as encapsulation to create public and private entities.
In the next article, we’ll take a deep look into the JavaScript prototype and how to use it to implement the OOP principle of inheritance.