The Web Developer Bootcamp 2024

Colt Steele

Back to OOP Index Page


oop - Constructor Functions

We have been building up our Color Converter app in the previous discussions, and so far, we have discussed Prototypes, and Factory Functions. We have seen how these processes work and while using a Factory Function gives us a good start on our app, there is one drawback.

As a refresher, here is the code:

  • JavaScript
  • // create the makeColor Object
  • function makeColor ( r, g, b ) {
    • // create an empty object
    • const color = {};
    • // add properties to the object
    • color.r = r;
    • color.g = g;
    • color.b = b;
    • // create the rgb method
    • color.rgb = function() {
      • const { r, g, b } = this;
      • return `rgb(${r}, ${g}, ${b})`;
    • };
    • // create the hex method
    • color.hex = function() {
      • const { r, g, b } = this;
      • return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
    • };
    • // return the results
    • return color;
  • }
Inside of our makeColor function, we create the color object. We then create two functions, color.rgb, and color.hex that are used to manipulate, or process, the data from this color object.

This code works very well, but there is a small problem. Every time we run the makeColor function, we are creating a new object and reassigning the internal functions to each new object.

So these internal functions (methods) become unique to each and every object created, which results in each and every object we create, having it's own unique instance of the object's methods. So every time you run the makeColor function with different values, you will get different results, but you are creating multiple instances of the object and it's methods.

Basic Setup

Our code could be much more efficient if we could write it in a way that only has one object instance, and only one instance of the internal functions (methods). The way to accomplish this would be to build our methods inside of the object's prototype using a Constructor Function.

Let's start with a simple example:

  • function Car ( make, model, year ) {
    • this.make = make;
    • this.model = model;
    • this.year = year;
  • }
  •  
  • var myCar = new Car ( 'Ford', 'Mustang', 1970 );
  • console.log(myCar.make); // logs Ford
We notice a couple of things here:
  • 1st - The function name is expressed with a capitol letter.
    • this is done to indicate that this is not a regular function, but is a function that helps you create objects. In other words, a Constructor Function.
  • 2nd - The function does not return anything.
  • 3rd - We're not creating an object within the function to hold the data. Instead, we are using this exclusively to assign the values


The this Problem

Now we have another problem that we will have to adjust for. If we console.log(this); we will get an object. But it is not our Car object. It is the root window object. This is because within our function, we have not created any objects for this to work with, and so this will refer to the next nearest parent object, which is the window object.

We can resolve this problem by using :

The new Keyword

When a function is called with the new keyword, the function will be used as a constructor function, and new will do the following things:

  • Creates a blank, plain JavaScript object.
  • Points the object's [[Prototype]] to the constructor function's prototype property, if the prototype is an Object. Otherwise, the function stays as a plain object with Object.prototype as its [[Prototype]].
  • Executes the constructor function with the given arguments, binding the object as the this context (i.e. all references to this in the constructor function now refer to the function object).
  • If the constructor function returns a non-primitive, this return value becomes the result of the whole new expression. Otherwise, if the constructor function doesn't return anything or returns a primitive, the function object is returned instead. (Normally constructors don't return a value, but they can choose to do so to override the normal object creation process.)
*note: Properties/objects added to the constructor function's prototype property are therefore accessible to all instances created from the constructor function.

So using the new keyword will create an object for this to refer to. And any properties or objects that are added to the constructor function's [[prototype]] property are accessible to ALL of the instances created from the constructor function

Building the Constructor

So now, let's redefine our MakeColor function:

  • JavaScript
  • // create the makeColor function
  • function MakeColor ( r, g, b ) {
    • // add properties to the object
    • this.r = r;
    • this.g = g;
    • this.b = b;
    • console.log(this);
  • }
First off, notice how the first letter of the function name is a capitol letter. This designates it as "not a normal function, but a Constructor Function". And as we've learned, if we call the function like so:
  • MakeColor ( 255, 40, 100 );
