JS遍歷循環方法性能對比:for/while/for in/for of/map/foreach/every


這周codeReview例會,又遇到map與foreach到底誰問題。單獨圖方便,我會選擇用map一個函數搞定一切。但是從語義的角度來講,如果只是單純遍歷,還是推薦選擇foreach。其實formap 與foreach,性能相差不大(個人測試數據在10000000,最后有測試案例)。如果用foreach 去實現map的效果,性能上就會比map差(因為需要操作另外一個數組).

使用for,變量提前聲明,性能會有一丟丟提升。如果循環變量i掛在全局變量上,也會造成性能損耗

如果i是掛在全局上的,因為他每次loop完都要從全局中找回i值,i++ 和 判斷

而封裝在 function里面的,對比與在全局里找i,單單在function 里找起來比較快

——《javascript循環時間判斷優化!

從性能上考量,我從eslint上禁止 for in。

之前在gem代碼重構的過程中,講了很多次 for in for map foreach等遍歷情況,但是沒有過系統性地解析。

這次決定 把之前看的東西,東拼西湊地再來一篇總結。

遍歷數組性能分析

對數組的遍歷大家最常用的就是for循環,ES5的話也可以使用forEach,ES5具有遍歷數組功能的還有map、filter、some、every、reduce、reduceRight等,只不過他們的返回結果不一樣。

如果都做同樣的遍歷,他們的性能是怎么樣的呢?

{ name: 'time-While', value: 18 },

{ name: 'time-ForFilter', value: 123 },

{ name: 'time-ForEvery', value: 139 },

{ name: 'time-ForSome', value: 140 },

{ name: 'time-ForOf', value: 158 },

{ name: 'time-ForEach', value: 174 },

{ name: 'time-ForMap', value: 190 },

{ name: 'time-For', value: 544 },

{ name: 'time-ForIn', value: 6119 }

結果是 while 是最快的(理論上,感覺for與while應該是等效的)。 formap等es5 函數快於 for,formap 快於foreach . for in 最慢

為什么for in 這么慢?

使用for in會遍歷數組所有的可枚舉屬性,包括原型。例如上栗的原型方法method和name屬性

解釋器遇到for...in 循環時,在后台需要為對象建立一個枚舉器(enumerator),這是一個昂貴的操作!

for in 注意事項

  • index索引為字符串型數字,不能直接進行幾何運算

  • 遍歷順序有可能不是按照實際數組的內部順序

for in遍歷的是數組的索引(即鍵名),而for of遍歷的是數組元素值。 所以for in更適合遍歷對象,不要使用for in遍歷數組

for in 遍歷順序問題

關於for in 屬性問題,可以看下面兩段代碼

const arr = [100, 'B', 4, '5', 3,  'A', 0];
for (const key in arr) {
  console.log(`index:${key} value:${arr[key]}`);
}
console.log('________\n');
function Foo() {
  this[100] = 100;
  this.B = 'B';
  this[4] = 4;
  this['5'] = '5';
  this[3] = 3;
  this.A = 'A';
  this[0] = 0;
}
const bar = new Foo();
for (const key in bar) {
  console.log(`index:${key} value:${bar[key]}`);
}

在ECMAScript規范中定義了 「數字屬性應該按照索引值⼤⼩升序排列,字符 串屬性根據創建時的順序升序排列。」

V8內部,為了有效地提升存儲和訪問這兩種屬性的性能,分別使⽤了兩個 線性數據結構來分別保存排序 屬性和常規屬性,具體結構如下圖所⽰:

js V8 排序屬 常規屬性

對象中的數字屬性稱為 「排序屬性」,在V8中被稱為 elements,字符串屬性就被稱為 「常規屬性」, 在V8中被稱為 properties。

在elements對象中,會按照順序存放排序屬性,properties屬性則指向了properties對 象,在properties對象中,會按照創建時的順序保存了常規屬性。關於 for in 與 for of更詳細的,參看  https://zhuanlan.zhihu.com/p/161892289

for ..in 與 for..of區別

一句話概括:for in是遍歷(object)鍵名,for of是遍歷(array)鍵值——for of 循環用來獲取一對鍵值對中的值,而 for in 獲取的是 鍵名。

  • for in 循環出的是key(並且key的類型是string),for of 循環出的是value。

  • for of 是es6引新引入的特性,修復了es5引入的for in 的不足。

  • for of 不能循環普通的對象,需要通過Object.keys搭配使用。

