JavaScript | for 语句详解

简介: JavaScript | for 语句详解

前言


今天来看看一个最最最基础的 for 语句吧!


有什么好说的呢?请往下看吧...


正文


一、语法


for 语句用于创建一个循环,它包含了三个可选的表达式(包围在圆括号之中,使用分号 ; 分隔),后跟一个用于在循环中执行的语句(通常为块语句)。

for ([initialization]; [condition]; [final-expression])
  statement


  • initialization
    一个表达式(包含赋值语句)或变量声明。可使用 varlet 声明变量(但不能使用 const 关键字声明)。但两者声明的变量作用域不同,前者 varfor 循环处于同样的作用域中,而后者 let 则是语句的局部变量。该表达式的结果无意义。
  • condition
    一个表达式被用于确定每一次循环是否能被执行。如果表达式结果为 truestatement 将被执行。该表达式是可选的,如果被忽略,那么被认为永远为证。如果表达式结果为 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 */;

注意,这里的分号 ; 是强制性的,是 JavaScript 中的少数几种强制分号的情况。若没有分号,循环声明之后的行将被视为循环语句。


例如:

for (var i = 0; i < 10; i++)
  console.log('loop') // 这个被当作 for 语句的 statement 循环体
console.log('no loop') // 但这个不属于循环体哦


三、BTW


关于能否省略分号的问题,顺便一下。

  1. 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 条件为真时,执行了一个空语句。

  1. do...while 语句中,以下形式是允许的哦,语法不会出错。

do statement while(condition) out-of-loop-statement
// 根据 ASI 机制,JavaScript 解析器看到的其实是长这样的:
do statement; while(condition); out-of-loop-statement;


  1. for 语句中,即使把 initializationconditionfinal-expression 表达式都省略了,但是分号 ; 却一个都不能省略。当循环体不执行任何语句时,分号 ; 也不能省略。

// Correct
for (; ;); // 当然这样是没意义,而且会陷入死循环。但语法是正确的,为了举例罢了。
// SyntaxError
for (;);
// Bad, 容易产生非预期结果
for (;;)


四、被忽略的地方


  1. for 语句的 initializationconditionfinal-expression 除了表达式形式,还可以是函数形式。若 initializationfinal-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 表达式中使用 varlet 声明变量是有区别的。

上面提到使用 var 声明的变量,其作用域与 for 循环处于同样的作用域中,而使用 let 则是 for 循环内部的块级作用域。

看看以下两个示例的异同,不同点在于 varlet


// 示例一
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


上面的示例中,为什么结果会有差异呢?我们来分析一下:


示例一分析:

在 ES6 之前,JavaScript 的变量是没有块级作用域的,只有函数作用域和全局作用域。所以,在示例一中,变量 i 与 变量 arr 同属全局作用域。当循环执行完毕,i 已经增加到 10。接着执行 arr[6](),由于匿名函数 function () { console.log(i) } 内部并没有声明 i 变量,于是从上一级作用域(这里的上一级作用域是全局作用域)中查找 i,并找到其值为 10,因此打印结果为 10

注意,在示例一中,由始至终只有一个全局作用域。

示例二分析:

在示例二中,使用了 let 来声明变量 i,此时 i 不再与 arr 同属全局作用域了。

此时,其实存在三个作用域:包括 arr 所在的全局作用域、i 所在的块级作用域、以及 for 语句循环体内的块级作用域。为什么有三个不同的作用域?看示例:

/* 全局作用域 */
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 循环的圆括号的三个表达式,其作用域是同一个。
// 注意,只能在循环体内部访问圆括号内的变量,反之不行。

我们打个断点看下就清楚了:


14.webp.jpg


接着分析示例二,当我们使用 let 时,每次循环 JavaScript 引擎会重新创建环境,大致是拷贝上一次的变量及其值到本次循环中(详情看标准:CreatePerIterationEnvironment),这也是为什么重新创建环境,i 还能取到上一次值的原因。需要注意的是,initialization 表达式仅在首次执行 for 循环的时候进行初始化,下一次创建环境的时候并不会执行它,因此 i 不会重置为 0

