前言
for循环是一个语句(statement),语句的意思就是语法层面上的流程控制,基本的语法为:
js
/**
* 三个可选表达式
* for(
* initialization 计步变量初始化;
* condition 循环程序继续执行的条件;
* afterthought 循环程序最后要执行的任务;
* )
* 循环体 loop body
* {}
*/
for (var i = 0; i < 5; i++) {
console.log(i) // 0,1,2,3,4
}
console.log(i) // 5
结果打印出来的是0,1,2,3,4,下面来解释一下for循环执行的过程。
- 首先声明变量并进行初始化, var i=0;
- 判断条件是否成立,0<5,成立;
- 如果条件成立执行循环体中的语句,console.log(i);
- 一次循环结束后会在循环体内部完成afterthought的任务,即i++;
- 继续判断条件是否成立,1<5,若成立则继续执行循环体,若不成立则结束循环程序;
- 最终循环外的i值将为5;
下面详细说下这三个表达式。
初始化计步变量
也就是说var i = 0
是一个声明变量并赋值的过程即初始化变量,那么它跟for循环的执行顺序是什么呢?其实是先进行初始化计步器,相当于:
js
var i = 0;
for (; i < 5; i++) {// 注意分号不能省略!
console.log(i)
}
除了使用var
进行变量声明外我们还经常使用let
进行变量的声明,区别就在于:
var
声明的计步变量是与for循环所在的作用域保持一致let
声明的变量会在for循环的循环体作用域中进行声明
js
// for循环在去全局作用域下var声明的变量也在全局作用域中,在函数内使用for循环则属于局部变量
for (var i = 0; i < 5; i++) {
console.log(i)
}
function test() { ==> function test() {
for (var i = 0; i < 5; i++) { var i = 0;
console.log(i) for (; i < 5; i++) {
} console.log(i)
} }
}
// let声明
// 此时在for循环外部访问变量i就会报错,因为i会被声明在循环体内部的作用域
for (let i = 0; i < 5; i++) {
console.log(i)
}
console.log(i) // i is not defined
// 如果把初始化计步变量提出来的话就会跟使用var声明变量相同
let i = 0;
for (; i < 5; i++) {
console.log(i) // 0 1 2 3 4
}
console.log(i) // 5
循环条件
condition是在每次循环迭代之前进行计算并且结果一定是一个布尔值,true即执行循环体,false则结束循环。
如果不写循环条件,也就是说是一个empty expression空表达式,则JS引擎会解析条件为true。
js
for (let i = 0; ; i++) {
console.log(i) // 代码将进入死循环
}
// 等同于
for (let i = 0; true; i++) {
console.log(i) // 代码将进入死循环
}
并且for循环是同步执行的,循环结束之后也就是condition为false才会执行for循环之后的代码。
js
for (let i = 0; i < 3; i++) {
console.log(i)
}
console.log("循环结束")
// 0
// 1
// 2
// 循环结束
迭代程序与块语句
afterthought是在每次循环迭代的最后进行计算,下次计算condition之前进行的,既然知道了它的执行时机那么我们可以写出下面的代码:
js
// 省略循环体,仅限于简单的逻辑并且不太好维护
for (var i = 0; i < 3; console.log(i), i++);
// 0
// 1
// 2
循环的技巧与特性
for循环中使用关键字
js
let obj = {}
obj.count = 3
for (var i = "count" in obj ? obj.count : 1; i < 5; i++) {
console.log(i)
}
上述代码意在使用obj对象中的count作为初始化变量的值,这是会有提示有报错信息,是因为for in本身就是一个关键字所以不可以直接这样写,可以使用括号提高其计算的优先级:
js
let obj = {}
obj.count = 3
for (var i = ("count" in obj ? obj.count : 1); i < 5; i++) {
console.log(i)
}
或者
let obj = {}
obj.count = 3
for (var i = ("count" in obj) ? obj.count : 1; i < 5; i++) {
console.log(i)
}
for循环中不同的表现形式
js
var i = 0;
// 可以直接将表达式写在循环体中
for (; ;) {
if (i >= 5) {
break; // 手动结束循环
}
console.log(i)
i++;
}
那接着有的小伙伴就想到了以下的写法:
js
var i = 0;
// 可以直接用while循环进行替代
while (true) {
if (i >= 5) {
break;
}
console.log(i)
i++;
}
循环中的词法声明
再来详细了解一下初始化计数变量时var
和let
的区别。
var 声明
先来看一个例子:
js
var arr = [];
for (var i = 0; i < 3; i++) {
const test = () => {
console.log(i)
}
arr.push(test)
}
arr.forEach(fn => fn())
// 3 3 3
为什么最终结果会是三个3呢?其实这里涉及到了闭包的知识,fn
函数内部访问了外部的作用域,想了解闭包更详细的知识可以看这篇文章
闭包是一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合,闭包可以让开发者从内部函数访问外部函数的作用域,在JavaScript中,闭包会随着函数的创建而被同时创建。
在循环执行的过程中test
函数其实是没有被执行的,当被收集到arr
数组中并被执行时候i
的引用发生了变化,前面有提到过使用var
声明的变量是跟for循环所在的作用域保持一致的,所以当执行函数fn
时打印出的变量i
其实是全局作用域下的变量i
,而此时i
的值已经变为了3,也就是说如果在每一次循环中调用fn
函数确实可以打印出0,1,2,但最后由于是访问的全局作用域也就是for循环完成之后的结果才导致打印出3,3,3,这里我希望我说明白了,个人感觉还是比较重要且经典的尤其是在面试中。
如果说上面的例子没有勾起你的回忆,那就看下面的例子:
js
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
}, 100)
}
// 3 3 3
道理是一样的,都是由于最后主线程执行完毕后访问全局作用域下的变量导致结果为333的。
let 声明
再来深入了解一下let
的使用,let
是在loop body中进行的,let
创建的是本地作用域即for循环中的循环体,可以简单的理解为:
js
for(){
let i = 0;
}
- 在initialization的过程中let会给for循环体创建一个新的词法作用域,每次循环之前都会创建
- 会把上一次的i的值赋值给新的词法作用域中的 i
- afterthought在新的词法作用域中进行计算
参考以下的例子理解这段话:
js
for (let i = 0; i < 3; i++) {
console.log(i)
}
// 背后的原理
let lastStep = 0; // js引擎会提供一个用来记录的变量
{
let i = lastStep;
console.log(i);
lastStep++;
}
{
let i = lastStep;
console.log(i);
lastStep++;
}
{
let i = lastStep;
console.log(i);
lastStep++;
}
目的其实就是为了隔离作用域,但这里其实不是在每一个循环体中都重新初始化initialization中i的值,我们可以做一下尝试:
js
for (let i = 0, output = () => { console.log('output', i) }; i < 3; i++) {
console.log(i)
output();
}
// 0
// output 0
// 1
// output 0
// 2
// output 0
for (let i = 0, output = () => { console.log('output', i) }; i < 3; i++, output = () => { console.log('output', i) }) {
console.log(i)
output();
}
// 0
// output 0
// 1
// output 1
// 2
// output 2
可以看到第一个例子中的output函数输出都为0,第二个是输出012,也就证明了initialization只会执行一次,js引擎在看到let i = 0
时会记录该值,然后在执行循环体程序之前创建一个新的词法作用域并声明一个新的变量i,然后将上一次记录的值赋值给的新的变量i
js
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
}, 100)
}
// 0 1 2
那么这道面试题换成let
声明道理也是一样的啦!