對於他們的區別,一般就看下面一段代碼就可:

{
  const b = [1, 2, 3, 4];    // 創建一個數組
  b.name = '小明';               // 給數組添加一個屬性
  Array.prototype.age = 12;      // 給數組的原型也添加一個屬性
  console.log('for in ---------------');
  for (const key in b) {
    console.log(key);
  }
  console.log('for of ---------------');
  for (const key of b) {
    console.log(key);
  }
  console.log('forEach ---------------');
  b.forEach((item) => {
    console.log(item);
  });
}
console.log('______________\n');
{
  const b = { a: 1, b: 2 };    // 創建一個對象
  b.name = '小明';               // 給對象添加一個屬性
  Object.prototype.age = 12;      // 給對象的原型也添加一個屬性
  console.log('for in ---------------');
  for (const key in b) {
    console.log(key);
  }
  console.log('forEach ---------------');
  Object.keys(b).forEach((item) => {
    console.log(item);
  });
}

可以通過hasOwnProperty限制for..in 遍歷范圍。

for...in

for...in 循環只遍歷可枚舉屬性(包括它的原型鏈上的可枚舉屬性)。這個代碼是為普通對象設計的,不適用於數組的遍歷

JavaScript中的可枚舉屬性與不可枚舉屬性

在JavaScript中,對象的屬性分為可枚舉和不可枚舉之分,它們是由屬性的enumerable值決定的。可枚舉性決定了這個屬性能否被for…in查找遍歷到。

像 Array和Object使用內置構造函數所創建的對象都會繼承自Object.prototype和String.prototype的不可枚舉屬性,例如 String 的 indexOf()  方法或 Object的toString()方法。循環將遍歷對象本身的所有可枚舉屬性,以及對象從其構造函數原型中繼承的屬性(更接近原型鏈中對象的屬性覆蓋原型屬性)。

枚舉性屬性的影響
  1. for in (遍歷所有可枚舉屬性,不僅是 own properties 也包括原型鏈上的所有屬性)

  2. Object.keys(只返回對象本身具有的可枚舉的屬性)

  3. JSON.stringify() (只讀取對象本身可枚舉屬性,並序列化為JSON字符串)

  4. Object.assign() (復制自身可枚舉的屬性,進行淺拷貝)

引入enumerable的最初目的,就是讓某些屬性可以規避掉for...in操作。比如,對象原型的toString方法,以及數組的length屬性,就通過這種手段,不會被for...in遍歷到。

for...of

for of 是es6引新引入的特性,修復了es5引入的for in 的不足。

for...of 只可遍歷可迭代對象,for...of 語句在可迭代對象(包括Array,Map,Set,String,TypedArray,arguments 對象等等)上創建一個迭代循環,調用自定義迭代鈎子,並為每個不同屬性的值執行語句

什么數據可以for of遍歷

一個數據結構只要部署了 Symbol.iterator 屬性, 就被視為具有 iterator接口, 就可以使用 for of循環。

些數據結構部署了 Symbol.iteratoer屬性了呢?

只要有 iterator 接口的數據結構,都可以使用 for of循環。

  • 數組 Array

  • Map

  • Set

  • String

  • arguments對象

  • Nodelist對象, 就是獲取的dom列表集合

-以上這些都可以直接使用 for of 循環。 凡是部署了 iterator 接口的數據結構也都可以使用數組的 擴展運算符(...)、和解構賦值等操作。

for of不可以遍歷普通對象,想要遍歷對象的屬性,可以用for in循環, 或內建的Object.keys()方法。

for循環與ES5新增的foreach/map 等方法有何區別?

forEach 不支持在循環中添加刪除操作,因為在使用 forEach 循環的時候數組(集合)就已經被鎖定不能被修改。(改了也沒用)

在 for 循環中可以使用 continue,break 來控制循環和跳出循環,這個是 forEach 所不具備的。【在這種情況下,從性能的角度考慮,for 是要比 forEach 有優勢的。 替代方法是 filter、some等專用方法。

遍歷對象性能分析

