首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >JS中的那些循环

JS中的那些循环

原创
作者头像
BLUSE
修改于 2022-11-21 15:00:57
修改于 2022-11-21 15:00:57
2.9K0
举报
文章被收录于专栏:前端web技术前端web技术

一、forEach

定义

一个函数, 数组的普通循环遍历, 并为每个数组元素执行一次传入的callback

代码语言:javascript
复制
/**
 * @param {*} element 当前处理元素
 * @param {number} index 当前处理元素索引
 * @param {*} array forEach操作的数组
 * @param {*} thisArg 执行回调函数时绑定的this, 对箭头函数无效
 * @returns {undefined}
 */
forEach((element, index, array) => { /* … */ }, thisArg)

特点

1、 副作用: forEach返回undefined, 函数本身不会直接改变调用它的对象, 但是可以在callback里面对原数组进行修改

2、 改变长度: 虽然callback可以修改原数组, 但遍历的范围在第一次调用时就会确定, 即在callback中对数组长度进行操作, 不影响本次遍历范围. 可以说其副作用取决于callback, 修改可以分为两种

a) 新增元素

在遍历过程中 callback 对 array 新增元素, 直接影响到了原数组; 不过遍历次数仍为 2次, 即最开始 a 数组的长度

代码语言:javascript
复制
const a = [1, 2];
a.forEach((v, i, array) => {
  array.push(a.length);
  console.log(`index ${i}: v-${v}, array-[${array}]`);
});
console.log(`a: [${a}]`)
// index 0: v-1, array-[1,2,2]
// index 1: v-2, array-[1,2,2,3]
// a: [1,2,2,3]

b) 减少元素

在遍历过程中 callback 某一刻弹出 array 第一个元素, 直接影响到了原数组; 虽然遍历范围不变, 仍为 4, 但因为数组长度减小了, 所以会按最新的数组顺序 [2,3,4] 进行遍历, 且无法遍历到之前最后一个索引 [3]

代码语言:javascript
复制
const a = [1, 2, 3, 4];
a.forEach((v, i, array) => {
  console.log(`index ${i}: v-${v}, array-[${array}]`);
  if (i === 1) {
    a.shift();
  }
});
console.log(`a: [${a}]`);
// index 0: v-1, array-[1,2,3,4]
// index 1: v-2, array-[1,2,3,4]
// index 2: v-4, array-[2,3,4]
// a: [2,3,4]

3、 改变元素值: 虽然callback的对长度的修改不影响遍历范围, 但如果在执行过程中, callback修改遍历初已定范围内的元素值, 则后续的遍历值会发生变化

在遍历中对数组已有值重新赋值, 可以看到访问内容已经改变

代码语言:javascript
复制
const a = [1, 2];
a.forEach((v, i, array) => {
  console.log(`index ${i}: v-${v}, array-[${array}]`);
  array[i + 1] = 'new';
});
console.log(`a: [${a}]`)
// index 0: v-1, array-[1,2]
// index 1: v-new, array-[1,new]
// a: [1,new,new]

4、 跳过未初始化值: 对于数组中未初始化的值, forEach会直接跳过, 但是不会改变遍历元素的索引值

代码语言:javascript
复制
[1, , , 4].forEach((v, i, array) => {
  console.log(`index ${i}: v-${v}, array-[${array}]`);
});
// index 0: v-1, array-[1,,,4]
// index 3: v-4, array-[1,,,4]

5、 不可中断: forEach一旦执行, 一般情况下无法停止, 除非是中途抛出异常, 然后捕获

代码语言:javascript
复制
(() => {
  [1, 2, 3].forEach((v, i, array) => {
    console.log(`index ${i}: v-${v}, array-[${array}]`);
    // 此处的返回无效, 既不中断循环, 也不跳出外层函数
    return;
  });
})();
// index 0: v-1, array-[1,2,3]
// index 1: v-2, array-[1,2,3]
// index 2: v-3, array-[1,2,3]

二、for...in

定义

语句表达式, 以任意顺序遍历一个对象中, 除 Symbol 以外的可枚举属性, 包括继承的可枚举属性

代码语言:javascript
复制
/**
 * variable 当前遍历的属性名
 * object 被遍历的对象
 */
for (variable in object) {
  /* ... */
}

