0

I'm practicing using recursion, and there's something I don't quite get. For example, I wrote this simple countdown function, which is supposed to wait until a second elapsed until counting down to the next second.

I first wrote it like so:

function countdown(sec) {
  console.warn(sec);
  if(sec > 0) {
     sec--;
     setTimeout(countdown(sec), 1000);
  }
}

It does not wait one second between each log. This works:

function countdown(sec){
   console.warn(sec);
   setTimeout(function() {
       sec--;
       if (sec > 0) {
          countdown(sec);
       }
   }, 1000);
};

I don't really understand what's wrong with the first approach. I guess it's something with setTimeout that I don't quite understand, and scoping..?

Thanks in advance for any explanations.

--- edited & working, thanks guys! ---

I didn't know about bind being used as a shorthand.

function countdown(sec) {
   console.warn(sec);
   if (sec > 0) {
       sec--;
       setTimeout(countdown.bind(null, sec), 1000);
   }
}
Vera
  • 233
  • 2
  • 5
  • 12
  • I think you are getting confused with passing a function as argument vs calling the function. The first one calls a function hence the recursion and the second one passes a function as parameter which is what is required – smac89 Mar 15 '17 at 19:45
  • Google for "settimeout called immediately", there are too many results there and I cannot pick one that is the best. – zerkms Mar 15 '17 at 19:45
  • Possible duplicate of [Passing a function as an argument in a javascript function](http://stackoverflow.com/questions/5752030/passing-a-function-as-an-argument-in-a-javascript-function) – smac89 Mar 15 '17 at 19:46
  • setTimeout expects first parameter as a function, In your first approach - you are executing the function. so you can modify you setTimeout call to `setTimeout(countdown.bind(this, sec), 1000);`. Here `bind` will return a closure i.e) a scoped function which holds your `sec` value – ajai Jothi Mar 15 '17 at 19:48
  • oh thank you ajai & smac89! understood. yes indeed, in the first approach, I'm calling countdown(sec) so, this works, even without the bind: `setTimeout(function(){countdown(sec)}, 1000)` Thank you! – Vera Mar 15 '17 at 19:53

2 Answers2

1

JosephNields is correct for why your code was not working, but I'd also like to stress that recursion generally doesn't involve mutating state values, ie sec--

Instead, just pass sec - 1 as the next value for countdown. In other words, there's no gain in setting sec to a smaller number, just recurse with the smaller number

var countdown = function (sec) {
  console.log(sec)
  if (sec > 0)
    setTimeout(countdown, 1000, sec - 1)
}

countdown(10)

Also, wouldn't it be great to know when a timer is done? This example shows passing around another value as you recurse.

var countdown = function (sec, done) {
  console.log(sec)
  if (sec > 0)
    setTimeout(countdown, 1000, sec - 1, done)
  else
    done()
}

countdown(5, function () {
  console.log('timer is all done!')
})
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • to be fair sec - 1 doesn't really mutate state. Setting sec in one closure won't change its value in another. But it is cleaner to just write sec-1 – Joseph Nields Mar 16 '17 at 19:20
  • I'm not sure what you're saying. `sec - 1` does not mutate state; that is correct. But! *"setting sec in one closure"* – assuming you're referring to `sec--` – *does* actually mutate state. It's only irrelevant in this case because Numbers are pass-by-value in a function application. – Mulan Mar 16 '17 at 19:23
  • that's what I mean - it's just passing the value. So if you access `sec` somewhere after the recursive call, it will have the value originally passed to the recursive call, and not any value set by the recursive call – Joseph Nields Mar 16 '17 at 19:27
0

the first argument to setTimeout should be a function.

When you call setTimeout(countdown(sec),1000), it gets evaluated to setTimeout(undefined, 1000) because countdown(sec) does not return a value.

Your second method works, but as shorthand you could also do this: setTimout(coundown.bind(null, sec), 1000). This creates a function that will execute countdown(sec) when called, which is basically equivalent to the following:

setTimeout(
    function() {
        countdown(sec);
    }, 
    1000
);

More info here: Use of the JavaScript 'bind' method

Community
  • 1
  • 1
Joseph Nields
  • 5,527
  • 2
  • 32
  • 48