前言:setTimeout是JavaScript中常见的一个window对象方法,本文将介绍关于它的一些基础知识和易出错的地方。浏览器
做用:setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。 安全
基本语法:多线程
let timeoutId = window.setTimeout(func[, delay, param1, params2, ...]); let timeoutId = scope.setTimeout(code[, delay]); let timeoutId = window.setTimeout(function, milliseconds);
timeoutID 是该延时操做的数字ID, 此ID随后能够用来做为window.clearTimeout方法的参数。异步
func是你想要在delay毫秒以后执行的函数。函数
code 在第二种语法,是指你想要在delay毫秒以后执行的代码字符串,(使用该语法是不推荐的,
不推荐的缘由和eval()同样,即:一、安全性差(可被植入恶意代码)二、执行效率低(须要将字符串解析为代码再执行))oop
delay 是延迟的毫秒数(1秒=1000毫秒),函数的调用会在该延迟以后发生。若是省略该参数,delay取默认值0。this
须要注意的是,IE9 及更早的 IE 浏览器不支持第一种语法中向延迟函数传递额外参数的功能。插件
//如下是一个简单实例,3秒后弹窗提示 function myFunction(){ setTimeout(function(){alert("Hello")},3000); }
先来看一些程序线程
//请判断如下代码输出结果 setTimeout(function(){ alert("Hello World"); },1000); while(true){}; //该函数会陷入死循环,1秒后并不会弹出提醒
//请写出如下代码输出结果 setTimeout(function (){ console.log('a') },0) console.log('b') //输出结果为b,a
有同窗可能会认为:第一段代码在1秒后会弹窗提示Hello World。而第二段代码,把延迟毫秒数设为0,就会当即执行,先输出a,再输出b。显然,实际的结果不是这样。 code
为何呢?由于:
JavaScript引擎是单线程运行的,浏览器不管在何时都只且只有一个线程在运行JavaScript程序。
咱们先来介绍下浏览器渲染时的线程机制:
浏览器的内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。
JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JavaScript线程在运行JavaScript程序。
GUI渲染线程负责渲染浏览器界面,当界面须要重绘(Repaint)或因为某种操做引起回流(Reflow)时,该线程就会执行。但须要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时当即被执行。
事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其余线程如鼠标点击、Ajax异步请求等,但因为JavaScript的单线程关系全部这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)。
JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JavaScript线程在运行JavaScript程序。
回归开始的问题:
第一段代码始终会执行同步代码,陷入死循环,根本不会执行setTimeout内的函数,也就不会弹窗。
第二段代码,通过查找资料,能够得知,setTimeout有一个最小执行时间,当指定的时间小于该时间时,浏览器会用最小容许的时间做为setTimeout的时间间隔,也就是说——即便咱们把setTimeout的毫秒数设置为0,被调用的程序也没有立刻启动,它仍然会放在事件队列的最后。当同步代码执行完,输出b,而后再执行延迟函数,输出a。
this是JavaScript中一个重要的考察点,能够简单记为:this是指向函数执行时的当前对象,假若没有明确的当前对象,它就是指向window的。
请看以下代码
//求函数执行结果 var a=1; var obj={ a:2, b:function(){ setTimeout(function(){ console.log(this.a); },2000); } }; obj.b(); //函数输出为1
由setTimeout()调用的代码运行在与所在函数彻底分离的执行环境上。这会致使,这些代码中包含的 this 关键字会指向 window (或全局)对象,这和所指望的this的值是不同的。也就是当执行时,this.a并不能读取obj对象中的a,而是会找到全局对象的a,故输出结果为1。
为了改变这种状况,有如下两种方法
//方法一 var a=1; var obj={ var me = this; a:2, b:function(){ setTimeout(function(){ console.log(me.a); },2000); } }; obj.b(); //方法二 var a=1; var obj={ a:2, b:function(){ setTimeout(function(){ console.log(this.a); }.bind(this),2000); } }; obj.b();
(function() { console.log(1); setTimeout(function(){console.log(2)}, 1000); setTimeout(function(){console.log(3)}, 0); console.log(4); })(); //输出结果是1,4,3,2
解释:参照第二部分的事件机制
将 JavaScript 代码包含在一个函数块中有什么用途?为何要这么作?
换句话说,为何要用当即执行函数表达式(Immediately-Invoked Function Expression)。
IIFE 有两个比较经典的使用场景,一是相似于在循环中定时输出数据项,二是相似于 JQuery/Node 的插件和模块开发。
for(var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); } //输出为5,5,5,5,5
使用IIFE
for(var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); }, 1000); })(i) } //输出为0,1,2,3,4
若是 list 很大,下面的这段递归代码会形成堆栈溢出。若是在不改变递归模式的前提下修善这段代码?
var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // process the list item... nextListItem(); } };
解决方案:加入定时器
var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // process the list item... setTimeout(nextListItem(), 0); } };
setTimeout(function () { console.log('three'); }, 0); Promise.resolve().then(function () { console.log('two'); }); console.log('one'); // one // two // three
解释:当即resolve的Promise对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是当即执行,所以最早输出。
function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100);
上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的做用域(即Timer函数),后者的this指向运行时所在的做用域(即全局对象)。因此,3100毫秒以后,timer.s1被更新了3次,而timer.s2一次都没更新。