遍歷對象,之前用for in,我現在一般用Object.keys來獲取值數組。再來遍歷對象。他們的性能對比如何?

{ name: 'Object.keys.map', value: 21 },

{ name: 'forIn', value: 30 }

Object.keys來遍歷對象,也比for in 要快

數組測試代碼

const size = 10000000;

let times = [];
{
  const arrFor = new Array(size).fill(1);
  let timeFor = +new Date();
  console.time('arrFor');
  for (let i = 0;i < arrFor.length;i++) {
    const b = arrFor[i];
    //
  }
  console.timeEnd('arrFor');
  timeFor = new Date().getTime() - timeFor;
  times.push({ name: 'time-For', value: timeFor });
}

{
  const arrWhile = new Array(size).fill(1);
  let timeWhile = +new Date();
  console.time('timeWhile');
  let i = arrWhile.length - 1;
  while (i > -1) {
    const b = arrWhile[i];
    i--;
  }
  console.timeEnd('timeWhile');
  timeWhile = new Date().getTime() - timeWhile;
  times.push({ name: 'time-While', value: timeWhile });
}

{
  const arrForOf = new Array(size).fill(1);
  let timeForOf = +new Date();
  console.time('timeForOf');
  for (const item of arrForOf) {

  }
  console.timeEnd('timeForOf');
  timeForOf = new Date().getTime() - timeForOf;
  times.push({ name: 'time-ForOf', value: timeForOf });
}
{
  const arrForIn = new Array(size).fill(1);
  let timeForIn = +new Date();
  console.time('timeForIn');
  for (const key in arrForIn) {
    // 注意key不是
  }
  console.timeEnd('timeForIn');
  timeForIn = new Date().getTime() - timeForIn;
  times.push({ name: 'time-ForIn', value: timeForIn });
}
{
  const arrForEach = new Array(size).fill(1);
  let timeForEach = +new Date();
  console.time('timeForEach');
  arrForEach.forEach((item, index) => {

  });
  console.timeEnd('timeForEach');
  timeForEach = new Date().getTime() - timeForEach;
  times.push({ name: 'time-ForEach', value: timeForEach });
}
{
  const arrForMap = new Array(size).fill(1);
  let timeForMap = +new Date();
  console.time('timeForMap');
  arrForMap.map((item, index) => {

  });
  console.timeEnd('timeForMap');
  timeForMap = new Date().getTime() - timeForMap;
  times.push({ name: 'time-ForMap', value: timeForMap });
}
{
  const arrForEvery = new Array(size).fill(1);
  let timeForEvery = +new Date();
  console.time('timeForEvery');
  arrForEvery.every((item, index) => true);
  console.timeEnd('timeForEvery');
  timeForEvery = new Date().getTime() - timeForEvery;
  times.push({ name: 'time-ForEvery', value: timeForEvery });
}

{
  const arrForEvery = new Array(size).fill(1);
  let timeForEvery = +new Date();
  console.time('timeForSome');
  arrForEvery.some((item, index) => false);
  console.timeEnd('timeForSome');
  timeForEvery = new Date().getTime() - timeForEvery;
  times.push({ name: 'time-ForSome', value: timeForEvery });
}
{
  const arrForEvery = new Array(size).fill(1);
  let timeForEvery = +new Date();
  console.time('timeForFilter');
  arrForEvery.filter((item, index) => false);
  console.timeEnd('timeForFilter');
  timeForEvery = new Date().getTime() - timeForEvery;
  times.push({ name: 'time-ForFilter', value: timeForEvery });
}
times = times.sort((a, b) =>     a.value - b.value);
console.log(times);

不知道這個測試代碼是否可以改進。

foreach與map獲得一個新數組

const size = 10000000;

let times = [];

{
  const arrForEach = new Array(size).fill(1);
  let timeForEach = +new Date();
  console.time('timeForEach');
  const arr1 = [];
  arrForEach.forEach((item, index) => {
    arr1.push(item + 1);
  });
  console.timeEnd('timeForEach');
  timeForEach = new Date().getTime() - timeForEach;
  times.push({ name: 'time-ForEach', value: timeForEach });
}
{
  const arrForMap = new Array(size).fill(1);
  let timeForMap = +new Date();
  console.time('timeForMap');
  const arr1 = arrForMap.map((item, index) => item + 1);
  console.timeEnd('timeForMap');
  timeForMap = new Date().getTime() - timeForMap;
  times.push({ name: 'time-ForMap', value: timeForMap });
}
times = times.sort((a, b) =>     a.value - b.value);
console.log(times);

