Classes and factory functions in JavaScript. What to choose?

Original author: Cristi Salcescu
  • Transfer
JavaScript has various ways to create objects. In particular, we are talking about constructions that use the keyword classand the so-called factory functions. The author of the material, the translation of which we publish today, explores and compares these two concepts in search of an answer to the question about the pros and cons of each of them.

image

Overview


The keyword classappeared in ECMAScript 2015 (ES6), as a result we now have two competing patterns for creating objects. In order to compare them, I will describe the same object ( TodoModel) using the class syntax and applying the factory function.

Here's what the description looks like TodoModelusing the keyword class:

classTodoModel{
    constructor(){
        this.todos = [];
        this.lastChange = null;
    }
    
    addToPrivateList(){ 
       console.log("addToPrivateList"); 
    }
    add() { console.log("add"); }
    reload(){}
}

Here is a description of the same object made by means of the factory function:

function TodoModel(){
    var todos = [];
    var lastChange = null;
        
    function addToPrivateList(){ 
        console.log("addToPrivateList"); 
    }
    functionadd() { console.log("add"); }
    function reload(){}
    
    returnObject.freeze({
        add,
        reload
    });
}

Consider the features of these two approaches to creating classes.

Encapsulation


The first feature that can be seen when comparing classes and factory functions is that all members, fields, and methods of objects created using the keyword are classpublicly available.

var todoModel = new TodoModel();
console.log(todoModel.todos);     //[]console.log(todoModel.lastChange) //null
todoModel.addToPrivateList();     //addToPrivateList

When using factory functions, only what we consciously open is publicly available, everything else is hidden inside the received object.

var todoModel = TodoModel();
console.log(todoModel.todos);     //undefinedconsole.log(todoModel.lastChange) //undefined
todoModel.addToPrivateList();     //taskModel.addToPrivateList
                                    is not a function

API immunity


After the object is created, I expect that its API will not change, that is, I expect immunity from it. However, we can easily change the implementation of publicly accessible methods of objects created using the keyword class.

todoModel.reload = function() { console.log("a new reload"); }
todoModel.reload();            //a new reload

This problem can be solved by calling Object.freeze(TodoModel.prototype)after the class is declared, or using the decorator to “freeze” classes when it will be supported.

On the other hand, the API of an object created using a factory function is immutable. Note the use of the command Object.freeze()to process the return object, which contains only the public methods of the new object. The private data of this object can be modified, but this can only be done using these public methods.

todoModel.reload = function() { console.log("a new reload"); }
todoModel.reload();            //reload

This keyword


Objects created using the keyword are classsubject to the long-standing problem of losing context this. For example, it thisloses context in nested functions. This not only complicates the programming process, such behavior is also a constant source of errors.

classTodoModel{
    constructor(){
        this.todos = [];
    }
    
    reload(){ 
        setTimeout(functionlog() { 
           console.log(this.todos);    //undefined
        }, 0);
    }
}
todoModel.reload();                   //undefined

Here's how to thislose context when using the appropriate method in the DOM event:

$("#btn").click(todoModel.reload);    //undefined

Objects created using factory functions do not suffer from a similar problem, since the keyword is thisnot used here.

functionTodoModel(){
    var todos = [];
        
    functionreload(){ 
        setTimeout(functionlog() { 
           console.log(todos);        //[]
       }, 0);
    }
}
todoModel.reload();                   //[]
$("#btn").click(todoModel.reload);    //[]

This keyword and arrow functions


Arrow functions partially solve problems associated with loss of context thiswhen using classes, but at the same time, they create a new problem. Namely, when using arrow functions in classes, the keyword thisno longer loses context in nested functions. However, it thisloses context when dealing with DOM events.

I redesigned the class TodoModelusing arrow functions. It is worth noting that in the process of refactoring, when replacing ordinary functions with arrow ones, we lose something important for code readability: function names. Take a look at the following example.

//имя указывает на цель использования функции
setTimeout(functionrenderTodosForReview() { 
      /* code */ 
}, 0);
//код менее понятен при использовании стрелочной функции
setTimeout(() => { 
      /* code */ 
}, 0);

When using arrow functions, I have to read the function text in order to understand what exactly it does. I would like to read the name of the function and understand its essence, and not read all its code. Of course, you can ensure good readability of the code when using arrow functions. For example, you can make the habit of using arrow functions like this:

var renderTodosForReview = () => { 
     /* code */ 
};
setTimeout(renderTodosForReview, 0);

New operator


When creating objects based on classes, you need to use the operator new. And when creating objects using factory functions is newnot required. However, if the use newimproves the readability of the code, this operator can also be used with factory functions, there will be no harm from this.

var todoModel= new TodoModel();

When usednew with a factory function, the function simply returns the object it created.

Security


Suppose an application uses an object Userto work with authorization mechanisms. I created a couple of such objects using both of the approaches described here.

Here is a description of the object Userusing the class:

classUser{
    constructor(){
        this.authorized = false;
    }
    
    isAuthorized(){
        returnthis.authorized;
    }
}
const user = new User();

Here's what the same object described by means of the factory function looks like :

functionUser() {
    var authorized = false;
       
    function isAuthorized(){
       return authorized;
    }
    
    returnObject.freeze({
        isAuthorized
    });
}
const user = User();

