Let's say I were to write a class (this has no literal meaning I'm just writing some BS)
class A {
constructor(name, date) {
this.nameDateString = name date;
}
someMethod() {
return `${this.nameDateString} ${this.nameDateString}`;
}
addAnotherNameDateString(name, date) {
this.nameDateString = name date;
}
}
Would writing a function that creates a closure (am I using this term right?) on some data by defining some functions and returning them:
const createA = (name, date) => {
let nameDateString = name date;
const someMethod = () => {
return `${nameDateString} ${nameDateString}`;
}
const addAnotherNameDateString = (name, date) => {
nameDateString = name date;
}
return { someMethod, addAnotherNameDateString };
}
be less efficient in terms of memory?
For example, would
Array(100000).fill(null).map((_, i) => new A(i, new Date() i*123456))
be more advantageous than:
Array(100000).fill(null).map((_, i) => createA(i, new Date() i*123456))
CodePudding user response:
Just up front: Programming using a style with createA
or similar is quite common; it's one of the popular ways is JavaScript is used. Programming with class A
or similar is also quite common, and one of the (perhaps slightly more) popular ways JavaScript is used.
Would [creating different functions each time] be less efficient in terms of memory?
Yes, a bit, for a couple of different reasons: function objects and the closure. But whether that matters is another thing entirely, relative to other objectives/concerns.
With your class
, the function objects for the methods are on A.prototype
and are reused by each instance of the class (they're inherited from the prototype assigned to them):
class A {
constructor(name, date) {
this.nameDateString = name date;
}
someMethod() {
return `${this.nameDateString} ${this.nameDateString}`;
}
addAnotherNameDateString(name, date) {
this.nameDateString = name date;
}
}
const a1 = new A("Joe", new Date());
const a2 = new A("Mary", new Date());
console.log(a1.someMethod === a2.someMethod); // true
With your createA
function, different function objects for those methods are created every time createA
is called:
const createA = (name, date) => {
let nameDateString = name date;
const someMethod = () => {
return `${nameDateString} ${nameDateString}`;
}
const addAnotherNameDateString = (name, date) => {
nameDateString = name date;
}
return { someMethod, addAnotherNameDateString };
}
const a1 = createA("Joe", new Date());
const a2 = createA("Mary", new Date());
console.log(a1.someMethod === a2.someMethod); // false
So the objects created by createA
have a larger memory imprint than the objects created by new A
, because they have their own copies of those function objects (and because of the closure, more in a moment).
But, function objects aren't that big. The underlying code (the bytecode and/or machine code created by parsing the source text) will be reused (by any half-decent JavaScript engine). That is, the code for someMethod
will be reused by the multiple function objects created for someMethod
by different calls to createA
. Suppose we have:
const a1 = createA("Joe", new Date());
const a2 = createA("Mary", new Date());
Then we end up with something something like this in memory (lots of details omitted, of course):
−−−−−−−−−−−− a1−−>| (object) | −−−−−−−−−−−− −−−−−−−−−−−−−−−−−−−− | someMethod |−−−>| (function) | | ... | −−−−−−−−−−−−−−−−−−−− −−−−−−−−−−−− | name: "someMethod" | | length: 0 | −−| [[Environment]] | | | {{Code}} |−−−−−−−−−−−−−−− | −−−−−−−−−−−−−−−−−−−− | | | | −−−−−−−−−−−−−−−−−−−−−−−−−−− | −−−−−−−>| (environment object) | | −−−−−−−−−−−−−−−−−−−−−−−−−−− | | nameDateString: "Joe..." | | −−−−−−−−−−−−−−−−−−−−−−−−−−− | | −−−−−−−−−−−−−−−−− −−−−−−−−−−−− −>| (code) | a2−−>| (object) | | −−−−−−−−−−−−−−−−− −−−−−−−−−−−− −−−−−−−−−−−−−−−−−−−− | | 100100001000... | | someMethod |−−−>| (function) | | −−−−−−−−−−−−−−−−− | ... | −−−−−−−−−−−−−−−−−−−− | −−−−−−−−−−−− | name: "someMethod" | | | length: 0 | | −−| [[Environment]] | | | | {{Code}} |−−−−−−−−−−−−−−− | −−−−−−−−−−−−−−−−−−−− | | −−−−−−−−−−−−−−−−−−−−−−−−−−− −−−−−−−>| (environment object) | −−−−−−−−−−−−−−−−−−−−−−−−−−− | nameDateString: "Mary..." | −−−−−−−−−−−−−−−−−−−−−−−−−−−
That diagram points out the other slight difference: The createA
style will keep the environment object for the call to createA
in memory, since someMethod
and addAnotherNameDateString
close over that environment. So instead of a single object created and retained by new A
, you have four objects created and retained by createA
:
- The object
createA
returns. - A function object for
someMethod
... - ...and
addAnotherNameDateString
- The lexical environment object they close over
In contrast, new A
just produces one object.
But understanding that, if the createA
style fits your goals better than the class A
style, or vice-versa, use the one that fits your goals. The size difference is unlikely to matter for 100,000 instances (even on mobile devices; but certainly it might in an embedded environment). But if you're in the millions, you may well need to start thinking about avoiding memory pressure.
If you're worried about the memory impact, measure it with your real code. In major browsers, you can use a Memory tab in the dev tools to help you measure the impact of your code choices on memory consumption.
CodePudding user response:
This is dependent on the JavaScript engine running your program; some engines could be clever enough to realize that createA
's functions are always the same but with different closures, and optimize accordingly. You would need to measure both approaches to see for sure. (As discussed in the comments, any major engine these days will likely optimize things enough that the underlying code object is shared between functions, even if the variable closure is different.)
With a class
, the engine doesn't need to be clever since the methods are on the prototype
of the class, and it's more idiomatic these days anyway, so I'd just go with that.