因為map直接返回了。foreach需要操作另外一個數組,造成性能損耗。我猜的哈。

for變量提前聲明與while性能對比

const size = 10000000;

let times = [];
{
  const arrFor = new Array(size).fill(1);
  let timeFor = +new Date();
  console.time('arrFor0');
  for (let i = 0 ;i < arrFor.length;i++) {
    const b = arrFor[i];
    //
  }
  console.timeEnd('arrFor0');
  timeFor = new Date().getTime() - timeFor;
  times.push({ name: 'time-For0', value: timeFor });
}
{
  const arrFor = new Array(size).fill(1);
  let timeFor = +new Date();
  console.time('arrFor');
  for (let i = size - 1;i > -1;i--) {
    const b = arrFor[i];
    //
  }
  console.timeEnd('arrFor');
  timeFor = new Date().getTime() - timeFor;
  times.push({ name: 'time-For', value: timeFor });
}
{
  const arrFor = new Array(size).fill(1);
  let timeFor = +new Date();
  console.time('arrFor1');
  let i = 0;
  for (;i < size;i++) {
    const b = arrFor[i];
    //
  }
  console.timeEnd('arrFor1');
  timeFor = new Date().getTime() - timeFor;
  times.push({ name: 'time-For1', value: timeFor });
}
{
  const arrFor = new Array(size).fill(1);
  let timeFor = +new Date();
  console.time('arrFor2');
  let i = size - 1;
  for (;i > -1;i--) {
    const b = arrFor[i];
    //
  }
  console.timeEnd('arrFor2');
  timeFor = new Date().getTime() - timeFor;
  times.push({ name: 'time-For2', value: timeFor });
}
{
  const arrWhile = new Array(size).fill(1);
  let timeWhile = +new Date();
  console.time('timeWhile');
  let i = size - 1;
  while (i > -1) {
    const b = arrWhile[i];
    i--;
  }
  console.timeEnd('timeWhile');
  timeWhile = new Date().getTime() - timeWhile;
  times.push({ name: 'time-While', value: timeWhile });
}
times = times.sort((a, b) =>     a.value - b.value);
console.log(times);

測試結果:

{ name: 'time-For2', value: 11 },

{ name: 'time-While', value: 11 },

{ name: 'time-For', value: 14 },

{ name: 'time-For1', value: 14 },

{ name: 'time-For0', value: 18 }

對象測試代碼

const size = 100000;

let times = [];
{
  const arrFor = Array.from(new Array(size), (n, index) => [index, index + 1]);
  let timeFor = +new Date();
  const obj = Object.fromEntries(arrFor);
  console.time('forIn');
  for (const key in obj) {
    const item  = obj[key];
  }
  console.timeEnd('forIn');
  timeFor = new Date().getTime() - timeFor;
  times.push({ name: 'forIn', value: timeFor });
}
{
  const arrFor = Array.from(new Array(size), (n, index) => [index, index + 1]);
  let timeFor = +new Date();
  const obj = Object.fromEntries(arrFor);
  console.time('Object.keys.map');
  Object.keys(obj).map((key) => {
    const item = obj[key];
  });
  console.timeEnd('Object.keys.map');
  timeFor = new Date().getTime() - timeFor;
  times.push({ name: 'Object.keys.map', value: timeFor });
}
times = times.sort((a, b) =>     a.value - b.value);
console.log(times);

先這樣吧

后面再來整理一下。

 

參考文章:

Js中for in 和for of的區別 https://juejin.cn/post/6844903601261772808

for…in和for…of的用法與區別 https://segmentfault.com/a/1190000022348279

[JavaScript] for、forEach、for...of、for...in 的區別與比較 https://blog.csdn.net/csdn_yudong/article/details/85053698

for in 和 for of 的區別? https://zhuanlan.zhihu.com/p/282961866

百度前端面試題:for in 和 for of的區別詳解以及為for in的輸出順序 https://zhuanlan.zhihu.com/p/161892289

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM