在一些文章中或者工作面试问题上,会遇见这种看似简单的经典问题。
公司主营业务:成都网站建设、成都网站制作、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。成都创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。成都创新互联推出莘县免费做网站回馈大家。
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log('hello word');
/*output
'hello word'
5
5
5
5
5
*/
对于老鸟来说这种问题不足挂齿,但是如果你是新手正在学习 js 的路上如火如荼或是刚好遇到了此类问题一知半解,那么这篇文章将给你带来原理和解答。 小小问题背后别有洞天。
JS 是典型的单线程语言,所谓单线程就是只能同时执行一个任务。
之所以是单线程而不是多线程,是为了避免多线程对同一 DOM 对象操作的冲突。比如 A 线程创造一
操作系统的进程和线程:
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
一个进程至少有一个线程,复杂的进程有多个线程。操作系统通过多核cpu快速交替执行这些线程就给人一种同时执行的感觉。
单线程就意味着,所有任务需要排队,前一个任务结束,后一个任务才会执行。前面的任务耗时过长,后面的任务也得硬着头皮等待。而任务执行慢通常不是 CPU 性能不行,而是 I/O 设备操作耗时长,比如Ajax操作从网络获取数据。
JS 设计者意识到,遇到这种情况主线程可以完全不管 I/O 设备的结果,先挂起 I/O 耗时的任务,然后执行排在后面的任务。直到 I/O 设备返回了结果,并发来了通知,再回过来执行先前挂起的任务。
所以,设计者把浏览器的程序任务可以分为两种,同步任务和异步任务:
例子中的代码运行机制看这里:
一文说清 JS 运行时环境(Event Loop)
回过头来看文章开头那段代码
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log('hello word');
setTimeout()
方法中作为 Timers 属于异步任务,每次循环就会被分发到 WEB API's 的容器中,并且作为参数的匿名函数也会被存储到内存堆中,也就是说这种操作 JS 运行时 会重复 5 次。console.log('hello word')
在循环完成后被推入执行栈执行,打印字符串。i
,重复 5 次结束。所以实质上可以看作(取巧方便理解,非实质):
// 同步执行
var i;
for(i = 0; i < 5; i++) {
}
// 同步执行
console.log('hello word');
// 异步执行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);
作用域简单的说就是 JS 函数当前执行的上下文语境。函数在这个上下文语境中才能访问和引用这个语境中的其他变量。子作用域可以访问和引用父作用域中的变量,反之不行。
一个函数对象在JS中被创建的时候同时创建了闭包,闭包是由该函数对象和它所在的语境而构成的一个组合。通常返回一个函数的引用。
// 一个典型的闭包
function makeFunc() {
var text = "hello world";
function displayName() {
console.log(text);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();// hello world
我们可以利用闭包的原理让定时器打印出 0, 1, 2, 3, 4。
for(var i = 0; i < 5; i++) {
((i) => {
setTimeout(function () {
console.log(i);
});
})(i);
}
console.log('hello word');
在上面的代码中,使用了一个技巧 立即函数 给计时器单独提供了一个新的作用域(上下文语境),加上里面的计时器就刚好组成了一个异步的闭包组合,而且是立刻调用的。
通过上面的手段就可以很好的避免var
声明的循环变量暴露在全局作用域带来的问题。从而打印出 0, 1, 2, 3, 4。
通过let
声明循环变量也是很好的解决手段,let
允许你声明一个被限制在块作用域中的变量,这个就是块级作用域。
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log('hello word');
let
是ES6语法,而块级作用域的出现解决了var
循环变量泄露为全局变量的问题和变量覆盖的问题。
对于不能兼容ES6的浏览器,我们也可以使用ES5try...catch
语句,形成类似闭包的效果。
for(var i = 0; i < 5; i++) {
try {
throw(i)
} catch(j) {
setTimeout(function () {
console.log(j);
});
}
}
console.log('hello word');
回到上面的代码,着重说下let
是如何做到每次循环变量i
能够保存当前的上下文语境,并传值传给下次循环的:
let
关键字声明的变量i
至始至终都是属于for
循环块级作用域内的局部变量。for
循环每迭代一次,局部变量i
就将当前的状态单独保存在内存中。for
循环的块级作用域对应的变量对象 => 全局变量对象,所以匿名函数和for
循环的块级作用域(上下文语境)形成了闭包这样的关系。i
值都是局部变量i
单独保存在内存中值。