Classes and factory functions in JavaScript. What to choose?
- Transfer
JavaScript has various ways to create objects. In particular, we are talking about constructions that use the keyword
The keyword
Here's what the description looks like
Here is a description of the same object made by means of the factory function:
Consider the features of these two approaches to creating classes.
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
When using factory functions, only what we consciously open is publicly available, everything else is hidden inside the received object.
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
This problem can be solved by calling
On the other hand, the API of an object created using a factory function is immutable. Note the use of the command
Objects created using the keyword are
Here's how to
Objects created using factory functions do not suffer from a similar problem, since the keyword is
Arrow functions partially solve problems associated with loss of context
I redesigned the class
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:
When creating objects based on classes, you need to use the operator
When used
Suppose an application uses an object
Here is a description of the object
Here's what the same object described by means of the factory function looks like :
Objects created using the keyword
For example, you can obtain the appropriate rights directly from the developer console if the variable
This example was prepared using the Plunker resource . In order to access global variables, change the context in the console tab from
Modifying an object using the developer console An
object created using a factory function cannot be modified externally.
Classes support both inheritance and composition of objects.
I created an inheritance example in which a class
When using factory functions, inheritance is not supported; here you can only use composition. Alternatively, you can use the command
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.
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)
Before continuing with the analysis of memory costs, two types of objects should be distinguished:
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
An object is
Objects stored in an array
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.
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:
OOP without using classes comes down to the following:
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
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?
class
and 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.Overview
The keyword
class
appeared 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
TodoModel
using 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
class
publicly 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
class
subject to the long-standing problem of losing context this
. For example, it this
loses 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
this
lose 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
this
not 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
this
when using classes, but at the same time, they create a new problem. Namely, when using arrow functions in classes, the keyword this
no longer loses context in nested functions. However, it this
loses context when dealing with DOM events. I redesigned the class
TodoModel
using 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 new
not required. However, if the use new
improves 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 used
new
with a factory function, the function simply returns the object it created.Security
Suppose an application uses an object
User
to 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
User
using 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
class
are 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
user
is global. To verify this, open the sample code and modify the variable user
from the console. This example was prepared using the Plunker resource . In order to access global variables, change the context in the console tab from
top
to 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
SpecialService
is 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 Service
in 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
TodoModel
in order to clarify the difference between objects and data structures.function TodoModel(){
var todos = [];
functionadd() { }
function reload(){ }
returnObject.freeze({
add,
reload
});
}
An object is
TodoModel
responsible for storing todo
and 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 array
todos
Are 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?