Define Classes or Objects in ECMAScript

Using predefined objects is only a part of the capabilities of an object-oriented language; its true strength lies in the ability to create custom classes and objects.

ECMAScript has many methods for creating objects or classes.

Factory method

Original method

Because an object's properties can be dynamically defined after the object is created, many developers wrote code similar to the following when JavaScript was first introduced:

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
;

Try It Yourself

In the above code, the object car is created. Then several properties are set for it: its color is blue, it has four doors, and it can travel 25 miles per gallon. The last property is actually a pointer to a function, meaning that the property is a method. After executing this code, the object car can be used.

However, there is a problem here, which is that it may be necessary to create multiple instances of cars.

Solution: Factory Method

To solve this problem, developers created factory functions that can create and return objects of specific types.

For example, the function createCar() can be used to encapsulate the operations for creating car objects listed earlier:

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  ;
  return oTempCar;
}
var oCar1 = createCar();
var oCar2 = createCar();

Try It Yourself

In this example, all the code from the first example is included in the createCar() function. In addition, there is an extra line of code that returns the car object (oTempCar) as the function value. Calling this function creates a new object and assigns it all the necessary properties, copying out a car object that we have described earlier. Therefore, by this method, we can easily create two versions of car objects (oCar1 and oCar2) with identical properties.

Pass parameters to the function

We can also modify the createCar() function to pass default values for each property, rather than simply assigning default values to the properties:

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() {
    alert(this.color);
  ;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//Output "red"
oCar2.showColor();		//Output "blue"

Try It Yourself

By adding parameters to the createCar() function, you can assign values to the color, doors, and mpg properties of the car object to be created. This makes two objects have the same properties but different property values.

Define object methods outside of the factory function

Although ECMAScript is becoming more formalized, the methods for creating objects have been neglected, and their standardization is still opposed to this day. Part of the reason is semantic (it does not seem as formal as using the constructor function with the new operator), and part of the reason is functional. The functional reason lies in the method of creating objects in this way. In the previous example, each time the function createCar() is called, a new function showColor() is created, meaning each object has its own version of showColor(). In fact, all objects share the same function.

Some developers define the methods of the object outside the factory function, and then point to the method through properties to avoid this problem:

function showColor() {
  alert(this.color);
}
function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//Output "red"
oCar2.showColor();		//Output "blue"

Try It Yourself

In the rewritten code above, the function showColor() is defined before the function createCar(). Inside createCar(), a pointer to the existing showColor() function is assigned to the object. Functionally, this solves the problem of repeatedly creating function objects; however, semantically, the function does not seem like an object method.

All these issues have led todefined by the developerthe appearance of the constructor.

Constructor method

Creating a constructor is as easy as creating a factory function. The first step is to choose the class name, which is the name of the constructor. By convention, the first letter of this name is capitalized to distinguish it from variable names that are usually lowercase. Apart from this difference, the constructor looks very much like a factory function. Consider the following example:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  ;
}
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

Try It Yourself

Here is an explanation of the difference between the above code and the factory method. First, no object is created within the constructor, instead the keyword 'this' is used. When constructing a function using the 'new' operator, an object is created before the first line of code is executed, and only 'this' can access the object. Then, the properties of 'this' can be directly assigned, by default the return value of the constructor (there is no need to explicitly use the 'return' operator).

Now, creating an object with the new operator and the class name Car is more like the creation of general objects in ECMAScript.

You might ask if this method has the same problem as the previous method in managing functions? Yes.

Like factory functions, constructors will repeatedly generate functions, creating an independent function version for each object. However, similar to factory functions, the constructor can also be rewritten by an external function, which has no semantic meaning. This is the advantage of the prototype method that will be discussed next.

Prototype method

This method utilizes the prototype property of objects and can be seen as the prototype that new objects depend on to be created.

Here, first, use an empty constructor to set the class name. Then, all properties and methods are directly assigned to the prototype property. We rewrite the previous example as follows:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
;
var oCar1 = new Car();
var oCar2 = new Car();

Try It Yourself

In this code, first, define the constructor (Car) with no code. The next few lines of code add properties to the prototype property of Car to define the properties of the Car object. When calling new Car(), all the properties of the prototype are immediately assigned to the object to be created, meaning that all Car instances store pointers to the showColor() function. Semantically, all properties seem to belong to a single object, thus solving the problems of the previous two methods.

In addition, using this method, you can also use the instanceof operator to check the type of the object pointed to by a given variable. Therefore, the following code will output TRUE:

alert(oCar1 instanceof Car); // Outputs "true"

The problem with the prototype method

The prototype method seems to be a good solution. Unfortunately, it is not as satisfying as one might hope.

Firstly, this constructor does not take any parameters. Using the prototype method, you cannot initialize the value of properties by passing parameters to the constructor, because both the color property of Car1 and Car2 are equal to "blue", the doors property of both are equal to 4, and the mpg property of both are equal to 25. This means that you must change the default value of the properties after the object is created, which is quite annoying, but there's more. The real problem arises when the properties point to an object, not a function. Function sharing does not cause a problem, but objects are rarely shared among multiple instances. Think about the following example:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
;
var oCar1 = new Car();
var oCar2 = new Car();
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//Output "Mike,John,Bill"
alert(oCar2.drivers);	//Output "Mike,John,Bill"

Try It Yourself

In the above code, the property drivers is a pointer to an Array object that contains two names "Mike" and "John". Since drivers is a reference value, both instances of Car point to the same array. This means that adding the value "Bill" to oCar1.drivers can also be seen in oCar2.drivers. Outputting either of these pointers results in displaying the string "Mike,John,Bill".

With so many issues when creating objects, you might wonder if there is a reasonable way to create objects. The answer is yes, it requires the combined use of constructors and prototype methods.

Mixed constructor/prototype method

By combining the constructor and prototype methods, objects can be created just like in other programming languages. This concept is very simple, that is, all non-function properties of the object are defined using the constructor, and the function properties (methods) of the object are defined using the prototype method. As a result, all functions are created only once, and each object has its own instance of object properties.

We have rewritten the previous example, the code is as follows:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
  alert(this.color);
;
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//Output "Mike,John,Bill"
alert(oCar2.drivers);	//Output "Mike,John"

Try It Yourself

Now it is more like creating a general object. All non-function properties are created in the constructor, which means that default values for properties can be assigned using the constructor's parameters again. Since only one instance of the showColor() function is created, there is no memory waste. Moreover, adding the value "Bill" to the drivers array of oCar1 does not affect the array of oCar2, so when outputting the values of these arrays, oCar1.drivers displays "Mike,John,Bill", while oCar2.drivers displays "Mike,John". Because the prototype method is used, the instanceof operator can still be used to determine the type of the object.

This is the main approach adopted by ECMAScript, which has the characteristics of other methods without their side effects. However, some developers still feel that this method is not perfect.

Dynamic Prototype Methods

For developers accustomed to using other languages, using the mixed constructor/proto method may feel less harmonious. After all, when defining a class, most object-oriented languages visually encapsulate properties and methods. Consider the following Java class:

class Car {
  public String color = "blue";
  public int doors = 4;
  public int mpg = 25;
  public Car(String color, int doors, int mpg) {
    this.color = color;
    this.doors = doors;
    this.mpg = mpg;
  }
  public void showColor() {
    System.out.println(color);
  }
}

Java packages all properties and methods of the Car class well, so seeing this code knows what it is intended to do, defining the information of an object. Critics of the mixed constructor/proto method argue that it is illogical to find properties within the constructor and methods outside of it. Therefore, they designed dynamic prototype methods to provide a more friendly coding style.

The basic idea of dynamic prototype methods is similar to that of mixed constructor/proto methods, that is, defining non-function properties within the constructor while function properties are defined using prototype properties. The only difference is the position where methods are assigned to the object. Below is the Car class rewritten using dynamic prototype methods:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
  if (typeof Car._initialized == "undefined"}
    Car.prototype.showColor = function() {
      alert(this.color);
    ;
    Car._initialized = true;
  }
}

Try It Yourself

This constructor does not change until the check typeof Car._initialized is not equal to "undefined". This is the most important part of the dynamic prototype method. If this value is undefined, the constructor will continue to define the object's methods in the prototype pattern, and then set Car._initialized to true. If this value is defined (its value is true when typeof is Boolean), the method will not be created. In short, this method uses a flag (_initialized) to determine whether any methods have been assigned to the prototype. This method is created and assigned only once, and traditional OOP developers will be pleased to find that this code looks more like class definitions in other languages.

Mixed factory method

This method is usually a workaround when the previous method cannot be applied. Its purpose is to create a pseudo-constructor that only returns a new instance of another object.

This code looks very similar to a factory function:

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  ;
  return oTempCar;
}

Try It Yourself

Unlike the classic method, this method uses the new operator, making it look like a real constructor:

var car = new Car();

Since the new operator is called within the Car() constructor, the second new operator (outside the constructor) will be ignored, and the object created within the constructor is passed back to the variable car.

This method has the same problem in the internal management of object methods as the classic method. It is strongly recommended that you avoid using this method unless absolutely necessary.

Which method to use

As mentioned earlier, the most widely used method at present is the mixed constructor/proto pattern. In addition, dynamic native methods are also popular, which are functionally equivalent to the constructor/proto pattern. You can use either of these two methods. However, do not use the classic constructor or prototype pattern alone, as this will introduce problems into the code.

Instance

One interesting aspect of objects is the way they solve problems. One of the most common problems in ECMAScript is the performance of string concatenation. Like other languages, ECMAScript strings are immutable, meaning their values cannot be changed. Consider the following code:

var str = "hello ";
str += "world";

In fact, the following steps are executed behind this code:

  1. Create a string to store "hello ";
  2. Create a string to store "world".
  3. Create a string to store the concatenated result.
  4. Copy the current content of str to the result.
  5. Copy "world" to the result.
  6. Update str to point to the result.

Each time a string concatenation is completed, steps 2 to 6 are executed, making this operation very resource-intensive. If this process is repeated hundreds of times, even thousands of times, it will cause performance issues. The solution is to store strings in an Array object and then create the final string using the join() method (with an empty string as the parameter). Imagine replacing the previous code with the following code:

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join('');

Thus, it is no problem to introduce as many strings as needed into the array, because the concatenation operation only occurs when the join() method is called. The steps executed at this time are as follows:

  1. Create a string to store the result
  2. Copy each string to the appropriate position in the result

Although this solution is good, there is a better way. The problem is that this code does not accurately reflect its intention. To make it easier to understand, you can package this feature with the StringBuffer class:

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
;
StringBuffer.prototype.toString = function() {
  return this._strings_.join('');
;

This code pays attention to the 'strings' attribute first, which is a private attribute by default. It has only two methods, namely the append() and toString() methods. The append() method takes a parameter, which appends the parameter to the string array. The toString() method calls the array's join method to return the truly concatenated string. To concatenate a group of strings using a StringBuffer object, you can use the following code:

var buffer = new StringBuffer();
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

Try It Yourself

You can test the performance of the StringBuffer object and the traditional string concatenation method with the following code:

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("Concatenation with plus: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");
var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 10000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();
document.write("<br />Concatenation with StringBuffer: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");

Try It Yourself

This code performs two tests on string concatenation, the first using the plus sign, and the second using the StringBuffer class. Each operation concatenates 10,000 strings. The date values d1 and d2 are used to determine the time required to complete the operation. Note that if no parameters are provided when creating a Date object, the object is assigned the current date and time. To calculate the time taken for the concatenation operation, simply subtract the millisecond representation of the dates (using the return value of the getTime() method). This is a common method for measuring JavaScript performance. The results of this test can help you compare the efficiency difference between using the StringBuffer class and using the plus sign.