Since JavaScript doesn't have true tail calls (yet) what you are experiencing isn't a slowdown based on the value of z but a slowdown based on stack growth and the inability of the garbage collector (GC) to cleanup the stack of scopes that are necessary to retain this functionality.
In theory, you can change this behavior by using setImmediate and the callback pattern to solve for this. Using setImmediate allows the scope of the current execution loop to fall out of use and get collected during the next GC cycle.
So, something like:
var x = function x(z){
console.log(z);
if (z > 0) {
setImmediate(function(){
x(z-1);
});
}
};
But this will not work because z is passed down into the scope for setImmediate and thus the previous scope of x can't be GC'd properly.
Instead you have to use IIFE (Immediately Invoked Function Expression) to achieve what you are looking for in combination with using setImmediate to allow the execution cycle to have time to allow for GC:
var x = function x(z){
console.log(z);
if (z > 0) {
setImmediate((function(newZ){
return function(){
x(newZ);
};
})(z-1));
}
};
Barring any typos I think I got the above correct. Of course if your using ES6 you can shorthand this greatly as well.
The other side of this equation is the spooling effect of console.log and the buffer resizes your going to incur for doing this in a browser. In an OS terminal these costs will be minimized as backbuffer scroll is limited and the back buffer is pre-allocated and reused.