一个函数, 数组的普通循环遍历, 并为每个数组元素执行一次传入的callback
/**
* @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 数组的长度
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]
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修改遍历初已定范围内的元素值, 则后续的遍历值会发生变化
在遍历中对数组已有值重新赋值, 可以看到访问内容已经改变
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会直接跳过, 但是不会改变遍历元素的索引值
[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一旦执行, 一般情况下无法停止, 除非是中途抛出异常, 然后捕获
(() => {
[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]
语句表达式, 以任意顺序遍历一个对象中, 除 Symbol 以外的可枚举属性, 包括继承的可枚举属性
/**
* variable 当前遍历的属性名
* object 被遍历的对象
*/
for (variable in object) {
/* ... */
}
1、 可以遍历到自身属性和原型上的属性, for...in内可以通过 Object.getOwnPropertyNames(intance) 或 intance.hasOwnProperty(prop) 来判断是自身属性还是原型属性
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 可枚举型属性
// 定义父类型, 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跳过某次循环
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、 遍历过程中改变对象属性, 对于遍历到属性前对其值的更改, 遍历过程中会实时更新; 对于遍历过程中新增加的属性, 不会再遍历到; 对于遍历到属性前删除的属性, 也不会再遍历到, 具体如下:
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进行遍历. 遍历数组时是按索引进行遍历
const intance = [1, 2];
for (const prop in intance) {
console.log(`intance[${prop}] = ${intance[prop]}`);
}
// intance[0] = 1
// intance[1] = 2
语句表达式, 对可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)进行遍历
/**
* variable 当前遍历的值
* iterable 被遍历的可迭代对象
*/
for (variable of iterable) {
/* ... */
}
1、 必须为可迭代对象, 可以使用 typeof obj[Symbol.iterator] === ‘function’ 来进行迭代对象判断, 如果为非迭代对象, 会直接报错
for (const v of { a: 1 }) {
console.log(v);
}
// TypeError: {(intermediate value)} is not iterable
2、 可遍历生成器
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、 可遍历自定义迭代对象
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跳过某次循环
const iterable = [1, 2, 3];
for (const v of iterable) {
if (v === 1) continue;
console.log(v);
return;
}
// 2
5、 在遍历过程中改变迭代对象长度, 会实时影响迭代流程. 遍历过程中, 后面的迭代会按照最新的数组值进行遍历, 已遍历过的索引, 不会再重新遍历一遍
a) 新增元素
const iterable = [1, 2];
for (const v of iterable) {
if (v === 1) iterable.push(3);
console.log(v);
}
// 1
// 2
// 3
b) 减少元素
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语句 | 表达式 | 条件表达式 | 是 | 无 |
借用这篇文章的测试思路, 创建一个大数组, 然后遍历该数组, 将值赋给新数组, 然后计算执行前后的时间差
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 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。