特点

1、 可以遍历到自身属性和原型上的属性, for...in内可以通过 Object.getOwnPropertyNames(intance) 或 intance.hasOwnProperty(prop) 来判断是自身属性还是原型属性

代码语言:javascript
复制
class Child {
  constructor() {
    this.childA = 1;
    this.childFunc = () => 2; // 属性值为函数也可以被遍历
  }
}
Child.prototype.parentA = 3;
Child.prototype.parentFunc = () => 4;
const intance = new Child();
for (const prop in intance) {
  const logPrefix = intance.hasOwnProperty(prop) ? 'ownerprop' : 'prototype';
  console.log(`${logPrefix}: ${prop} = ${typeof intance[prop] === 'function' ? intance[prop]() : intance[prop]}`)
}
// ownerprop: childA = 1
// ownerprop: childFunc = 2
// prototype: parentA = 3
// prototype: parentFunc = 4

2、 只能遍历自身或原型上的非 Sysmbol 可枚举型属性

代码语言:javascript
复制
// 定义父类型, parentB为Symbol类型属性, parentC为非枚举属性
const parent = { parentA: 1, [Symbol('parentB')]: 2 };
Object.defineProperty(parent, 'parentC', { 
  value: 3, enumerable: false 
});
// 定义子类型, 父类型属性为其原型属性, childB为Symbol类型属性, childC为非枚举属性
const intance = Object.create(parent, { 
  childA: { value: 4, enumerable: true },
  [Symbol('childB')]: { value: 5, enumerable: true },
  childC: { value: 6, enumerable: false },
});
for (const prop in intance) {
  if (intance.hasOwnProperty(prop)) {
    console.log(`ownerprop: ${prop} = ${intance[prop]}`);
  } else {
    console.log(`prototype: ${prop} = ${intance[prop]}`);
  }
}
// ownerprop: childA = 4
// prototype: parentA = 1

3、 与Object.keys 和 Object.getOwnPropertyNames 的区别, 一张表格对比说明

可遍历属性类型

Symbol属性

自身属性

原型属性

不可枚举属性

for...in

否

是

是

否

Object.keys

否

是

否

否

Object.getOwnPropertyNames

否

是

否

是

4、 遍历过程中可以使用return、break、throw随时退出中断, 可以使用continue跳过某次循环

代码语言:javascript
复制
const intance = { a: 1, b: 2, c: 3};
for (const prop in intance) {
  if (prop === 'a') continue;
  console.log(`${prop} = ${intance[prop]}`);
  return; // 或break、throw
}
// b = 1

5、 遍历过程中改变对象属性, 对于遍历到属性前对其值的更改, 遍历过程中会实时更新; 对于遍历过程中新增加的属性, 不会再遍历到; 对于遍历到属性前删除的属性, 也不会再遍历到, 具体如下:

代码语言:javascript
复制
const intance = { a: 1, b: 2, c: 3 };
for (const prop in intance) {
  console.log(`${prop} = ${intance[prop]}`);
  if (prop === 'a') {
    intance.b = 22;
    intance.d = 4;
    delete intance.c;
  }
}
console.log(`new intance: ${JSON.stringify(intance)}`);
// a = 1
// b = 22
// new intance: {"a":1,"b":22,"d":4}

6、 不建议用于遍历数组, 虽然不会报错, 但是数组最好选择用for..of进行遍历. 遍历数组时是按索引进行遍历

代码语言:javascript
复制
const intance = [1, 2];
for (const prop in intance) {
  console.log(`intance[${prop}] = ${intance[prop]}`);
}
// intance[0] = 1
// intance[1] = 2

三、for...of

定义

语句表达式, 对可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)进行遍历

代码语言:javascript
复制
/**
 * variable 当前遍历的值
 * iterable 被遍历的可迭代对象
 */
for (variable of iterable) {
   /* ... */
}

特点

1、 必须为可迭代对象, 可以使用 typeof obj[Symbol.iterator] === ‘function’ 来进行迭代对象判断, 如果为非迭代对象, 会直接报错

代码语言:javascript
复制
for (const v of { a: 1 }) {
  console.log(v);
}
// TypeError: {(intermediate value)} is not iterable

2、 可遍历生成器

