Search code examples
javascripttypescriptpass-by-referencepass-by-value

Is it most efficient to have all functions accept only parameter primitives wrapped by an object?


In JavaScript (or TypeScript), objects are passed by reference, unlike primitives which are copied in the function. Therefore, isn't it the case that this:

sum(one: number, two: number): number {
    return one + two;
}

is always less efficient than this?

sum(input: { one: number, two: number}): number {
    return input.one + input.two;
}

In the first function, 'one' and 'two' are made copies of. In the second function, 'one' and 'two' are simply references to the original 'one' and 'two' encased in an object, so no copies will be made, which saves computation right?

Of course, if I would need to manipulate the values of 'one' and 'two' and wouldn't want the changes to persist, I would use the second function.

Therefore, shouldn't it always be an advised best practice for JavaScript developers to encase their function parameter primitives in an object (given that they don't want manipulations to persist)?


Solution

  • Therefore, it should be the case that this:

    sum(one: number, two: number): number {
        return one + two;
    }
    

    is always less efficient than this:

    sum(input: { one: number, two: number}): number {
        return input.one + input.two;
    }
    

    That doesn't follow.

    What's passed to a function are the values of its arguments. There are a couple of different ways to interpret "value" in this context in computer science, so I'll use the really, really pragmatic one: The bits that go in a variable, on the stack, etc. at runtime.

    Values are extremely efficient to pass to functions. You push them on the stack, and the function pops them off the stack. So

    sum(1, 2);
    

    does this:

    • Pushes 1 on the stack.
    • Pushes 2 on the stack.
    • Calls the function, which
      • pops the values off the stack
      • adds them together
      • pushes the return value on the stack
      • returns

    (Handwaving away some details!)

    In your second example, to call sum, you have to create an object:

    sum({one: 1, two: 2});
    

    so it does this:

    • Allocates memory for the object.
    • Creates a property, with slots to remember the name "one" and the value for it; puts the value 1 in the value slot.
    • Creates a second property, with slots to remember the name "two" and the value for it; puts the value 2 in the value slot.
    • Pushes that object's reference (a value saying, roughly, where it is in memory) on the stack.
    • Calls the function, which:
      • pops the object reference off the stack.
      • looks up the property "one" in the object and puts its value in a local variable (which, amusingly, will probably be on the stack).
      • looks up the property "two" in the object and puts its value in a local variable.
      • adds them together
      • pushes the result on the stack
      • returns

    So you've saved one push/pop, but at the cost of allocating an object and filling in two properties on it, then looking up those values later; all of which is more expensive than a stack operation.

    Now, in JavaScript, we create an release objects a lot, so engines are very good at it. If you have a function where it makes the most sense to pass it an object, by all means do that. (For instance: If the function needs more than three pieces of information in order to do its work, it's often a better developer experience to have them pass in an object with named properties rather than having them remember the order of parameters, although of course IDEs help).