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!
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 b
s 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);