4

前言

初学JavaScript的时候,知道有各种for的时候,懵懵懂懂,也许是因为没有系统学习的缘故。现在我们把各种for都挨个辨明。

一、for

创建一个循环,包含三个可选表达式。三个可选表达式在圆括号中,由分号分隔。后跟一个循环中执行的语句或块语句。

语法

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

initialization

初始化语句。可写表达式、赋值语句、变量声明。

condition

循环条件表达式。如果表达式结果为truestatement会被执行。如果表达式结果为false,那么执行流程跳到for语句结构后面的第一条语句。不写表达式,就是永远为true

final-expression

每次循环的最后都要执行的表达式。执行时机是在下一次condition的计算之前。

statement

只要condition的结果为true就会被执行的语句。多条语句使用块语句({...})来包含。没有语句执行,使用空语句(;)。

示例

我想输出五个数字。

for (let i = 0; i < 5; i++)
    console.log(i);
/*
0
1
2
3
4
*/

另一种写法输出五个数字。可选的三个表达式,多行语句,需要使用{}包含起来。

for (let i = 0; ; i++) {
    if (i >= 5)
        break;
    console.log(i);
}
/*
0
1
2
3
4
*/

注意,如果不写条件表达式,就要确保循环体内能够跳出,防止死循环。break可以跳出循环。

二、for...in

以任意顺序遍历一个对象的可枚举属性。对于每个枚举的属性,...部分都会被执行。

语法

for (variable in object) {...}

variable

每次迭代的时候,将对象的属性名分配给变量。

object

被迭代枚举的对象。

示例

我想输出对象里所有的属性和值。

let o = {
    a: 1,
    b: 2,
    c: 3
};
for (const v in o) {
    console.log(`o.${v} = ${o[v]}`);
}
/*
o.a = 1
o.b = 2
o.c = 3
*/

可以看见for...in把所有的可枚举属性都枚举了出来,v的类型是String,所以访问当前遍历到的属性值使用了关联数组o[v]的方式。

for...in在遍历的时候,是以任意顺序遍历的。

let o = [];
o[0] = 1;
o['one'] = 2;
o[2] = 3;
for (const v in o) {
    console.log(`o[${v}] = ${o[v]}`);
}
/*
o[0] = 1
o[2] = 3
o[one] = 2
*/

因此当遇到对迭代访问顺序很重要的数组时,最好用整数索引。

我想累加数组所有的成员。

Array.prototype.age = 97;
let o = [1,2];
let sum = 0;
for (const v in o) {
    sum += o[v];
    console.log(`o[${v}] = ${o[v]}`);
}
console.log(`sum = ${sum}`);
/*
o[0] = 1
o[1] = 2
o[age] = 97
sum = 100
*/

很显然这里不符合我们的预期,因为for...in循环语句将返回所有可枚举属性,包括非整数类型的名称和继承的那些。还会获取到原型链上的可枚举属性。

我只想累加自身所有属性。

Array.prototype.age = 97;
let arr = [1, 2];
let sum = 0;
for (const v in arr) {
    if (arr.hasOwnProperty(v)) {
        sum += arr[v];
    }
    console.log(`arr[${v}] = ${arr[v]}`);
}
console.log(`sum = ${sum}`);
/*
o[0] = 1
o[1] = 2
o[age] = 97
sum = 3
*/

如果你只要考虑对象本身的属性,而不是它的原型,那么使用Object.getOwnPropertyNames()或执行Object.prototype.hasOwnProperty()来确定某属性是否是对象本身的属性(也能使用propertyIsEnumerable)。

三、Array.prototype.forEach()

对数组的每个元素执行一次提供的函数。返回值为undefined

语法

Array.forEach(callback[, thisArg])

callback

为数组每个元素执行的函数,这个函数接受三个参数。

currentValue

数组中正在处理的当前元素值。

index

数组中正在处理的当前元素的索引。

array

