Reputation: 6995
I've noticed there are two different ways to effectively write a class (or class-like thing) in JavaScript. Using a simple counter class as an example:
A real class (used as new Counter1()
)
class Counter1 {
constructor(count = 0) {
this.count = count;
}
inc() {
this.count++;
}
get() {
return this.count;
}
}
A function returning an object (used as Counter2()
)
function Counter2(count = 0) {
return {
inc() {
count++;
},
get() {
return count;
},
};
}
I have a lot of code that could be written in either one of these styles. Which should I use?
This is my understanding:
Counter1
can be used with instanceof
and inheritance, but is more verbose and doesn't have real private properties (eg count
property is exposed).
Counter2
is more concise and has real private methods/state (just local variables in the function), but can't be used with instanceof
and inheritance. It also creates new copies of the inc
and get
functions for each instance, which may impact performance? I don't know.
Upvotes: 3
Views: 8598
Reputation: 29109
The biggest difference between the two is that calling new
on a class
creates a this
object, while the modular function may or may not create an instance-like state object.
I am not going to post the advantages of using a class
because there are hundreds of them in Stack Overflow. Instead, here is why you might not want to use a class
.
By using the modular function instead of a class, you:
.this
on instance-like propertiesClass.[name]
on static-like propertiesthis
objectnew
to invoke itnew
keywordnew
keyword.class
properties lurking inside.In the most concise syntax
() => ({})
With constructor-like params and closure access
(a,b,c) => ({
someA: a,
bAndC: b + c
})
A full example
let lastTotal
const randomPct = () => Math.random()*100
function SomeModule(a,b,c) {
const myTotal = a + b + c
lastTotal = myTotal
const getASquared = () => a * a
return {
total: a + b + c,
getTotal() {
return a + b + c
},
myTotal,
lastTotal,
randomPct,
getASquared,
}
}
A composition example. You have the flexiblity to rename conflicts, or exclude any members of moduleA and moduleB, even conditionally.
const moduleA = useModuleA()
const moduleB = useModuleB()
const moduleC = (moduleA, moduleB) {
const exportedB = isAdmin() ? moduleB : {publicA: moduleB.publicA, publicB:moduleB.publicB}
return {
...moduleA,
...exportedB,
hello: 'World',
}
}
Upvotes: 0
Reputation: 6995
I put it in a jsperf page here to test the speed of new instance creation in either style: https://jsperf.com/class-vs-object-2
Here are the results on my computer (new instances created per second):
Test Chrome Firefox Edge
new Counter1(); 217,901,688 2,086,800,399 31,106,382
Counter2(); 30,495,508 36,113,817 9,232,171
I don't know why the Firefox number with class
is so high (hopefully it didn't notice the code had no effect and elide the whole thing) or why the Edge numbers are so low.
So at least with regards to performance, a real class is much faster, probably because it avoids the overhead of creating a new copy of the methods for each instance.
While the verbosity and lack of private state with classes is annoying, I think the performance benefit is more important, but it depends on the situation.
Edit
I've made the test call inc()
and get()
on the resulting object as well and the numbers are basically the same:
Test Chrome Firefox Edge
new Counter1(); 215,352,342 2,072,278,834 23,570,036
Counter2(); 14,309,836 35,564,201 9,801,748
Upvotes: 4
Reputation: 3842
I recommend to use classes, because of that,
Class methods are non-enumerable.
A class definition sets of enumerable flag to false for all class member methods in the "prototype". That's good, because if we for..in
over an object, we usually don't want it's class methods.
Classes have a default constructor() {}.
If there is no constructor
in the class
constructor, then an empty function is generated, same as if we had written constructor() {}.
Classes always use strict.
All code inside the class construct is automatically in strict mode.
We can also assign methods to the class function, not to its "prototype".
You can go further here.
Upvotes: 1
Reputation:
Which should I use?
This question is basically a matter of personal preference. True believers in OOP will of course adopt the class
approach. The one clear difference is that that would allow you to subclass Counter1
, but it's not clear why you would ever want to do that, and even OOP aficionados warn against building overly complex class hierarchies.
Yes, the second alternative does place the methods in question on each and every instance, instead of the prototype. Yes, that does impose some theoretical performance penalty. But this is not something you ever need to worry about, unless you are writing a game with one counter for each of a million objects.
Presumably you are using some kind of transpiler or running in a native ES6 environment. If for some reason you are not, then of course your second alternative has the advantage that it will run in any browser from the last ten years (if you rewrite the methods as inc: function() { count++; }
).
Upvotes: 0
Reputation: 9
Neither one should have a very big impact on performance. While classes (Counter1
in your example) technically have slightly better performance, this is only noticeable if you are creating several thousand instances of the class per second (which is probably a bad design choice anyway).
As for other advantages, there are advantages and disadvantages to both. FunFunFunction is a great YouTube channel for learning about JavaScript, and it's where I first learned about factory functions (Counter
). The channel owner strongly favors these over using class
es.
One of the reasons for this is that you can't get consistent behavior out of the this
keyword with the way classes are implemented in JS, as it may refer to something like an HTML element from which a function was called rather than the object that you're expecting it to.
For more details, see this video.
Of course, this is only one side of the argument, and I'm sure there's arguments for using class
as well. I prefer factory functions, but in the end you really just need to pick one and roll with it.
Upvotes: 0