天天看點

JS周遊循環方法性能對比:for/while/for in/for of/map/foreach/e

這周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 是最快的。 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内部,為了有效地提升存儲和通路這兩種屬性的性能,分别使⽤了兩個 線性資料結構來分别儲存排序 屬性和正常屬性,具體結構如下圖所⽰:

對象中的數字屬性稱為 「排序屬性」,在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 隻可周遊可疊代對象,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');
  let i = 0
  for ( ;i < arrFor;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('arrFor');
  for (let i = arrFor.length - 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('arrFor2');
  let i = arrFor.length - 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 = 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 });
}
times = times.sort((a, b) =>     a.value - b.value);
console.log(times);      

測試結果:

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

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

{ name: 'time-For', 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

繼續閱讀