而每次循环,循环体 statement 内都会产生一个块级作用域,对应作用域内的 i 值就是本次循环的 i 的值。而且,由于我们循环体内含有 arr[i] = function () { console.log(i) },会形成闭包。

所以,最后执行 arr[6]() 时,先从块级作用域下查找,并查找到为 6,因此它不会继续往上一级块级作用域下查找,自然也不会再往全局作用域下查找了。


可以断点调试看下:


15.webp.jpg


而示例一断点调试可知,由始至终就一个全局作用域。(不贴图了)

关于示例二,相当于以下这样,那作用域就更加地清晰了:

var arr = []
{
  let i
  for (i = 0; i < 10; i++) {
    let _i = i
    arr[_i] = function () {
      console.log(_i)
    }
  }
}
arr[6]() // 6


我们再看下,Babel 是如何转换的:


16.webp.jpg


看到这个,那不就想起那个经典面试题:如何修改代码使其打印出 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')
  })


BTW,在 Array.prototype.forEach() 使用 async/await 可能得不到预期结果哦,详情看文章分析。


未完待续...


七、参考



目录
相关文章
|
前端开发 JavaScript
初学JavaScript&输出语句
前端三大件分别为:HTML、CSS、JavaScript。其中最重要的当属JavaScript。 虽然现在开发通常使用框架,但是这些基础的才最重要的,就像一座高楼不能没有稳固的地基一样。打好基础,走遍天下都不怕。JavaScript(简称“JS”) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。 即JavaScript 是一种编程语言,主要参与构建 Web 前端应用。
初学JavaScript&输出语句
|
前端开发 JavaScript
Web前端开发笔记——第四章 JavaScript程序设计 第四节 条件语句和循环语句
Web前端开发笔记——第四章 JavaScript程序设计 第四节 条件语句和循环语句
Web前端开发笔记——第四章 JavaScript程序设计 第四节 条件语句和循环语句
|
JavaScript 前端开发
JavaScript 代码结构:语句、分号和注释
JavaScript 代码结构:语句、分号和注释
187 0
JavaScript 代码结构:语句、分号和注释
|
JavaScript 前端开发
JavaScript的break和continue语句的使用和区别
JavaScript的break和continue语句的使用和区别 JavaScript中break 和 continue都是用来控制循环结构,即用在for循环和while循环中。区别在于: break 语句用于跳出循环。 continue 用于跳过循环中的一个迭代。 1.break语句 break语句会终止循环并且跳出循环 for(var j=0;j&lt;5;j++){ if(j==3){ break; } console.log(j);//0 1 2 }
|
JavaScript 前端开发
JavaScript的流程控制之if条件语句和switch选择语句(一)
JavaScript的流程控制(一) 接下来我么来讲一下JavaScript的流程控制,JavaScript中的流程控制主要包括条件语句和循环语句。这篇博客主要讲条件语句。说到条件语句主要包括if条件语句和switch选择语句两种。 1.if语句 //语法: // if(判断的条件){ // 条件成立时的执行代码 // } //if语句当条件不成立时,不执行任何的代码 var oDate=new Date(); console.log(oDate.getDay()); if(oDate.getDay()==6){
|
JavaScript 前端开发 Shell
开发中切忌使用的JavaScript语句 —— with语句 和 eval语句
开发中切忌使用的JavaScript语句 —— with语句 和 eval语句
156 0
|
JavaScript 前端开发 容器
前端(JavaScript)------运算符及分支语句、循环语句
先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。
97 0
|
JavaScript 前端开发
JS查漏补缺——with语句、eval函数
JS查漏补缺系列是我在学习JS高级语法时做的笔记,通过实践费曼学习法进一步加深自己对其的理解,也希望别人能通过我的笔记能学习到相关的知识点。这一次我们来了解with语句、eval函数
95 0
|
JavaScript
js输入输出语句
js输入输出语句
93 0
js输入输出语句
|
JavaScript 前端开发
JavaScript入门之分支语句
引入 我们知道,当今世界丰富多彩的代码世界如果用语句来区分的话,不外乎三种: 顺序语句 分支语句 循环语句 正是这简简单单的三类控制语句,构成了花样繁多的程序世界。