Home > Software design >  Is creating an object with methods defined on it via a function less memory efficient than creating
Is creating an object with methods defined on it via a function less memory efficient than creating

Time:04-14

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.

  • Related