0

Background (You might want to skip this)

I'm working on a web app that animates the articulation of English phonemes, while playing the sound. It's based on the Interactive Sagittal Section by Daniel Currie Hall, and a first attempt can be found here.

For the next version, I want each phoneme to have it's own animation timings, which are defined in an array, which in turn, is included in an object variable.

For the sake of simplicity for this post, I have moved the timing array variable from the object into the function.

Problem

I set up a for loop that I thought would reference the index i and array t to set the milliseconds for each setTimeout.

function animateSam() {

  var t = [0, 1000, 2000, 3000, 4000];
  var key = "key_0";

  for (var i = 0; i < t.length; i++) {
    setTimeout(function() {
      console.log(i);
      key = "key_" + i.toString();
      console.log(key);

      //do stuff here

    }, t[i]);
  }
}

animateSam()

However, it seems the milliseconds are set by whatever i happens to be when the function gets to the top of the stack.

Question: Is there a reliable way to set the milliseconds from the array?

Thailandian
  • 577
  • 5
  • 14
  • 3
    Use `let` instead of `var`, or, even better, use array methods instead of `for` loops. – CertainPerformance Jun 12 '18 at 10:22
  • Did you try to move `i` into another variable inside `for loop body` or more like copy the milliseconds into another variable? Like `var x = t[i]` – r3dst0rm Jun 12 '18 at 10:22
  • In addition to `let` instead of `var`, why don't you use `i * 1000` instead of the array approach? Seems counterintuitive to hardcode what is easy math.. – baao Jun 12 '18 at 10:23
  • why not just set `setTimeout(function() {}, 1000)`? – Ivan Jun 12 '18 at 10:24
  • What exactly are you asking? The timeouts will be exactly as specified in the array - one second apart. Are you asking why each `alert` shows you `5`? – Amadan Jun 12 '18 at 10:24
  • 1
    @ivan, baao: this is just an example - phonemes do not have one-second-apart timings, his real use case is not that array. – Amadan Jun 12 '18 at 10:26
  • Likely duplicate of [JavaScript closure inside loops – simple practical example](https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Amadan Jun 12 '18 at 10:27
  • The `i` value when the function executes will be the i value at the time the function executes. However the `t[i]` when you set the timeout should be correct. – apokryfos Jun 12 '18 at 10:33
  • PS I know it's test code but I hope you know you should never do `key = ...` without assigning it to a var/let/const – Dominic Jun 12 '18 at 10:34
  • Sorry all - I should have added that each phoneme will have different array lengths as well as different array values, which is why I could not nest the setTimeout functions. – Thailandian Jun 12 '18 at 10:59
  • 1
    @Dominic Tobias I had assigned "var key;" outside of the for loop, but forgot to copy it over to my simplified example. Thanks for the heads up, and I've modified the question. – Thailandian Jun 12 '18 at 11:30

3 Answers3

3

The for ends before the setTimeout function has finished, so you have to set the timeout inside a closure:

function animateSam(phoneme) {

  var t = [0,1000,2000,3000,4000];

  for (var i = 0; i < t.length; i++) {
    (function(index) {
        setTimeout(function() {
            alert (index);
            key = "key_" + index.toString();
            alert (key);

            //do stuff here

        }, t[index]);
    })(i);
  }
}

Here you have the explanation of why is this happening: https://hackernoon.com/how-to-use-javascript-closures-with-confidence-85cd1f841a6b

A. Llorente
  • 1,142
  • 6
  • 16
3

The for loop will loop all elements before the first setTimeout is triggered because of its asynchronous nature. By the time your loop runs, i will be equal to 5. Therefore, you get the same output five times.

You could use a method from the Array class, for example .forEach:

This ensures that the function is enclosed.

[0, 1000, 2000, 3000, 4000].forEach((t, i) => {
  setTimeout(function() {

    console.log(i);
    console.log(`key_${i}`);

    //do stuff here

  }, t)
});

Side note: I would advise you not to use alert while working/debugging as it is honestly quite confusing and annoying to work with. Best is to use a simple console.log.


Some more clarifications on the code:

.forEach takes in as primary argument the callback function to run on each of element. This callback can itself take two arguments (in our previous code t was the current element's value and i the current element's index in the array):

Array.forEach(function(value, index) {

});

But you can use the arrow function syntax, instead of defining the callback with function(e,i) { ... } you define it with: (e,i) => { ... }. That's all! Then the code will look like:

Array.forEach((value,index) => {

});

This syntax is a shorter way of defining your callback. There are some differences though.

Ivan
  • 34,531
  • 8
  • 55
  • 100
  • Thanks @Ivan Your code is actually simpler, but I don't quite understand the syntax (it's different to other examples of forEach I have seen), so I went with A. Llorente's answer as it will be easier to apply to my actual code. – Thailandian Jun 12 '18 at 11:05
  • You're right about console.log - alerts were helping to confuse me! – Thailandian Jun 12 '18 at 11:06
  • @Thailandian, it's ok no problem. I made an edit to explain the syntax. I hope it will help you understand better – Ivan Jun 12 '18 at 11:31
  • Thanks @Ivan. I've already finished modifying my code but it's great that it's there as a reference for others that stumble in here. – Thailandian Jun 12 '18 at 14:34
1

I would suggest using a function closure as follows:

function animateSam(phoneme) {

  var t = [0,1000,2000,3000,4000];

  var handleAnimation = function (idx) {
    return function() {
      alert(idx);
      key = "key_" + idx.toString();
      alert(key);
      //do stuff here
    };
  }
  for (var i = 0; i < t.length; i++) {
    setTimeout(handleAnimation(i), t[i]);
  }
}

I this example you wrap the actual function in a wrapper function which captures the variable and passes on the value.

Exelian
  • 5,749
  • 1
  • 30
  • 49