js中的遍历循环

206 阅读17分钟

for

for循环应该是最普遍的,使用最多的一种循环遍历方法了,所以也导致其可读性和易维护性比较差,但是它可以及时break出循环。

let arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
    console.log(i, arr[i])
}
// 0 1
// 1 2
// 2 3

while

也是JS常见的循环

arr = ["1", "2", "3", "4"];
var i = 0;
while (arr[i]) {
    console.log(arr[i] + "<br>")
    i++;
};

do while循环

此循环是while循环的一个变体,它首先执行一次操作,然后再进行条件判断,是true的话就继续执行操作,否则的话,循环结束。

let i = 3;
do {
    console.log(i)
    i--;
}
while (i > 0)
// 3
// 2
// 1

for...in

语法

for (variable in object)
  statement
  • variable在每次迭代时,variable 会被赋值为不同的属性名。
  • object 非 Symbol 类型的可枚举属性被迭代的对象。

说明

  • for…in…不应该用于迭代于一个关注于索引顺序的Array,它是为遍历对象而构建的
  • for...in循环主要针对于对象的遍历,当想要获取对象的对应键值时,使用for...in还是比较方便的
//针对对象来说  
//任何对象都继承了Object对象,或者其它对象,继承的类的属性是默认不可遍历的,
//for... in循环遍历的时候会跳过,但是这个属性是可以更改为可以遍历的,那么就会造成遍历到不属于自身的属性。
//结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性。否则就可以产生遍历失真的情况。
let obj = {name: 'xiaohua', sex: 'male', age: '28'}
for(let key in obj){
    if(obj.hasOwnProperty(key)){
        console.log(obj[key])
    }
}

for...of

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

对于for...of的循环,可以由 breakthrow 或 return 终止。在这些情况下,迭代器关闭。 throw/return/break;// 跳出循环 continue; 语句时,会终止当前的这一次循环,进入下一次的循环中。

语法

for (variable of iterable) {
    //statements
}
  • variable 在每次迭代中,将不同属性的值分配给变量。
  • iterable 被迭代枚举其属性的对象。
let iterable = [10, 20, 30];
for (let value of iterable) {
    value += 1;
    console.log(value);
}
// 11
// 21
// 31
let iterable = "boo";
for (let value of iterable) {
  console.log(value);
}
// "b"
// "o"
// "o"

forEach

定义

forEach()  方法对数组的每个元素执行一次给定的函数。

const array1 = ['a', 'b', 'c'];
array1.forEach(element => console.log(element));

// expected output: "a"
// expected output: "b"
// expected output: "c"

语法

// 箭头函数
forEach((element) => { /* … */ })
forEach((element, index) => { /* … */ })
forEach((element, index, array) => { /* … */ })

// 回调函数
forEach(callbackFn)
forEach(callbackFn, thisArg)

// 内联回调函数
forEach(function(element) { /* … */ })
forEach(function(element, index) { /* … */ })
forEach(function(element, index, array){ /* … */ })
forEach(function(element, index, array) { /* … */ }, thisArg)

参数说明

  • callbackFn:为数组中每个元素执行的函数。函数调用时带有以下参数:
    • element: 数组中正在处理的当前元素。
    • index:数组中正在处理的当前元素的索引。
    • arrayforEach() 方法正在操作的数组。
  • thisArg:可选参数。当执行回调函数 callbackFn 时,用作 this 的值。

返回值

undefined

特征说明

  • 如果 thisArg 参数有值,则每次 callbackFn 函数被调用时,this 都会指向 thisArg 参数。如果省略了 thisArg 参数,或者其值为 null 或 undefinedthis 则指向全局对象。按照函数观察到 this 的常用规则callbackFn 函数最终可观察到 this 值。
  • forEach() 遍历的范围在第一次调用 callbackFn 前就会确定。调用 forEach 后添加到数组中的项不会被 callbackFn 访问到。如果已经存在的值被改变,则传递给 callbackFn 的值是 forEach() 遍历到他们那一刻的值。已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了例如使用 shift(),之后的元素将被跳过。
  • forEach() 为每个数组元素执行一次 callbackFn 函数;与 map() 或者 reduce() 不同的是,它总是返回 undefined 值,并且不可链式调用。
  • forEach 不会直接改变调用它的对象,但是那个对象可能会被 callbackFn 函数改变。
  • 除了抛出异常以外,没有办法中止或跳出 forEach() 循环。如果你需要中止或跳出循环,forEach() 方法不是应当使用的工具。

