for循环详解,一起成为更专业的前端工程师!

前言

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循环执行的过程。

  1. 首先声明变量并进行初始化, var i=0;
  2. 判断条件是否成立,0<5,成立;
  3. 如果条件成立执行循环体中的语句,console.log(i);
  4. 一次循环结束后会在循环体内部完成afterthought的任务,即i++;
  5. 继续判断条件是否成立,1<5,若成立则继续执行循环体,若不成立则结束循环程序;
  6. 最终循环外的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
// 循环结束
mindmap for consdition为false exits loop 退出循环 goto the first expression/statement after for 执行for循环后面的第一个表达式或语句

迭代程序与块语句

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++;
}

循环中的词法声明

再来详细了解一下初始化计数变量时varlet的区别。

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声明道理也是一样的啦!

相关推荐
鑫~阳15 分钟前
html + css 淘宝网实战
前端·css·html
Catherinemin20 分钟前
CSS|14 z-index
前端·css
心软小念1 小时前
外包干了27天,技术退步明显。。。。。
软件测试·面试
2401_882727572 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder2 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂2 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand2 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL3 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿3 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫3 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js