如果再写 for 循环,我就锤自己!
幾種遍歷方法中for執行最快,它沒有任何額外的函數調用棧和上下文。但在實際開發中我們要結合語義話、可讀性和程序性能,去選擇究竟使用哪種方案。下面來看for?,?foreach?,?map?,?for...in?,?for...of五種方法現場battle。
自我介紹
for
我是最早出現的一方遍歷語句,在座的各位需稱我一聲爺爺。我能滿足開發人員的絕大多數的需求。
//?遍歷數組 let?arr?=?[1,2,3]; for(let?i?=?0;i?<?arr.length;i++){console.log(i)?//?索引,數組下標console.log(arr[i])?//?數組下標所對應的元素 }//?遍歷對象 let?profile?=?{name:"April",nickname:"二十七刻",country:"China"}; for(let?i?=?0,?keys=Object.keys(profile);?i?<?keys.length;i++){console.log(keys[i])?//?對象的鍵值console.log(profile[keys[i]])?//?對象的鍵對應的值 }//?遍歷字符串 let?str?=?"abcdef"; for(let?i?=?0;i?<?str.length?;i++){console.log(i)?//?索引?字符串的下標console.log(str[i])?//?字符串下標所對應的元素 }//?遍歷DOM?節點 let?articleParagraphs?=?document.querySelectorAll('.article?>?p'); for(let?i?=?0;i<articleParagraphs.length;i++){articleParagraphs[i].classList.add("paragraph");//?給class名為“article”節點下的 p 標簽添加一個名為“paragraph” class屬性。 }forEach
我是ES5版本發布的。按升序為數組中含有效值的每一項執行一次 callback 函數,那些已刪除或者未初始化的項將被跳過(例如在稀疏數組上)。我是 for 循環的加強版。
//?遍歷數組 let?arr?=?[1,2,3]; arr.forEach(i?=>?console.log(i))//?logs?1 //?logs?2 //?logs?3 //?直接輸出了數組的元素//遍歷對象 let?profile?=?{name:"April",nickname:"二十七刻",country:"China"}; let?keys?=?Object.keys(profile); keys.forEach(i?=>?{console.log(i)?//?對象的鍵值console.log(profile[i])?//?對象的鍵對應的值 })map
我也是ES5版本發布的,我可以創建一個新數組,新數組的結果是原數組中的每個元素都調用一次提供的函數后的返回值。
let?arr?=?[1,2,3,4,5]; let?res?=?arr.map(i?=>?i?*?i);console.log(res)?//?logs?[1,?4,?9,?16,?25]for...in枚舉
我是ES5版本發布的。以任意順序遍歷一個對象的除Symbol以外的可枚舉屬性。
//?遍歷對象 let?profile?=?{name:"April",nickname:"二十七刻",country:"China"}; for(let?i?in?profile){let?item?=?profile[i];console.log(item)?//?對象的鍵值console.log(i)?//?對象的鍵對應的值//?遍歷數組 let?arr?=?['a','b','c']; for(let?i?in?arr){let?item?=?arr[i];console.log(item)?//?數組下標所對應的元素console.log(i)?//?索引,數組下標//?遍歷字符串 let?str?=?"abcd" for(let?i?in?str){let?item?=?str[i];console.log(item)?//?字符串下標所對應的元素console.log(i)?//?索引?字符串的下標 }for...of迭代
我是ES6版本發布的。在可迭代對象(包括 Array,Map,Set,String,TypedArray,arguments 對象等等)上創建一個迭代循環,調用自定義迭代鉤子,并為每個不同屬性的值執行語句。
//?迭代數組數組 let?arr?=?['a','b','c']; for(let?item?of?arr){console.log(item) } //?logs?'a' //?logs?'b' //?logs?'c'//?迭代字符串 let?str?=?"abc"; for?(let?value?of?str)?{console.log(value); } //?logs?'a' //?logs?'b' //?logs?'c'//?迭代map let?iterable?=?new?Map([["a",?1],?["b",?2],?["c",?3]] for?(let?entry?of?iterable)?{console.log(entry); } //?logs?["a",?1] //?logs?["b",?2] //?logs?["c",?3]//?迭代map獲取鍵值 for?(let?[key,?value]?of?iterable)?{console.log(key)console.log(value); }//?迭代set let?iterable?=?new?Set([1,?1,?2,?2,?3,?3,4]); for?(let?value?of?iterable)?{console.log(value); } //?logs?1 //?logs?2 //?logs?3 //?logs?4//?迭代?DOM?節點 let?articleParagraphs?=?document.querySelectorAll('.article?>?p'); for?(let?paragraph?of?articleParagraphs)?{paragraph.classList.add("paragraph");//?給class名為“article”節點下的 p 標簽添加一個名為“paragraph” class屬性。 }//?迭代arguments類數組對象 (function()?{for?(let?argument?of?arguments)?{console.log(argument);} })(1,?2,?3); // logs: //?1 //?2 //?3//?迭代類型數組 let?typeArr?=?new?Uint8Array([0x00,?0xff]); for?(let?value?of?typeArr)?{console.log(value); } // logs: //?0 //?255經過第一輪的自我介紹和技能展示后,我們了解到:
-
for語句是最原始的循環語句。定義一個變量i(數字類型,表示數組的下標),按照一定的條件,對i進行循環累加。條件通常為循環對象的長度,當超過長度就停止循環。因為對象無法判斷長度,所以搭配Object.keys()使用。
-
forEach?ES5 提出。自稱是for語句的加強版,可以發現它比for語句在寫法上簡單了很多。但是本質上也是數組的循環。forEach每個數組元素執行一次 callback 函數。也就是調用它的數組,因此,不會改變原數組。返回值是undefine。
-
map?ES5 提出。給原數組中的每個元素都按順序調用一次 ?callback 函數。生成一個新數組,不修改調用它的原數組本身。返回值是新的數組。
-
for...in?ES5 提出。遍歷對象上的可枚舉屬性,包括原型對象上的屬性,且按任意順序進行遍歷,也就是順序不固定。遍歷數組時把數組的下標當作鍵值,此時的i是個字符串型的。它是為遍歷對象屬性而構建的,不建議與數組一起使用。
-
for...of?ES6 提出。只遍歷可迭代對象的數據。
能力甄別
作為一個程序員,僅僅認識他們是遠遠不夠的,在實際開發中鑒別他們各自的優缺點。因地制宜的使用他們,揚長避短。從而提高程序的整體性能才是能力之所在。
關于跳出循環體
在循環中滿足一定條件就跳出循環體,或者跳過不符合條件的數據繼續循環其它數據。是經常會遇到的需求。常用的語句是break?與?continue。
簡單的說一下二者的區別,就當復習好了。
-
break語句是跳出當前循環,并執行當前循環之后的語句;
-
continue語句是終止當前循環,并繼續執行下一次循環;
注意:forEach?與map?是不支持跳出循環體的,其它三種方法均支持。
原理?:查看forEach實現原理,就會理解這個問題。
Array.prototype.forEach(callbackfn?[,thisArg]{}傳入的function是這里的回調函數。在回調函數里面使用break肯定是非法的,因為break只能用于跳出循環,回調函數不是循環體。
在回調函數中使用return,只是將結果返回到上級函數,也就是這個for循環中,并沒有結束for循環,所以return也是無效的。
map()?同理。
map()鏈式調用
map()?方法是可以鏈式調用的,這意味著它可以方便的結合其它方法一起使用。例如:reduce(),?sort(),?filter()?等。但是其它方法并不能做到這一點。forEach()的返回值是undefined,所以無法鏈式調用。
//?將元素乘以本身,再進行求和。 let?arr?=?[1,?2,?3,?4,?5]; let?res1?=?arr.map(item?=>?item?*?item).reduce((total,?value)?=>?total?+?value);console.log(res1)?//?logs?55?undefined"for...in會遍歷出原型對象上的屬性
Object.prototype.objCustom?=?function()?{}; Array.prototype.arrCustom?=?function()?{}; var?arr?=?['a',?'b',?'c']; arr.foo?=?'hello for?(var?i?in?arr)?{console.log(i); } //?logs //?0 //?1 //?2 //?foo //?arrCustom //?objCustom然而在實際的開發中,我們并不需要原型對象上的屬性。這種情況下我們可以使用hasOwnProperty()?方法,它會返回一個布爾值,指示對象自身屬性中是否具有指定的屬性(也就是,是否有指定的鍵)。如下:
Object.prototype.objCustom?=?function()?{}; Array.prototype.arrCustom?=?function()?{}; var?arr?=?['a',?'b',?'c']; arr.foo?=?'hello for?(var?i?in?arr)?{if?(arr.hasOwnProperty(i))?{console.log(i);} } //?logs //?0 //?1 //?2 //?foo//?可見數組本身的屬性還是無法擺脫。此時建議使用 forEach對于純對象的遍歷,選擇for..in枚舉更方便;對于數組遍歷,如果不需要知道索引for..of迭代更合適,因為還可以中斷;如果需要知道索引,則forEach()更合適;對于其他字符串,類數組,類型數組的迭代,for..of更占上風更勝一籌。但是注意低版本瀏覽器的是配性。
性能
有興趣的讀者可以找一組數據自行測試,文章就直接給出結果了,并做相應的解釋。
for?>?for-of?>?forEach?>?map?>?for-in-
for?循環當然是最簡單的,因為它沒有任何額外的函數調用棧和上下文;
-
for...of只要具有Iterator接口的數據結構,都可以使用它迭代成員。它直接讀取的是鍵值。
-
forEach,因為它其實比我們想象得要復雜一些,它實際上是array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循環的語法糖,還有諸多參數和上下文需要在執行的時候考慮進來,這里可能拖慢性能;
-
map()?最慢,因為它的返回值是一個等長的全新的數組,數組創建和賦值產生的性能開銷很大。
-
for...in需要窮舉對象的所有屬性,包括自定義的添加的屬性也能遍歷到。且for...in的key是String類型,有轉換過程,開銷比較大。
總結
在實際開發中我們要結合語義話、可讀性和程序性能,去選擇究竟使用哪種方案?
-
如果你需要將數組按照某種規則映射為另一個數組,就應該用 map。
-
如果你需要進行簡單的遍歷,用 forEach 或者 for of。
-
如果你需要對迭代器進行遍歷,用 for of。
-
如果你需要過濾出符合條件的項,用 filterr。
-
如果你需要先按照規則映射為新數組,再根據條件過濾,那就用一個 map 加一個 filter。
總之,因地制宜,因時而變。千萬不要因為過分追求性能,而忽略了語義和可讀性。在您的統治之下,他們5個只能是各自發揮長處,誰都別想稱霸。
總結
以上是生活随笔為你收集整理的如果再写 for 循环,我就锤自己!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 字节一面,被连问 MySQL 索引,脸都
- 下一篇: Redis 开发陷阱及避坑指南!