剖析Javascript中forEach()并重写

我们平时用的forEach()一般是这样用的

var myArr = [1,5,8]  
myArr.forEach((v,i)=>{  
    console.log(v,i)  
})  
//运行后是这样的  
    1 0  
    5 1  
    8 2

其实底层还是把数组循环了一遍,并且在回调函数里传了每个数组的值和下标。

常规的方法去重写forEach()

const myForEach = function (arr, fn) {
    let i;
    for (i = 0; i < arr.length; i++) {
      fn(arr[i], i);
    }
};

使用

let arr = ['a', 'b', 'c', 'd'];
myForEach(arr, (item, index) => {
    console.log(item, index);
});

image.png

这里是不是和上面用forEach()的输出完全一致啊

但是调用方法和上面并不一样,那我们怎么把他封装到Array对象里去呢

这样就要用到js里的原型链prototype,其实Javascript中所有的对象都是Object的实例,并继承Object.prototype的属性和方法。

重写forEach()挂载到Array.prototype上

      Array.prototype.newForEach = function (fn) {
        let i;
        for (i = 0; i < this.length; i++) {
          fn(this[i], i);
        }
      };

使用

 let arr = ['a', 'b', 'c', 'd'];
 arr.newForEach((item, index) => {
    console.log(item, index);
  });

这个就完全和forEach一样啦,当我们在开发项目时如果对后台返回的数据数组中每个数据都要做统一处理时,这时候我们就可以重写forEach()。这样大家都可以统一直接用这个方法,开发效率就会大大提高

与for循环的区别

本质区别

for循环是js提出时就有的循环方法。

forEach是ES5提出的,挂载在可迭代对象原型上的方法,例如Array Set Map。

forEach是一个迭代器,负责遍历可迭代对象。那么遍历,迭代,可迭代对象分别是什么呢。

  • 遍历:指的对数据结构的每一个成员进行有规律的且为一次访问的行为。
  • 迭代:迭代是递归的一种特殊形式,是迭代器提供的一种方法,默认情况下是按照一定顺序逐个访问数据结构成员。迭代也是一种遍历行为。
  • 可迭代对象:ES6中引入了 iterable 类型,Array Set Map String arguments NodeList 都属于 iterable,他们特点就是都拥有 [Symbol.iterator] 方法,包含他的对象被认为是可迭代的 iterable

image.png

在了解这些后就知道 forEach 其实是一个迭代器,他与 for 循环本质上的区别是 forEach 是负责遍历(Array Set Map)可迭代对象的,而 for 循环是一种循环机制,只是能通过它遍历出数组。

什么是迭代器? 之前提到的 Generator 生成器,当它被调用时就会生成一个迭代器对象(Iterator Object),它有一个 .next()方法,每次调用返回一个对象{value:value,done:Boolean}value返回的是 yield 后的返回值,当 yield 结束,done 变为 true,通过不断调用并依次的迭代访问内部的值。

迭代器是一种特殊对象。ES6规范中它的标志是返回对象的 next() 方法,迭代行为判断在 done 之中。在不暴露内部表示的情况下,迭代器实现了遍历。

let arr = [1, 2, 3, 4]  // 可迭代对象
let iterator = arr[Symbol.iterator]()  // 调用 Symbol.iterator 后生成了迭代器对象
console.log(iterator.next()); // {value: 1, done: false}  访问迭代器对象的next方法
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

我们看到了。只要是可迭代对象,调用内部的 Symbol.iterator 都会提供一个迭代器,并根据迭代器返回的next 方法来访问内部,这也是 for…of 的实现原理。

let arr = [1, 2, 3, 4]
for (const item of arr) {
    console.log(item); // 1 2 3 4 
}

把调用 next 方法返回对象的 value 值并保存在 item 中,直到 value 为 undefined 跳出循环,所有可迭代对象可供for…of消费。 再来看看其他可迭代对象:

function num(params) {
    console.log(arguments); // Arguments(6) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    let iterator = arguments[Symbol.iterator]()
    console.log(iterator.next()); // {value: 1, done: false}
    console.log(iterator.next()); // {value: 2, done: false}
    console.log(iterator.next()); // {value: 3, done: false}
    console.log(iterator.next()); // {value: 4, done: false}
    console.log(iterator.next()); // {value: undefined, done: true}
}
num(1, 2, 3, 4)

let set = new Set('1234')
set.forEach(item => {
    console.log(item); // 1 2 3 4
})
let iterator = set[Symbol.iterator]()
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

