今天来看看一个最最最基础的 for
语句吧!
有什么好说的呢?请往下看吧...
一、语法
for
语句用于创建一个循环,它包含了三个可选的表达式(包围在圆括号之中,使用分号 ;
分隔),后跟一个用于在循环中执行的语句(通常为块语句)。
for ([initialization]; [condition]; [final-expression])
statement
initialization
一个表达式(包含赋值语句)或变量声明。可使用var
或let
声明变量(但不能使用const
关键字声明)。但两者声明的变量作用域不同,前者var
与for
循环处于同样的作用域中,而后者let
则是语句的局部变量。该表达式的结果无意义。condition
一个表达式被用于确定每一次循环是否能被执行。如果表达式结果为true
,statement
将被执行。该表达式是可选的,如果被忽略,那么被认为永远为证。如果表达式结果为false
,那么执行流程将被跳到for
语句结构后面的第一条语句。final-expression
每次循环的最后都要执行的表达式。执行时机是在下一次condition
的计算之前。通常被用于更新或递增计数器变量。statement
只要condition
的结果为true
,就会被执行的语句。要在循环体内执行多条语句,使用一个块语句来包括要执行的语句。没有任何语句要执行,使用一个空语句(;
)
类似下面的循环语句,再熟悉再常见不过了。
for (var i = 0; i < 10; i++) {
// statements
}
二、示例
由于 for
语句头部圆括号中的所有三个表达式都是可选的,因此下面列举一些相对没那么“常见”的示例。
例如,省略 initialization
初始化块中的表达式:
var i = 0
for (; i < 10; i++) {
console.log(i)
// more statements
}
例如,省略 condition
表达式,但必须在 statement
循环体内跳出循环,避免死循环。
for (var i = 0; ; i++) {
console.log(i)
if (i > 3) break
// more statements
}
甚至,你可以忽略所有的表达式。同样的,要确保使用 break
语句来跳出循环,并且还要修改(增加)一个变量,使得 break
语句的条件在某个时候为真。
var i = 0
for (; ;) {
if (i > 3) break
console.log(i)
i++
}
但有一个需要特别注意的是,当循环体 statement
不执行任何语句时,必须使用一个空语句(即 ;
,且分号是不能省略的)。
for (var i = 0; i < 10; i++)/* empty statement */;
例如:
for (var i = 0; i < 10; i++)
console.log('loop') // 这个被当作 for 语句的 statement 循环体
console.log('no loop') // 但这个不属于循环体哦
三、BTW
关于能否省略分号的问题,顺便一下。
- 在 if...else 语句中,根据 ASI 机制,由于
)
和else
无法构成合法的语句,且 JavaScript 解析器不会在else
之前自动插入分号;
,导致词法分析阶段就出错了。
// SyntaxError: Unexpected token 'else'
if (true) else console.log('...')
// Correct, but this "if" does nothing!
if (true); else console.log('...')
正确做法是,需主动在 else
之前键入分号 ;
(不可省略),表示 if
条件为真时,执行了一个空语句。
- 在 do...while 语句中,以下形式是允许的哦,语法不会出错。
do statement while(condition) out-of-loop-statement
// 根据 ASI 机制,JavaScript 解析器看到的其实是长这样的:
do statement; while(condition); out-of-loop-statement;
- 在 for 语句中,即使把
initialization
、condition
、final-expression
表达式都省略了,但是分号;
却一个都不能省略。当循环体不执行任何语句时,分号;
也不能省略。
// Correct
for (; ;); // 当然这样是没意义,而且会陷入死循环。但语法是正确的,为了举例罢了。
// SyntaxError
for (;);
// Bad, 容易产生非预期结果
for (;;)
四、被忽略的地方
-
for
语句的initialization
、condition
、final-expression
除了表达式形式,还可以是函数形式。若initialization
、final-expression
以函数形式存在,它们的返回值是无实际意义的。但是condition
则必须返回一个布尔值。
for (var i = 0; i < 10; i++) {
console.log(i)
}
// 相当于
function compare(number) {
// 若 condition 是一个较为复杂的表达式,使用函数形式或许可以使得代码更清晰。
return number < 10
}
for (var i = 0; compare(i); i++) {
console.log(i)
}
五、经典面试题
在 initialization
表达式中使用 var
和 let
声明变量是有区别的。
上面提到使用 var
声明的变量,其作用域与 for
循环处于同样的作用域中,而使用 let
则是 for
循环内部的块级作用域。
看看以下两个示例的异同,不同点在于 var
和 let
:
// 示例一
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
arr[6]() // 10
// 示例二
var arr = []
for (let i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
arr[6]() // 6
上面的示例中,为什么结果会有差异呢?我们来分析一下:
/* 全局作用域 */
for (let i = 0 /* 块级作用域 1 */; i < 10; i++) {
/* 块级作用域 2 */
let i = 'abc'
console.log(i) // 结果是打印了 10 遍 "abc",而不是 0 ~ 10 哦
}
// 理由:
// 假设 1 和 2 是同级作用域下,我们重复使用 let 关键字来声明 i 变量,
// 理应抛出语法错误,如:SyntaxError: Identifier 'i' has already been declared
// 但事实上运行是没问题的,说明通过了词法分析。
// 再者,假设我们在循环体内声明 let j = 'temp',然后在 final-expression 表达式内是无法访问变量 j 的。
// 综上,可知它俩作用域是不一样的。
// 基于以上反证结论,我们有理由认为:
// 若在 for 语句的圆括号和循环体内使用了 let 来声明变量,它们所处的作用域是不一样的。
// 而在 for 循环的圆括号的三个表达式,其作用域是同一个。
// 注意,只能在循环体内部访问圆括号内的变量,反之不行。
我们打个断点看下就清楚了:
可以断点调试看下:
而示例一断点调试可知,由始至终就一个全局作用域。(不贴图了)
关于示例二,相当于以下这样,那作用域就更加地清晰了:
var arr = []
{
let i
for (i = 0; i < 10; i++) {
let _i = i
arr[_i] = function () {
console.log(_i)
}
}
}
arr[6]() // 6
我们再看下,Babel 是如何转换的:
看到这个,那不就想起那个经典面试题:如何修改代码使其打印出 0 ~ 9 吗?
六、async 在 for 语句中的应用
例如,实现休眠效果。
function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
async function traverse() {
for (let i = 1; i <= 5; i++) {
console.log(i)
await sleep(1000)
}
}
traverse() // 间隔 1 秒,依次输出 1 ~ 5
例如,在网络请求中,可以实现多次重复尝试。
async function request(url) {
let res
let err
const MAX_NUM_RETRIES = 3
for (let i = 0; i < MAX_NUM_RETRIES; i++) {
try {
res = await fetch(url).then(res => res.json())
break
} catch (e) {
err = e
// Do nothing and make it continue.
}
}
if (res) return res
throw err
}
request('http://192.168.1.102:7701/config')
.then(res => {
console.log('success')
})
.catch(err => {
console.log('fail')
})
未完待续...
七、参考
- The for Statement(ECMAScript)
- JavaScript ASI 机制详解
- let 和 const 命令