javascriptdecoratorfunction-expressionjavascript-decorators

Function Expression vs Function Declaration in Javascript Decorators


I have a problem understanding how different ways to create a function are affecting decorators. I am trying to create a decorator that will allow me to count how may times function was called:


function counter(func) {

    wrapper = function(...args) {
        wrapper.counter++;
        let arguments = [].slice.call(args, 0)
        let result = func.apply(null, arguments);
        return result;
    }

    wrapper.counter = 0;
    return wrapper;
}

function sum(x, y, z) {
    return x + y + z
}

function mul(a, b, c) {
    return a * b * c
}

a = counter(sum);
b = counter(mul);

console.log(`sum of 3, 6 and 8 is ${a(3, 6, 8)}`);
console.log(`mul of 1, 2 and 3 is ${b(1, 2, 3)}`);
console.log(`sum of 2, 2 and 2 is ${a(2, 2, 2)}`);
console.log(`mul of 5, 6 and 2 is ${b(5, 6, 2)}`);

console.log(`a.counter is ${a.counter}`);
console.log(`b.counter is ${b.counter}`);

This code results in following output:

sum of 3, 6 and 8 is 17
mul of 1, 2 and 3 is 6
sum of 2, 2 and 2 is 6
mul of 5, 6 and 2 is 60
a.counter is 0
b.counter is 4

As you can see, a and b are sharing reference the same counter, one that belongs to b, which shouldn't be happening.

However, if I change function expression wrapper = function(...args) to function declaration function wrapper(...args), like this:

function counter(func) {

    function wrapper(...args) {
        wrapper.counter++;
        let arguments = [].slice.call(args, 0)
        let result = func.apply(null, arguments);
        return result;
    }

    wrapper.counter = 0;
    return wrapper;
}

function sum(x, y, z) {
    return x + y + z
}

function mul(a, b, c) {
    return a * b * c
}

a = counter(sum);
b = counter(mul);

console.log(`sum of 3, 6 and 8 is ${a(3, 6, 8)}`);
console.log(`mul of 1, 2 and 3 is ${b(1, 2, 3)}`);
console.log(`sum of 2, 2 and 2 is ${a(2, 2, 2)}`);
console.log(`mul of 5, 6 and 2 is ${b(5, 6, 2)}`);

console.log(`a.counter is ${a.counter}`);
console.log(`b.counter is ${b.counter}`);

Then a and b are having correct counters and everything works fine:

sum of 3, 6 and 8 is 17
mul of 1, 2 and 3 is 6
sum of 2, 2 and 2 is 6
mul of 5, 6 and 2 is 60
a.counter is 2
b.counter is 2

What causes changes in behavior like that?

I tried to find solution to this problem myself, but didn't found anything of help. Any help is appreciated!


Solution

  • The main issue here isn't to do with function expressions vs function declarations, it's to do with the fact that you're creating wrapper without var, let or const keywords. When you create a variable without one of these, they become a global variable, so it's not scoped to counter function. That means that when you call counter() the first time, it creates your wrapper function and stores a reference to that in a, it also creates a global wrapper variable to hold the function that was just created. Calling counter() again then overwrites that global variable to store the new function you just created, but a still holds a reference to the original one. Each time you perform wrapper.counter++; you're now updating the global wrapper function' .counter property, which is bs function (not a's). This results inn a.counnter being 0 but b.counter being 4.

    When you do function wrapper(...args) {} on the other hand, wrapper is naturally scoped to the function/scope it's declared in, and doesn't become a global variable like in your first example. To fix your first snippet, you can declare wrapper with const to make it scoped to counter and to avoid creating a global variable that's accessible outside of counter.

    function counter(func) {
    
      const wrapper = function(...args) {
        wrapper.counter++;
        let arguments = [].slice.call(args, 0)
        let result = func.apply(null, arguments);
        return result;
      }
    
      wrapper.counter = 0;
      return wrapper;
    }
    
    function sum(x, y, z) {
      return x + y + z
    }
    
    function mul(a, b, c) {
      return a * b * c
    }
    
    a = counter(sum);
    b = counter(mul);
    
    console.log(`sum of 3, 6 and 8 is ${a(3, 6, 8)}`);
    console.log(`mul of 1, 2 and 3 is ${b(1, 2, 3)}`);
    console.log(`sum of 2, 2 and 2 is ${a(2, 2, 2)}`);
    console.log(`mul of 5, 6 and 2 is ${b(5, 6, 2)}`);
    
    console.log(`a.counter is ${a.counter}`);
    console.log(`b.counter is ${b.counter}`);

    Using "use strict" would've helped you catch this bug also, as in strict mode, you can't assign to an undeclared variable.

    "use strict";
    function counter(func) {
      wrapper = function(...args) {} // "Uncaught ReferenceError: wrapper is not defined"
    }
    
    const a = counter((a, b) => a + b);