所以我们也能很直观的看到可迭代对象中的 Symbol.iterator 属性被调用时都能生成迭代器,而 forEach 也是生成一个迭代器,在内部的回调函数中传递出每个元素的值。

语法区别

(1)基本语法见上文

(2)forEach的中断 在js中有break return continue 对函数进行中断或跳出循环的操作,我们在 for循环中会用到一些中断行为,对于优化数组遍历查找是很好的,但由于forEach属于迭代器,只能按序依次遍历完成,所以不支持上述的中断行为。

  • 它总是返回未定义
  • 不能使用 continue 关键字跳过迭代
  • 不能用 break 关键字提前结束循环
let arr = [1, 2, 3, 4],
    i = 0,
    length = arr.length;
for (; i < length; i++) {
    console.log(arr[i]); //1,2
    if (arr[i] === 2) {
        break;
    };
};

arr.forEach((self,index) => {
    console.log(self);
    if (self === 2) {
        break; //报错
    };
});

arr.forEach((self,index) => {
    console.log(self);
    if (self === 2) {
        continue; //报错
    };
});

可以使用 return 语句。 尽管它不会返回除 undefined 之外的值,但它允许您跳过当前迭代。

let arr = ['a', 'b', 'c', 'd'];
arr.forEach(function (item, index, array) {
    if (index === 2) {
      return;
    }
    console.log(index);
  });

image.png

如果一定要在 forEach 中跳出循环呢?其实是有办法的,借助try/catch

try {
    var arr = [1, 2, 3, 4];
    arr.forEach(function (item, index) {
        //跳出条件
        if (item === 3) {
            throw new Error("LoopTerminates");
        }
        //do something
        console.log(item);
    });
} catch (e) {
    if (e.message !== "LoopTerminates") throw e;
};

(3)for 循环可以控制循环起点 forEach 的循环起点只能为0不能进行人为干预,而for循环不同

let arr = [1, 2, 3, 4],
    i = 1,
    length = arr.length;

for (; i < length; i++) {
    console.log(arr[i]) // 2 3 4
};

性能区别

在性能对比方面我们加入一个 map 迭代器,它与 filter 一样都是生成新数组。

我们对比 for forEach map 的性能在浏览器环境中都是什么样的: 性能比较:for > forEach > map

在chrome 62 和 Node.js v9.1.0环境下:for 循环比 forEach 快1倍,forEach 比 map 快20%左右。

原因分析

  • for:for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。
  • forEach:对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。
  • map:map 最慢的原因是因为 map 会返回一个新的数组,数组的创建和赋值会导致分配内存空间,因此会带来较大的性能开销。如果将map嵌套在一个循环中,便会带来更多不必要的内存消耗。

当大家使用迭代器遍历一个数组时,如果不需要返回一个新数组却使用 map 是违背设计初衷的。

map

定义

map()  方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。

const array1 = [1, 4, 9, 16];

// Pass a function to map
const map1 = array1.map(x => x * 2);

console.log(map1);
// Expected output: Array [2, 8, 18, 32]

语法

// 箭头函数
map((element) => { /* … */ })
map((element, index) => { /* … */ })
map((element, index, array) => { /* … */ })

// 回调函数
map(callbackFn)
map(callbackFn, thisArg)

// 内联回调函数
map(function(element) { /* … */ })
map(function(element, index) { /* … */ })
map(function(element, index, array){ /* … */ })
map(function(element, index, array) { /* … */ }, thisArg)

参数说明

  • callbackFn:生成新数组元素的函数,使用三个参数:
    • element: 数组中正在处理的当前元素。
    • index:数组中正在处理的当前元素的索引。
    • arraymap() 方法正在操作的数组。
  • thisArg:可选参数。当执行回调函数 callbackFn 时,用作 this 的值。

返回值

一个新数组,每个元素都是回调函数的返回值。

特征说明

map 方法会给原数组中的每个元素都按顺序调用一次 callbackFn 函数。callbackFn 每次执行后的返回值(包括 undefined)组合起来形成一个新数组。 callbackFn 函数只会在有值的索引上被调用;那些从来没被赋过值或者使用 delete 删除的索引则不会被调用。

因为 map 生成一个新数组,当你不打算使用返回的新数组却使用 map 是违背设计初衷的,请用 forEach 或者 for-of 替代。

forEach vs map

相同点

  • 参数相同,只能遍历数组
  • 停止迭代唯一的方法是从回调函数中抛出异常
  • forEach()map() 被调用时,不会改变原数组,也就是调用它的数组(尽管 callbackFn 函数在被调用时可能会改变原数组)。
  • forEach()map()方法处理数组元素的范围是在 callbackFn 方法第一次调用之前就已经确定了。调用 map 方法之后追加的数组元素不会被 callbackFn 访问。

