Solution
The issue arises when the loop finishes executing before the timeouts callbacks have begun, resulting in all callbacks having the same value of i
as len - 1
.
Using ES6
For those working with ES6
, a simple switch from var
to let
can resolve this problem:
for(let i = 0; i < len; i++) {
setTimeout(function() {
text[i].classList.remove("copied");
}, 1000);
}
Utilizing ES5
If your project is restricted to ES5
, you will need to create a new scope to correctly handle the timeouts like so:
Implementing a New Scope with a Try-Catch Block
for(var i = 0; i < len; i++) {
try { throw i; } catch(i) {
setTimeout(function() {
text[i].classList.remove("copied");
}, 1000);
}
}
Creating a New Scope with an IIFE
for(var i = 0; i < len; i++) {
(function(i) {
setTimeout(function() {
text[i].classList.remove("copied");
}, 1000);
})();
}
Establishing a New Scope via Function Call
for(var i = 0; i < len; i++) {
loopCallBack(i);
}
function loopCallBack(i) {
setTimeout(function() {
text[i].classList.remove("copied");
}, 1000);
}
Full Example using ES5 Syntax
function copyLink() {
var text = document.getElementsByClassName("text"),
len = text.length;
for (var i = 0; i < len; i++) {
(function(i) {
text[i].classList.add("copied");
setTimeout(function() {
text[i].classList.remove("copied");
}, 1000);
})(i);
}
}
.copied {
color: red;
}
<p class="link text">Text</p>
<p class="demo text">Some text</p>
<a href="javascript:copyLink()">Click me</a>
An Alternative Approach
Rather than introducing a fresh scope for each iteration, consider placing the for loop inside the setTimeout
callback function itself:
function copyLink() {
var text = document.getElementsByClassName("text"),
len = text.length;
for (var i = 0; i < len; i++) {
text[i].classList.add("copied");
}
setTimeout(function() {
for (var i = 0; i < len; i++) {
text[i].classList.remove("copied");
}
}, 1000);
}
.copied {
color: red;
}
<p class="link text">Text</p>
<p class="demo text">Some text</p>
<a href="javascript:copyLink()">Click me</a>