the console will log our object properties, but they will be the properties for the parent window object.

And our fix for this is to call the function with the new keyword. This designates our Constructor Function" as an Object:

  • new MakeColor ( 255, 40, 100);
and now the console logs our new object that the new keyword has created.

And if you look at the prototype properties for this object, you will see that our MakeColor function, is now a Constructor Function. And because it is a Constructor Function, it has created an object for this to refer to.

And now that we have a Constructor Function, we can add methods to its [[prototype]], which will be available for use in any instance of this object.


Adding Methods

When we constructed our Factory Function, we built the methods as functions that resided within the makeColor function itself. Constructor Functions are a little different in that we build the methods externally from the Constructor Function.

*note: we could build the methods inside of the Constructor Function but it would not set the method inside of the [[prototype]] which is what we need if we only want one method instance that would work on all Constructor Function objects.

So we just build our methods externally and refer them to the Constructor Function in the naming of the method. This is the same process we have seen before when adding to, or editing [[prototype]] methods:

  • // create the rgb method
  • MakeColor.prototype.rgb = function() {
    • const { r, g, b } = this;
    • return `rgb(${r}, ${g}, ${b})`;
  • };
  • // create the hex method
  • MakeColor.prototype.hex = function() {
    • const { r, g, b } = this;
    • return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
  • };

And now if we look in the console at the MakeColor object that was logged, we see that we have indeed created an object, and that the .rgb and .hex methods are listed properly as [[prototype]] methods.

The end result is that there is only one instance of our MakeColor Object, and only one instance of the [[prototype]] methods .rgb and .hex, irregardless of how many times we call the function or our methods.

*Note: One very important thing to remember when creating our new methods is do not use "Arrow Functions" when defining our methods. They treat this differently and could create problems.

Using the Function

So we now have all of our pieces and parts needed for our function to work. All that is left is to "call" our function. We do this like any other function call, but we must include the "new" keyword, like so:

  • const newColor = new MakeColor();
so we are "calling" our function using the new keyword, and assigning it to the variable newColor.

And now we can use our new methods with newColor:

  • newColor.rgb ( xx, xx, xx );
or,
  • newColor.hex ( xx, xx, xx );

Final Code

Here is the final code. Note that we have added "default" values for the r/g/b/a channels, in the appropriate places, to avoid errors if all values are not input.

  • JavaScript
  •  
  • // create the Constructor Function
  • // adds default values for r/g/b
  • function MakeColor ( r = 190, g = 155, b = 90 ) {
    • this.r = r;
    • this.g = g;
    • this.b = b;
  • };
  •  
  • // create the rgb [[prototype]] method
  • MakeColor.prototype.rgb = function() {
    • const { r, g, b } = this;
    • return `rgb(${r}, ${g}, ${b})`;
  • };
  •  
  • // create the rgba [[prototype]] method
  • // adds default value for the alpha channel
  • MakeColor.prototype.rgba = function( a = 1.0 ) {
    • const { r, g, b } = this;
    • return `rgba(${r}, ${g}, ${b}, ${a})`;
  • };
  •  
  • // create the hex [[prototype]] method
  • MakeColor.prototype.hex = function() {
    • const { r, g, b } = this;
    • return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
  • };
  •  
  • // Call the color object
  • // because we set default color values in the Constructor
  • // any, or all of the input values are optional
  • const newColor = new MakeColor();
  •  
  • // Use the newColor object
  • console.log(`newColor.rgb = ${newColor.rgb()}`);
    • // outputs newColor.rgb = rgb(190, 155, 90)
  • console.log(`newColor.rgba = ${newColor.rgba(0.5)}`); // overrides the default opacity of 1.0
    • // outputs newColor.rgba = rgba(190, 155, 90, 0.5)
  • console.log(`newColor.hex = ${newColor.hex()}`);
    • // outputs newColor.hex = #be9b5a


Back to Top