如果需要终止循环,那么应该使用简单的 for 循环或 for-of / for-in 循环

不同点

  • forEach()方法没有返回值,而map()方法有返回值。
  • map()会分配内存空间存储新数组并返回,forEach()不会返回数据。

是否修改原数组

基本数据类型

当数组中的item为基本数据类型,不会改动原数组

let array = [1, 2, 3, 4];
array.forEach(ele => {
ele = ele * 3;
});
console.log(array); // [1,2,3,4]
let array = [1, 2, 3, 4];
array.map(ele => {
ele = ele * 3;
});
console.log(array); // [1,2,3,4]

引用数据类型

当数组中的item为引用数据类型,可以改动原数组


let objArr = [
    {
      name: 'wxw1',
      age: 22
    },
    {
      name: 'wxw2',
      age: 33
    }
];
objArr.forEach(ele => {
    if (ele.name === 'wxw2') {
      ele.age = 88;
    }
});
console.log(objArr); // [{name: "wxw1", age: 22},{name: "wxw2", age: 88}]

let objArr = [
    {
      name: 'wxw1',
      age: 22
    },
    {
      name: 'wxw2',
      age: 33
    }
];
objArr.map(ele => {
    if (ele.name === 'wxw2') {
      ele.age = 88;
    }
});
console.log(objArr); // [{name: "wxw1", age: 22},{name: "wxw2", age: 88}]

引用类型改变整个单次循环的item ->不行

let changeItemArr = [
    {
      name: 'wxw1',
      age: 22
    },
    {
      name: 'wxw2',
      age: 33
    }
];
changeItemArr.forEach(ele => {
if (ele.name === 'wxw2') {
  ele = {
    name: 'change',
    age: 77
  };
}
});
console.log(changeItemArr); // [{name: "wxw1", age: 22},{name: "wxw2", age: 33}]

map同理

[]方式

      // 基本类型可以欧~
      const numArr = [33, 4, 55];
      numArr.forEach((ele, index, arr) => {
        if (ele === 33) {
          arr[index] = 999;
        }
      });
      console.log(numArr); // [999, 4, 55]

      // 引用类型也可以欧~
      const allChangeArr = [
        { name: 'wxw', age: 22 },
        { name: 'wxw2', age: 33 }
      ];
      allChangeArr.forEach((ele, index, arr) => {
        if (ele.name === 'wxw2') {
          arr[index] = {
            name: 'change',
            age: 77
          };
        }
      });
      console.log(allChangeArr); // // [{name: "wxw", age: 22},{name: "change", age: 77}]

解析:

  • 基本类型我们当次循环拿到的ele,只是forEach/map给我们在另一个地方复制创建新元素,是和原数组这个元素没有半毛钱联系的!所以,我们使命给循环拿到的ele赋值都是无用功!
  • 专业的概念说就是:JavaScript是有基本数据类型引用数据类型之分的。对于基本数据类型:number,string,Boolean,null,undefined它们在栈内存中直接存储变量与值。而Object对象的真正的数据是保存在堆内存栈内只保存了对象的变量以及对应的堆的地址,所以操作Object其实就是直接操作了原数组对象本身。
  • forEach/map 的基本原理也是for循环,使用arr[index]的形式赋值改变,无论什么就都可以改变了。

filter

语法

语法同forEach/map

// 箭头函数
filter((element) => { /* … */ } )
filter((element, index) => { /* … */ } )
filter((element, index, array) => { /* … */ } )

// 回调函数
filter(callbackFn)
filter(callbackFn, thisArg)

// 内联回调函数
filter(function(element) { /* … */ })
filter(function(element, index) { /* … */ })
filter(function(element, index, array){ /* … */ })
filter(function(element, index, array) { /* … */ }, thisArg)
  • callback: 用来测试数组中的每个元素的函数;返回true表示该元素通过测试,保留该元素,false则不保留。他接受三个参数:
    • element:数组中当前正在处理的元素。
    • index:正在处理元素在数组中的索引
    • array:调用了filter的数组本身
  • thisArg: 执行callback时,用于this的值

返回值

一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。

说明

filter() 不会改变原数组。

filter() 遍历的元素范围在第一次调用 callbackFn 之前就已经确定了。修改已经访问过的或在确定的范围之外创建的元素,将不会被 callbackFn 访问。如果以相同的方式删除数组中的现有元素,则不会访问它们。

