作者:技術直男星辰
來源:juejin.cn/post/7018097650687803422
js中那么多回圈,for for...in for...of forEach,有些回圈感覺上是大同小異今天我們討論下for回圈和forEach的差異,我們從幾個維度展開討論:
for回圈和forEach的本質區別,for回圈和forEach的語法區別,for回圈和forEach的性能區別,
本質區別
for回圈是js提出時就有的回圈方法,forEach是ES5提出的,掛載在可迭代物件原型上的方法,例如Array Set Map,forEach是一個迭代器,負責遍歷可迭代物件,那么遍歷,迭代,可迭代物件分別是什么呢,
遍歷:指的對資料結構的每一個成員進行有規律的且為一次訪問的行為,
迭代:迭代是遞回的一種特殊形式,是迭代器提供的一種方法,默認情況下是按照一定順序逐個訪問資料結構成員,迭代也是一種遍歷行為,
可迭代物件:ES6中引入了 iterable 型別,Array Set Map String arguments NodeList 都屬于 iterable,他們特點就是都擁有 [Symbol.iterator] 方法,包含他的物件被認為是可迭代的 iterable,

在了解這些后就知道 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 也是生成一個迭代器,在內部的回呼函式中傳遞出每個元素的值,
(感興趣的同學可以搜下 forEach 原始碼, Array Set Map 實體上都掛載著 forEach ,但網上的答案大多數是通過 length 判斷長度, 利用for回圈機制實作的,但在 Set Map 上使用會報錯,所以我認為是呼叫的迭代器,不斷呼叫 next,傳參到回呼函式,由于網上沒查到答案也不妄下斷言了,有答案的人可以評論區留言)
for回圈和forEach的語法區別
了解了本質區別,在應用程序中,他們到底有什么語法區別呢?
forEach的引數,forEach的中斷,forEach洗掉自身元素,index不可被重置,for回圈可以控制回圈起點,
forEach 的引數
我們真正了解 forEach 的完整傳參內容嗎?它大概是這樣:
arr.forEach((self,index,arr) =>{},this)
self: 陣列當前遍歷的元素,默認從左往右依次獲取陣列元素,
index: 陣列當前元素的索引,第一個元素索引為0,依次類推,
arr: 當前遍歷的陣列,
this: 回呼函式中this指向,
let arr = [1, 2, 3, 4];
let person = {
name: '技術直男星辰'
};
arr.forEach(function (self, index, arr) {
console.log(`當前元素為${self}索引為${index},屬于陣列${arr}`);
console.log(this.name+='真帥');
}, person)
我們可以利用 arr 實作陣列去重:
let arr1 = [1, 2, 1, 3, 1];
let arr2 = [];
arr1.forEach(function (self, index, arr) {
arr.indexOf(self) === index ? arr2.push(self) : null;
});
console.log(arr2); // [1,2,3]

forEach 的中斷
在js中有break return continue 對函式進行中斷或跳出回圈的操作,我們在 for回圈中會用到一些中斷行為,對于優化陣列遍歷查找是很好的,但由于forEach屬于迭代器,只能按序依次遍歷完成,所以不支持上述的中斷行為,
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; //報錯
};
});
如果我一定要在 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;
};
若遇到 return 并不會報錯,但是不會生效
let arr = [1, 2, 3, 4];
function find(array, num) {
array.forEach((self, index) => {
if (self === num) {
return index;
};
});
};
let index = find(arr, 2);// undefined
forEach 洗掉自身元素,index不可被重置
在 forEach 中我們無法控制 index 的值,它只會無腦的自增直至大于陣列的 length 跳出回圈,所以也無法洗掉自身進行index重置,先看一個簡單例子:
let arr = [1,2,3,4]
arr.forEach((item, index) => {
console.log(item); // 1 2 3 4
index++;
});
index不會隨著函式體內部對它的增減而發生變化,在實際開發中,遍歷陣列同時洗掉某項的操作十分常見,在使用forEach洗掉時要注意,
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
};
那之前的陣列遍歷并洗掉滋生的操作就可以寫成
let arr = [1, 2, 1],
i = 0,
length = arr.length;
for (; i < length; i++) {
// 洗掉陣列中所有的1
if (arr[i] === 1) {
arr.splice(i, 1);
//重置i,否則i會跳一位
i--;
};
};
console.log(arr); // [2]
//等價于
var arr1 = arr.filter(index => index !== 1);
console.log(arr1) // [2]
for回圈和forEach的性能區別
在性能對比方面我們加入一個 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 的:
let data = https://www.cnblogs.com/javastack/p/[];
let data2 = [1,2,3];
data2.map(item=>data.push(item));
寫在最后:這是面試遇到的一個問題,當時只知道語法區別,并沒有從可迭代物件,迭代器,生成器和性能方面,多角度進一步區分兩者的異同,也希望能把一個簡單的問題從多角度展開細講,讓大家正在搞懂搞透徹,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
2.勁爆!Java 協程要來了,,,
3.Spring Boot 2.x 教程,太全了!
4.20w 程式員紅包封面,快快領取,,,
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/424872.html
標籤:Java