代码语言:javascript
复制
const iterator = function *() {
  yield 1;
  yield 2;
}();
console.log(iterator.next());
for (const v of iterator) {
  console.log(v);
}
// { value: 1, done: false }
// 2

3、 可遍历自定义迭代对象

代码语言:javascript
复制
const iterable = {
  [Symbol.iterator]() {
    return {
      i: 0,
      next() {
        if (this.i < 2) {
          return { value: this.i++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};
for (const v of iterable) {
  console.log(v);
}
// 0
// 1

4、 遍历过程中可以使用return、break、throw随时退出中断, 可以使用continue跳过某次循环

代码语言:javascript
复制
const iterable = [1, 2, 3];
for (const v of iterable) {
  if (v === 1) continue;
  console.log(v);
  return;
}
// 2

5、 在遍历过程中改变迭代对象长度, 会实时影响迭代流程. 遍历过程中, 后面的迭代会按照最新的数组值进行遍历, 已遍历过的索引, 不会再重新遍历一遍

a) 新增元素

代码语言:javascript
复制
const iterable = [1, 2];
for (const v of iterable) {
  if (v === 1) iterable.push(3);
  console.log(v);
}
// 1
// 2
// 3

b) 减少元素

代码语言:javascript
复制
const iterable = [1, 2, 3];
for (const v of iterable) {
  if (v === 1) iterable.shift();
  console.log(v);
}
// 1
// 3

四、其他循环

js中除了上述三种循环之外, 还有一下循环方式

1、 for语句

2、 do...while语句

3、 while语句

4、 map函数

5、 some函数

6、 every函数

以上不做详细介绍, 下面一张表格对他们进行不同维度的对比

五、循环对比

功能对比

类型

目标类型

是否可中断

返回值

forEach

函数

数组

否

undefined

for...in

表达式

非Symbol可枚举属性

是

无

for...of

表达式

对可迭代对象

是

无

map

函数

数组

否

新Array

some

函数

数组

否

Boolean

every

函数

数组

否

Boolean

for语句

表达式

数组

是

无

do...while

表达式

条件表达式

是

无

while语句

表达式

条件表达式

是

无

执行效率对比

借用这篇文章的测试思路, 创建一个大数组, 然后遍历该数组, 将值赋给新数组, 然后计算执行前后的时间差

代码语言:javascript
复制
const NUM = 1e7;
let arr = new Array(NUM).fill(1);

let arr1 =[];
console.time('for');
for (let i = 0; i < arr.length; i++) {
  arr1.push(arr[i])
}
console.timeEnd('for');

let arr2 =[];
console.time('缓存长度的for');
for (let i = 0, len = arr.length; i < len; i++ ){
  arr2.push(arr[i])
}
console.timeEnd('缓存长度的for');

let arr3 =[];
console.time('倒序for');
for (let i = arr.length-1; i >= 0; i--){
  arr3.push(arr[i])
}
console.timeEnd('倒序for');

let arr4 =[];
console.time('for...of');
for (let value of arr){
  arr4.push(value)
}
console.timeEnd('for...of');

let arr5 =[];
console.time('for...in');
for (let key in arr){
  arr5.push(arr[key])
}
console.timeEnd('for...in');

let arr6 =[];
console.time('forEach');
arr.forEach((value) => {
  arr6.push(value)
})
console.timeEnd('forEach');

let arr7 =[];
console.time('map');
arr.map((value) => {
  return arr7.push(value)
})
console.timeEnd('map');

本地测试环境: node v16.14.2

执行结果(耗时升序排列):

1、for: 254.181ms, 最简单的循环方式, 耗时最低

2、倒序for: 292.618ms

3、缓存长度的for: 343.922ms

4、forEach: 388.43ms, 执行函数时会存储函数执行堆栈, 执行效率比普通for略低

5、for...of: 471.445ms, 通过访问对象的迭代器进行循环

6、map: 549.118ms, 会对数组进行浅拷贝, 并返回新数组, 耗时较长

7、for...in: 2.222s, 耗时最长, 因为会访问到对象的原型上

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、forEach
    • 定义
    • 特点
  • 二、for...in
    • 定义
    • 特点
  • 三、for...of
    • 定义
    • 特点
  • 四、其他循环
  • 五、循环对比
    • 功能对比
    • 执行效率对比
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档