some

参考链接:developer.mozilla.org/zh-CN/docs/…

some()  方法测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值

语法

语法同forEach/map/filter,这里不做说明。

const array = [1, 2, 3, 4, 5];

// Checks whether an element is even
const even = (element) => element % 2 === 0;

console.log(array.some(even));
// Expected output: true

说明

数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false。

every

every()  方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

语法

语法同forEach/map/filter,这里不做说明。

const isBelowThreshold = (currentValue) => currentValue < 40;

const array1 = [1, 30, 39, 29, 10, 13];

console.log(array1.every(isBelowThreshold));
// Expected output: true

说明

如果回调函数的每一次返回都为 truthy 值,返回 true,否则返回 false

flatMap

定义

flatMap()  方法对数组中的每个元素应用给定的回调函数,然后将结果展开一级,返回一个新数组。它等价于在调用 map() 方法后再调用深度为 1 的 flat() 方法(arr.map(...args).flat()),但比分别调用这两个方法稍微更高效一些。

语法

flatMap(callbackFn)
flatMap(callbackFn, thisArg)

参数:

  • callbackFn:一个在数组的每个元素上执行的函数。它应该返回一个包含新数组元素的数组,或是要添加到新数组中的单个非数组值。该函数将被传入以下参数:
    • element:数组中正在处理的当前元素。
  • thisArg:在执行 callbackFn 时用作 this 的值。参见迭代方法

返回值: 一个新的数组,其中每个元素都是回调函数的结果,并且被展开一级。

const newArray = array.flatMap((currentValue, index, array) => { 
    // 返回值可以是数组、单个值,最终会被扁平化一层 
    return [/* 映射后的内容 */]; 
});

例子

eg1

const arr1 = [1, 2, 1];

const result = arr1.flatMap((num) => (num === 2 ? [2, 2] : 1));

console.log(result);
// Expected output: Array [1, 2, 2, 1]

eg2 基础映射 + 扁平化

对比 mapflatMap 的区别,能直观看到效果:

// 原始数组 
const arr = [1, 2, 3]; 
// 只用 map:返回二维数组 
const mapResult = arr.map(num => [num, num * 2]); console.log(mapResult); // [[1,2], [2,4], [3,6]]

// 用 flatMap:自动扁平化一层,返回一维数组 
const flatMapResult = arr.flatMap(num => [num, num * 2]); console.log(flatMapResult); // [1,2,2,4,3,6]

eg3 过滤 + 映射

flatMap 还能实现 “过滤 + 映射”:返回空数组时,会被扁平化后忽略,相当于过滤掉该元素。

// 需求:提取数组中大于 1 的数字,并每个数字翻倍 
const arr = [1, 2, 3, 0]; 
const result = arr.flatMap(num => { 
if (num > 1) { 
return [num * 2]; 
// 符合条件:返回数组(会被扁平化) 
} else { 
return []; 
// 不符合条件:返回空数组(扁平化后消失) 
} });
console.log(result); // [4, 6]

eg4 处理接口返回的嵌套数据

// 接口返回的嵌套数据 
const res = [ 
{ id: 1, tags: ['js', 'react'] }, 
{ id: 2, tags: ['css', 'vue'] }, 
{ id: 3, tags: [] } 
// 空标签 
]; 
// 需求:提取所有标签,去重后返回 
const allTags = [...new Set(res.flatMap(item => item.tags))]; 
console.log(allTags); // ["js", "react", "css", "vue"]

eg5 拆分字符串并过滤空值

// 需求:将句子拆分为单词,过滤掉空字符串
const sentences = ['hello world', '  ', 'js flatMap'];

const words = sentences.flatMap(sentence => {
  return sentence.trim() ? sentence.split(' ') : [];
});
console.log(words); // ["hello", "world", "js", "flatMap"]

说明

  • flatMap 只能扁平化一层数组,如果需要多层扁平化,需先用 flatMap 再用 flat(n),或直接用 flat(n)
  • 不会改变原数组,返回新数组。
  • 兼容性:ES2019 新增,现代浏览器 / Node.js 12+ 支持;如需兼容低版本,可手动实现 map + flat(1)

注意

  • flatMap = map + flat(1),核心作用是映射后扁平化一层数组
  • 适合需要 “映射 + 轻度扁平化” 或 “映射 + 过滤” 的场景,代码更简洁。
  • 返回空数组可实现过滤效果,是它的一个实用技巧。

reduce