在看一篇别人的博客时,看到一个这样的for循环写法,看一眼一脸懵逼,再看一眼还是懵逼,我杠上了,还看,还懵逼。。打扰了。。于是问别的同学,翻了mdn文档再加深理解,可算是整明白了,在此记录一下。

代码如下:

for(var i = 0, fn; fn = ['a', 'b'][i++];) {
    console.log(i)
    console.log(fn)
}

一开始没整明白这个写法for循环是怎么运行的(不看不知道一看吓一跳原来我菜的如此抠脚)
图片说明

其实理解起来挺简单的,是自己思路没捋清楚,看了一眼mdn的文档,语法如下

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

mdn文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for

for循环的圆括号内有三个可选的表达式,使用分号分隔,第一个表达式(initialization)一般用于初始化一个计数器(初始化变量或者赋值语句),第二个表达式(condition)通常用于循环的结束条件,如果条件为真则继续执行,否则跳出循环,第三个表达式(final-expression)通常写变量的自增语句。

在上面的代码中只写了前两个表达式,第三个自增表达式没写,但它被隐式的放在了数组的取值中被调用,所以其实三个表达式都是存在的

然后就是 i++ 和 ++i 的区别需要一下,前置++和后置++的结果是不同的,例如:

var i = 0;
console.log(i++);

图片说明

var i = 0;
console.log(++i);

图片说明

前一个console会先输出i的初始值0,i 再进行+1操作,即i = 0; console.log(i); i = i + 1
后一个console会先输出i + 1的值,i 再进行+1操作,即i = 0; console.log(i + 1); i = i + 1

因此上面的代码中['a', 'b'][i++]实际上是先取数组第零项(a)赋值给fn,i 再进行++操作,然后这里的for循环步骤为:

第一次执行:
① 执行第一个表达式,初始化变量:var i = 0, fn;
② 执行第二个表达式,判断for循环结束条件是否为真:此时 i 是0,['a', 'b'][0]存在,为真(true),同时把数组第零项赋值给fn,然后++操作
③ 执行for循环代码体内的程序(statement),console.log(i)、console.log(fn),此时 i 已经递增为1,console.log(i)输出1,而console.log(fn)就是数组的第零项a

第二次执行:
① 执行第二个表达式,判断for循环结束条件是否为真:此时 i 是1, ['a', 'b'][1]存在,为真(true),继续执行,把数组第一项赋值给fn,然后++操作
② 执行for循环代码体内的程序(statement),console.log(i)、console.log(fn),此时 i 已经递增为2,console.log(i)输出2,而console.log(fn)就是数组的第一项b
第三次执行:
① 执行第二个表达式,判断for循环结束条件是否为真:此时 i 是2, ['a', 'b'][2]为undefined,不存在,为假(false),跳出循环体(break)

下面代码是更容易理解的写法:

for(var i = 0,arr = ['a', 'b'], fn; arr[i];){
    fn = arr[i];
    i++;
    console.log(i);
    console.log(fn);
}
// 将表达式抽离的写法
var arr = ['a', 'b'];
var i = 0;
var fn;
for(;;){
    if(arr[i] == undefined) break;
    fn = arr[i];
    i++;
    console.log(i);
    console.log(fn);
}

输出结果:

图片说明

扩展小知识:

for循环还有一个特别之处,就是在设置循环变量的那部分(圆括号内)是一个父作用域,而循环体内部是一个单独的子作用域。

我们知道let不允许在相同作用域内重复声明一个变量,因此可以使用let进行验证

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

上面代码正确运行,输出了3次abc,表明for循环内部的变量i与循环变量i不在一个作用域,有各自独立的作用域。

有人可能会问这个作用域是不是全局作用域,再验证一下圆括号内的作用域和全局作用域是否是两个不同的作用域

let i = 666;
for (let i = 0; i < 3; i++) {
  console.log(i);
}
// 0
// 1
// 2

上面的代码正确输出0,1,2,说明for循环的圆括号内有自己单独的作用域。

如果在for循环的圆括号内使用var声明变量,这个用来计数的循环变量会被泄露为全局变量

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量,这也是为什么需要块级作用域的其中一点原因,因此推荐在for循环中使用let声明循环变量。

再一次验证for循环中使用var声明循环变量泄露为全局变量:

var i = 666;
for (var i = 0; i < 3; i++) {
  console.log(i);
}
console.log(i)
// 0
// 1
// 2
// 3

上面代码中,for循环结束后输出i是3,并没有读取一开始定义的i = 666,而是for循环的结束值直接替换了666,再一次说明了在for循环中使用var声明变量被泄露成全局变量。

参考文档:https://es6.ruanyifeng.com/#docs/let