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?

11
  • 3
    Use let instead of var, or, even better, use array methods instead of for loops. Commented Jun 12, 2018 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] Commented Jun 12, 2018 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.. Commented Jun 12, 2018 at 10:23
  • 1
    @ivan, baao: this is just an example - phonemes do not have one-second-apart timings, his real use case is not that array. Commented Jun 12, 2018 at 10:26
  • 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. Commented Jun 12, 2018 at 11:30

3 Answers 3

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

Sign up to request clarification or add additional context in comments.

2 Comments

By the way, this is a classic job interview question
Thanks @A. Llorente - especially for the link to the explanation. I just hate not understanding things even if a problem is solved on the surface :-)
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.

4 Comments

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.
You're right about console.log - alerts were helping to confuse me!
@Thailandian, it's ok no problem. I made an edit to explain the syntax. I hope it will help you understand better
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.
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.