forEach()方法正在操作的数组。

thisArg

可选参数。当执行回调 函数时用作this的值(参考对象)。

示例

我想输出所有元素。

function logArrayElements(element, index, array) {
    console.log(`a[${index}] = ${element}`);
}
[4, 2, 3].forEach(logArrayElements);
/*
a[0] = 4
a[1] = 2
a[2] = 3
*/

forEcah()会跳过已经删除或者为初始化的项(但不包括那些值为undefined的项,例如在稀疏数组上)。

function logArrayElements(element, index, array) {
    console.log(`a[${index}] = ${element}`);
}
[4, , 3].forEach(logArrayElements);
[1, undefined, 3].forEach(logArrayElements);
/*
a[0] = 4
a[2] = 3
a[0] = 1
a[1] = undefined
a[2] = 3
*/

没有办法终止会跳出forEcah()循环,除了抛出一个异常。

function logArrayElements(element, index, array) {
    console.log(`a[${index}] = ${element}`);
    break;
}
[1, 2, 3].forEach(logArrayElements);
/*
Uncaught SyntaxError: Illegal break statement
    at Array.forEach (<anonymous>)
    at <anonymous>:5:11
*/

使用return也无法中止循环。

使用thisArg,举个勉强的例子。通过自定义的add()方法,计算所添加数组的和sum和成员数count

function Counter() {
    this.sum = 0;
    this.count = 0;
}
Counter.prototype.add = function(array) {
    array.forEach(function(element) {
        this.sum += element;
        ++this.count;
    }, this);
};
let obj = new Counter();
obj.add([1, 3, 5, 7]);
console.log(obj.count);  // 4 === (1+1+1+1)
console.log(obj.sum);  // 16 === (1+3+5+7)
/*
4
16
*/

注意:如果使用箭头函数表达式传入函数参数,thisArg参数会被忽略,因为箭头函数在词法上绑定了this值。

如果数组在迭代时被修改了,则其他元素会被跳过。

let words = ["one", "two", "three", "four"];
words.forEach(function(word) {
  console.log(word);
  if (word === "two") {
    words.shift();
  }
});
/*
one
two
four
*/

当到达包含值"two"的项时,整个数组的第一个项被移除了,这导致所有剩下的项前移了一个位置。因为元素"four"现在在数组更前的位置,"three"会被跳过。forEach()不会在迭代之前创建数组的副本。

四、for...of

for...of语句在可以迭代的对象(ArrayMapSetStringTypedArrayarguments对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

语法

for (variable of iterable) {
    ...
}

variable

在每次迭代中,将不同属性的值分配给变量。

iterable

被迭代枚举其属性的对象。

示例

迭代Array

let a = [10, 20, 30];
for (let v of a) {
    console.log(v);
}
/*
10
20
30
*/

迭代String

let s = 'Tang';
for (let v of s) {
    console.log(v);
}
/*
T
a
n
g
*/

迭代arguments

(function() {
    for (let v of arguments) {
        console.log(v);
    }
}
)(1, 2, 3);
/*
1
2
3
*/

区别

无论是for...in还是for...of语句都是迭代一些东西。它们之间的主要区别在于它们的迭代方式。

for...in语句以原始插入顺序迭代对象的可枚举属性。

for...of语句遍历可迭代对象定义要迭代的数据。

以下示例显示了与Array一起使用时,for...of循环和for...in循环之间的区别。

Object.prototype.objCustom = function() {}; 
Array.prototype.arrCustom = function() {};

let iterable = [3, 5, 7];
iterable.foo = 'hello';

for (let i in iterable) {
  console.log(i);
}
/*
0
1
2
foo
arrCustom
objCustom
*/

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i);
  }
}
/*
0
1
2
foo
*/

for (let i of iterable) {
  console.log(i);
}
/*
3
5
7
*/

唐金健
426 声望965 粉丝

喜欢不断推倒重来,接近于完美的产品。