一直以为ES6出了后,新特性都是值得推广、使用的,直到看了这篇文章,甚有醍醐灌顶之感,来不及翻译,直接转载过来(原文需要翻墙查看)
原文地址:Class vs Factory function: exploring the way forward
ECMAScript 2015 (aka ES6) comes with the class syntax, so now we have two competing patterns for creating objects. In order to compare them, I’ll create the same object definition (TodoModel) as a class, and then as a factory function.
1 | class TodoModel { |
TodoModel as a Factory Function
1 | function TodoModel(){ |
Encapsulation
The first thing we notice is that all members, fields, and methods of a class object are public.
1 | var todoModel = new TodoModel(); |
The lack of encapsulation may create security problems. Take the example of a global object that can be modified directly from the Developer Console.
When using factory function, only the methods we expose are public, everything else is encapsulated.
1 | var todoModel = TodoModel(); |
this
this
losing context problems are still there when using class. For example, this
is losing context in nested functions. It is not only annoying during coding, but it’s also a constant source of bugs.
1 | class TodoModel { |
or this
is losing context when the method is used as a callback, like on a DOM event.
1 | $("#btn").click(todoModel.reload); //undefined |
There are no such problems when using a factory function, as it doesn’t use this
at all.
1 | function TodoModel(){ |
this and arrow function
The arrow function partially solves the this
loosing context issues in classes, but at the same time creates a new problem:
this
is no longer loosing context in nested functionsthis
is loosing context when the method is used as a callback- arrow function promotes the use of anonymous functions
I refactored the TodoModel
using the arrow function. It’s important to note that in the process of refactoring to the arrow function we can loose something very important for readability, the function name. Look for example at:
1 | //using function name to express intent |
Immutable API
Once the object is created, I’m expecting its API to be immutable. I can easily change the implementation of a public method to do something else when it was created using a class.
1 | todoModel.reload = function() { console.log("a new reload"); } |
This problem can be solved by calling Object.freeze(TodoModel.prototype)
after the class definition.
The API of the object created using a factory function is immutable. Notice the use of Object.freeze()
on the returned object containing only the public methods. The private data of the object can be modified, but only through these public methods.
1 | todoModel.reload = function() { console.log("a new reload"); } |
new
new
should be used when creating objects using classes.
new
is not required when creating objects with factory functions, but if that makes it more readable, you can go for it, there is no harm.
1 | var todoModel= new TodoModel(); |
Using new
with a factory function will just return the object created by the factory.
Composition over inheritance
Classes support both inheritance and composition.
Below is an example of inheritance where SpecialService
class inherits from Service
class:
1 | class Service { |
Here is another example where SpecialService
reuses member of Service
using composition:
1 | class Service { |
Factory functions promote composition over inheritance. Take a look at the next example where SpecialService
reuses members of Service
:
1 | function Service() { |
Memory
Classes are better at memory conservation, as they are implemented over the prototype system. All methods will be created only once in the prototype object and shared by all instances.
The memory cost of the factory function is noticeable when creating thousands of the same object.
Here is the page used for testing the memory cost when using factory function.
1 | The memory cost (in Chrome) |
Objects vs Data Structures
Before analyzing the memory cost any further, a distinction should be made between two kinds of objects:
- OOP Objects
- Data Objects (aka Data Structures)
Objects expose behavior and hide data.
Data Structures expose data and have no significant behavior.
I’ll take a look again at the TodoModel
example and explain these two kinds of objects.
1 | function TodoModel(){ |
TodoModel
is responsible for storing and managing the list oftodos
.TodoModel
is the OOP Object, the one exposing behavior and hiding data. There will be only one instance of it in the application, so there’s no extra memory cost when using the factory function.- The
todos
objects represent the Data Structures. There may be a lot of these objects, but they are just plain JavaScript objects. We are not interested in keeping their methods private — rather we actually want to expose all their data and methods. So all these objects will be built over the prototype system, and they will benefit from the memory conservation. They can be built using a simple object literal orObject.create()
.
UI Components
In the application, there may be hundreds or thousands of instances of a UI component. This is a situation where we need to make a trade-off between encapsulation and memory conservation.
Components will be built according to the component framework practice. For example, object literals will be used for Vue, or classes for React. Each component’s members will be public, but they will benefit from the memory conservation of the prototype system.
Conclusion
The strong points of class are its familiarity for people coming from a class-based background and its nicer syntax over the prototype system. However, its security problems and the usage of this
, a continuous source of losing context bugs, makes it a second option. As an exception, classes will be used if required by the component’s framework, as in the case of React.
Factory function is not only the better option for creating secured, encapsulated, and flexible OOP Objects but also opens the door for a new, unique to JavaScript, programming paradigm.
I think Class Free Object Oriented Programming is JavaScript’s gift to humanity.