Objects created using the keyword classare vulnerable to attacks if the attacker has a link to the object. Since all properties of all objects are publicly available, an attacker can use other objects to gain access to the object in which he is interested.

For example, you can obtain the appropriate rights directly from the developer console if the variable useris global. To verify this, open the sample code and modify the variable userfrom the console.

This example was prepared using the Plunker resource . In order to access global variables, change the context in the console tab from topto plunkerPreviewTarget(run.plnkr.co/).

user.authorized = true;            //доступ к закрытому свойству
user.isAuthorized = function() { returntrue; }  //переопределение APIconsole.log(user.isAuthorized());  //true


Modifying an object using the developer console An

object created using a factory function cannot be modified externally.

Composition and inheritance


Classes support both inheritance and composition of objects.

I created an inheritance example in which a class SpecialServiceis a class inheritor Service.

classService{
    log(){}
}
classSpecialServiceextendsService{
   logSomething(){ console.log("logSomething"); }  
}
var specialService = newSpecialService();
specialService.log();
specialService.logSomething();

When using factory functions, inheritance is not supported; here you can only use composition. Alternatively, you can use the command Object.assign()to copy all properties from existing objects. For example , suppose we need to reuse all the members of an object Servicein an object SpecialService.

function Service() {
    function log(){}      
    
    returnObject.freeze({
        log
    });
}
function SpecialService(args){
   var standardService = args.standardService;
  
   function logSomething(){ 
       console.log("logSomething"); 
   }
  
   returnObject.freeze(Object.assign({}, standardService, {
       logSomething
   }));
}
var specialService = SpecialService({
       standardService : Service()
    });
specialService.log();
specialService.logSomething();

Factory functions facilitate the use of composition instead of inheritance, which gives the developer a higher level of flexibility in terms of application design.

When using classes, you can also prefer composition to inheritance, in fact, these are just architectural decisions regarding the reuse of existing behavior.

Memory


Using classes helps to save memory, as they are implemented on the basis of the prototype system. All methods are created only once, in the prototype, they are used by all instances of the class.

The additional cost of memory consumed by objects created using factory functions is noticeable only when thousands of similar objects are created.

Here is the page used to find out the memory costs that are typical for using factory functions. Here are the results obtained in Chrome for a different number of objects with 10 and 20 methods.


Memory overhead (in Chrome)

OOP objects and data structures


Before continuing with the analysis of memory costs, two types of objects should be distinguished:

  • OOP objects
  • Objects with data (data structures).

Objects provide behavior and hide data.

Data structures provide data, but do not have any significant behavior.

Robert Martin, Clean Code .


Let us take a look at an example of an object already familiar to you TodoModelin order to clarify the difference between objects and data structures.

function TodoModel(){
    var todos = [];
           
    functionadd() { }
    function reload(){ }
       
    returnObject.freeze({
        add,
        reload
    });
}

An object is TodoModelresponsible for storing todoand managing a list of objects . TodoModel- This is an OOP object, the one that provides behavior and hides data. The application will have only one instance of it, so when you create it using the factory function, additional memory costs are not required.

Objects stored in an arraytodosAre data structures. There may be many such objects in a program, but these are ordinary JavaScript objects. We are not interested in making their methods private. Rather, we strive to ensure that all of their properties and methods are publicly available. As a result, all these objects will be built using a prototype system, so that we will be able to save memory. They can be created using a regular object literal or command Object.create().

User interface components


Applications can have hundreds or thousands of instances of user interface components. This is the situation in which a compromise must be found between encapsulation and saving memory.

Components will be created in accordance with the methods adopted in the framework used. For example, Vue uses object literals, while React uses classes. Each member of the component object will be publicly available, but thanks to the use of a prototype system, the use of such objects will save memory.

Opposing OOP paradigms


In a broader sense, classes and factory functions demonstrate the battle of two opposing paradigms of object-oriented programming.

Class-based OOP as applied to JavaScript means the following:

  • All objects in the application are described using the class syntax, using the types specified by the classes.
  • To write programs, they are looking for a language with static typing, the code on which is then transformed into JavaScript.
  • During development, they use interfaces.
  • Apply composition and inheritance.
  • Functional programming is used very little, or almost does not show interest in it.

OOP without using classes comes down to the following:

  • Types defined by the developer are not used. There is no place for something like this in the paradigm instanceof. All objects are created using object literals, some of them with public methods (OOP objects), some with public properties (data structures).
  • During development, dynamic typing is used.
  • Interfaces are not used. The developer is only interested in whether the object has the property it needs. Such an object can be created using a factory function.
  • Composition is applied, but not inheritance. If necessary, all members of one object are copied to another using Object.assign().
  • Functional programming is used.

Summary


The strengths of classes are that they are familiar to programmers who come to JS from languages ​​whose development is based on classes. Classes in JS are syntactic sugar for a prototype system. However, security issues and usage this, leading to persistent errors due to loss of context, put classes in second place compared to factory functions. As an exception, classes are resorted to when they are used in the framework used, for example, in React.

Factory functions are not only a tool for creating secure, encapsulated, and flexible OOP objects. This approach to creating classes also opens the door to a new programming paradigm unique to JavaScript.

I allow myself to quote Douglas Crockford in conclusion of this article : “I think OOP without classes is a gift to humanity from JavaScript.”

Dear readers! What and why is closer to you: classes or factory functions?


Also popular now: