JavaScript定时器setTimeout与setInterval详解及运行机制
本文详细介绍JavaScript中setTimeout和setInterval的用法、参数传递、上下文绑定问题及运行机制。重点解析定时器在事件队列中的执行逻辑,并针对for循环中定时器输出错误的经典案例,提供立即执行函数和let关键字两种解决方案。
JavaScript语言内置了定时执行代码的功能,这被称为定时器(timer)。实现这一功能主要依赖两个核心函数:和。
一、setTimeout()的使用方法
函数用于指定某个函数或代码片段在指定的毫秒数之后执行。该函数会返回一个整数,代表定时器的唯一标识符(ID),后续可通过此ID来取消该定时器。
var timerId = setTimeout(func|code, delay)上述代码里,setTimeout方法接收两个参数,第一个参数func|code代表延后执行的函数名或代码片段,第二个参数delay表示延后的毫秒数。
console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);上述代码的执行输出结果为1、3、2。这是因为将第二行语句推迟了1000毫秒执行,导致控制台先打印1和3,最后才打印2。
需要注意的是,如果将代码以字符串形式传入,引擎内部会使用eval函数将其转换为可执行代码。这种做法存在安全隐患,且不利于JavaScript引擎进行代码优化。因此,建议直接传入函数名或匿名函数,例如:
function f(){
console.log(2);
}
setTimeout(f,1000);
// 或者
setTimeout(function (){console.log(2)},1000);除了前两个必需参数外,还支持传递更多参数。这些额外参数会在回调函数执行时,作为实参传递给该函数。
setTimeout(function(a,b){
console.log(a+b);
},1000,1,1);在上面的示例中,共有4个参数。最后两个参数(1和1)将在1000毫秒后回调函数执行时,作为参数传递给回调函数。
在IE 9.0及更低版本中,仅支持两个参数,无法直接传递额外参数。此时有三种解决方案:一是自定义,利用apply方法将参数传入回调函数;二是在匿名函数中调用带参数的回调函数,再将匿名函数传入;三是使用bind方法将多余参数绑定到回调函数上,生成新函数后传入。
除了参数传递问题,还有一个关键点:被推迟执行的回调函数是在全局环境中执行的,这可能与函数定义时的上下文环境不同。
var x = 1;
var o = {
x: 2,
y: function(){
console.log(this.x);
}
};
setTimeout(o.y,1000);
// 1上述代码的输出结果为1,而非2。这表明回调函数的运行环境已变为全局环境,导致this指向发生变化。
再看一个容易忽略错误的示例。
function User(login) {
this.login = login;
this.sayHi = function() {
console.log(this.login);
}
}
var user = new User('John');
setTimeout(user.sayHi, 1000);上述代码仅会显示undefined。因为当user.sayHi执行时,它是在全局对象中运行,导致this.login无法获取到预期的值。
为避免此类问题,一种解决方法是将user.sayHi包裹在函数中执行。
setTimeout(function() {
user.sayHi();
}, 1000);在上述代码中,user.sayHi是在函数作用域内执行,而非全局作用域,因此能够正确显示预期值。
二、setInterval()的使用方法
函数的用法与完全一致,区别仅在于指定某个任务每隔一段时间执行一次,即无限次地定时执行。
<input type="button" onclick="clearInterval(timer)" value="stop">
<script>
var i = 1
var timer = setInterval(function() {
console.log(2);
}, 1000);
</script>上述代码表示每隔1000毫秒输出一个2,直到用户点击停止按钮为止。
与类似,方法除了前两个参数外,还可以接受更多参数,这些参数会传入回调函数。示例如下:
function f(){
for (var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
}
setInterval(f, 1000, "Hello World");
// Hello World
// Hello World
// Hello World
// ...如果网页不在浏览器当前窗口(或标签页)中,许多浏览器会限制指定的反复运行任务最多每秒执行一次。
指定的是“开始执行”之间的间隔,因此实际上,两次执行之间的间隔会小于指定的时间。假设指定每100毫秒执行一次,每次执行耗时5毫秒,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,例如需要105毫秒,那么它结束后,下一次执行就会立即开始。
var i = 1;
var timer = setInterval(function() {
alert(i++);
}, 2000);上述代码每隔2000毫秒弹出一个alert对话框。如果用户一直不点击“确定”,整个浏览器将处于“堵塞”状态,后续执行无法触发,导致任务累积。例如,第一次弹出alert对话框后,用户过了6000毫秒才点击“确定”,那么第二次、第三次、第四次执行将累积起来,它们之间不再有等待间隔。
为确保两次执行之间有固定的间隔,可以不用,而是每次执行结束后,使用指定下一次执行的具体时间。上述代码用改写如下:
var i = 1;
var timer = setTimeout(function() {
alert(i++);
timer = setTimeout(arguments.callee, 2000);
}, 2000);上述代码可以确保两次执行的间隔固定为2000毫秒。
基于这种思路,可以自定义一个函数,实现间隔时间确定的效果。
function interval(func, wait){
var interv = function(w){
return function(){
setTimeout(interv, w);
func.call(null);
}
}(wait);
setTimeout(interv, wait);
}
interval(function(){
console.log(2);
},1000);上述代码部署了一个interval函数,通过循环调用来模拟的行为。
三、clearTimeout()与clearInterval()的使用方法
和函数均返回一个表示计数器编号的整数值。将该整数传入clearTimeout和clearInterval函数,即可取消对应的定时器。
var id1 = setTimeout(f,1000);
var id2 = setInterval(f,1000);
clearTimeout(id1);
clearInterval(id2);四、定时器的运行机制
和的运行机制是:将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这意味着,指定的代码,必须等到本次执行的所有代码都执行完,才会执行。
每一轮Event Loop时,都会将“任务队列”中需要执行的任务一次执行完。和都是把任务添加到“任务队列”的尾部。因此,它们实际上要等到当前脚本的所有同步任务执行完,然后再等到本次Event Loop的“任务队列”的所有任务执行完,才会开始执行。由于前面的任务到底需要多少时间执行完是不确定的,所以没有办法保证,和指定的任务,一定会按照预定时间执行。
在日常编码中,你会发现,给 和 设定延迟时间往往并不准,或者干脆 (function(){xxx},0) 也不是立马执行(特别是有耗时代码在前),这是因为 js 是单线程的,有一个事件队列机制, 和 的回调会到了延迟时间塞入事件队列中,排队执行。
:延时 delay 毫秒之后,啥也不管,直接将回调函数加入事件队列。 :延时 delay 毫秒之后,先看看事件队列中是否存在还没有执行的回调函数( 的回调函数),如果存在,就不要再往事件队列里加入回调函数了。
看下面示例:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}结果:1 秒之后,同时输出 5 个 5。
因为 for 循环会先执行完(同步优先于异步优先于回调),这时五个 的回调全部塞入了事件队列中,然后 1 秒后一起执行了。
五、经典案例:解决for循环中定时器输出错误问题
for (var i = 0; i < 5; i++) {
setTimeout(function (){
console.log(i);
},1000);
}结果:5 5 5 5 5
为什么不是 1 2 3 4 5,问题出在作用域上。
因为 的 console.log(i); 的i是 var 定义的,所以是函数级的作用域,不属于 for 循环体,属于 global。等到 for 循环结束,i 已经等于 5 了,这个时候再执行 的五个回调函数(参考上面对事件机制的阐述),里面的 console.log(i); 的 i 去向上找作用域,只能找到 global下 的 i,即 5。所以输出都是 5。
解决办法:人为给 console.log(i); 创造作用域,保存i的值。
解决办法一:使用立即执行函数
for (var i = 0; i < 5; i++) {
(function(i){ //立刻执行函数
setTimeout(function (){
console.log(i);
},1000);
})(i);
}这里用到立刻执行函数。这样 console.log(i); 中的i就保存在每一次循环生成的立刻执行函数中的作用域里了。
解决办法二:使用let关键字
for (let i = 0; i < 5; i++) { //let 代替 var
setTimeout(function (){
console.log(i);
},1000);
}let 为代码块的作用域,所以每一次 for 循环,console.log(i); 都引用到 for 代码块作用域下的i,因为这样被引用,所以 for 循环结束后,这些作用域在 未执行前都不会被释放。
以上所述是小编给大家介绍的JavaScript中定时器()和()的用法,希望对大家有所帮助。在此也非常感谢大家对本站网站的支持!
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
JavaScript函数定义的三种方式:声明、表达式与箭头函数
2026-11-05 13:50:55
JavaScript中escape、encodeURI和encodeURIComponent对空格的处理差异
2026-11-05 12:47:23
