【面试题】对JavaScript中for in与for of的理解

简介: 【面试题】对JavaScript中for in与for of的理解

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!

for in

MDN描述:

for...in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

在没有深度了解过for in的作用前,我只停留在它能够遍历对象键和数组下标的的层面上,那现在就来探究什么是一个对象中的Symbol类型键(ES6之后新加的)和可枚举属性

JS中的对象类型包括Object,Array,Function,Date,Math....

本文主要用Object和Array来进行实践

首先,先来看一段代码:

const arr = [1, 2, 3, 4, 5];
const obj = {
  mark: "mark-v",
  jack: "jack-v",
  amy: "amy-v",
};
for (const i in arr) {  // 输出 0 1 2 3 4
  console.log(i);
}
for (const i in obj) { // 输出 mark jack amy
  console.log(i);
}

想必大家都很清楚输出的结果,如果是遍历数组则输出数组对应的下标,如果是遍历对象则输出对象的键名

继续加菜,再看下面的代码:

const arr = [1, 2, 3, 4, 5];
arr.value = "array"; // 给数组对象上添加一个value属性
const s1 = Symbol("symbol1"); // 定义两个symbo类型的值
const s2 = Symbol("symbol2");
const obj = {
  mark: "mark-v",
  jack: "jack-v",
  amy: "amy-v",
  [s1]: "symbol1-v", // 给obj初始化这两个symbol键值
  [s2]: "symbol2-v",
};
// 给obj原型上添加一个属性
Object.getPrototypeOf(obj).myProto = 'proto'
console.log(obj);
// 打印 {
//  mark: 'mark-v',
//  jack: 'jack-v',
//  amy: 'amy-v',
//  [Symbol(symbol1)]: 'symbol1-v',
//  [Symbol(symbol2)]: 'symbol2-v'
// }
for (const i in arr) {
  console.log(i); // 输出 0 1 2 3 4 value myProto
}
for (const i in obj) {
  console.log(i); // 输出 mark jack amy myProto
}

从上面输出结果可以看出,无论是数组元素或者对象上的键值,都是可枚举属性,并且可以看出Symbol类型的键,虽然定义在了对象中,但是for in会跳过Symbol类型键的遍历

并且for in还会往原型链上遍历,那为什么遍历obj和arr都会输出 'myProto' 呢,那是因为obj是一个对象,默认的构造函数是Object(),而arr是一个数组对象,默认的构造函数是Array(),但是Array的prototype原型属性本质上也是一个对象,所以Array原型的原型就是Object上的prototype属性

Object.getPrototypeOf(obj) === Object.getPrototypeOf(Object.getPrototypeOf(arr)) // true

所以arr和obj的原型链是有交点了,也就解释了他们都能遍历到myProto

我们也可以单纯认为一个对象中的Symbol类型的键对应的属性不是一个可枚举属性,但是其实它依旧是一个可枚举属性,只是被跳过了而已,这段话说起来有点绕,不过往下看就清楚了。

既然for in的遍历绕不开可枚举属性,那什么是可枚举属性呢,MDN的解释如下:

可枚举属性是指那些内部“可枚举”标志设置为 true 的属性,对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true,对于通过 Object.defineProperty 等定义的属性,该标识值默认为 false。可枚举的属性可以通过 for...in 循环进行遍历(除非该属性名是一个 Symbol)。属性的所有权是通过判断该属性是否直接属于某个对象决定的,而不是通过原型链继承的。

从上面可以得出,

  1. 可枚举属性是对象内部那些“可枚举”标志设置为true的属性
  2. 通过字面量定义的属性,可枚举标识默认为true,对于通过Object.defineProperty等定义的属性,该标识默认为flase
  3. for in循环只会对对象中的可枚举属性进行遍历,并且跳过属性名是Symbol类型的属性
  4. for in只会遍历对象本身上面的可枚举属性

请看如下代码:

const s1 = Symbol("symbol1");
const s2 = Symbol("symbol2");
// 以下字面量初始化和赋值的属性可枚举标识都为true
const obj = {          // 方式一
  mark: "mark-v",
};
obj.jack = "jack-v"     // 方式二
obj['amy'] = 'amy-v'    // 方式三
Object.defineProperty(obj, "kong", {
  value: "kong-v",
  enumerable: false,   // 可枚举标识,默认为 false
});
Object.defineProperty(obj, "john", {
  value: "john-v",
  enumerable: true,   // 设置为 true
});
// 通过Object.defineProperty设置Symbol类型键
Object.defineProperty(obj, s1, {
  value: "s1-v",
  enumerable: false,   
});
// 通过Object.defineProperty设置Symbol类型键,并且可枚举标识为true
Object.defineProperty(obj, s2, {
  value: "s2-v",
  enumerable: true,
});
console.log(obj);
// 打印 {
//   mark: 'mark-v',
//   jack: 'jack-v',
//   amy: 'amy-v',
//   john: 'john-v',
//   [Symbol(symbol2)]: 's2-v'
// }
for (const i in obj) {
  console.log(i);     // 输出 mark jack amy john
}

上面代码我们首先是创建了一个obj对象,有一个初始化属性mark,然后通过字面量方式定义了两个新属性,并且通过Object.defineProperty定义了2个string类型和2个symbol类型的键值,都各有一个可枚举和不可枚举标识

可以看到console.log打印obj,只包含了可枚举的属性,也就是说可枚举标识为false的属性是不会出现在整个obj对象的打印内容中,也就验证了就算Symbol类型键的属性可枚举标识为true,也是会被for in跳过。

额外拓展

const arr = [1, 2, 3, 4, 5];
// 设置arr对象属性0的值为100,不可枚举,不可修改
Object.defineProperty(arr, 0, {
  value: 100,
  enumerable: false,
  writable: false,
});
arr[0] = 1 // 尝试修改下标0的值
console.log(arr);  // 打印 [ 100, 2, 3, 4, 5 ]
for (const i in arr) {
  console.log(i);    // 输出 1 2 3 4
}

可以看到数组的下标也是数组对象的属性,设置了下标0不可枚举和不可修改后,for in遍历不到下标0,并且也修改不了下标0的值,但是好玩的是打印数组还是会完整打印出每个元素。

for of

MDN描述:

for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

for of是ES6新出的一个用于遍历可迭代对象(实现了Iterator接口)的语法

示例

// 迭代数组
const array = [10, 20, 30];
for (const value of array) {
    console.log(value); // 输出 10 20 30
}
// 迭代String
const string = 'kong'
for (const s of string) {
    console.log(s); // 输出 k o n g
}
// 迭代map
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (const item of map) {
  console.log(item);  // 输出  [ 'a', 1 ]  [ 'b', 2 ]  [ 'c', 3 ]
}
// 迭代set
const set = new Set([1, 1, 2, 2, 3, 3]);
for (let item of set) {
  console.log(item);  // 输出 1 2 3
}

关闭迭代器

对于for...of的循环,可以由 break, throwreturn 终止。在这些情况下,迭代器关闭。

return情况

const array = [10, 20, 30];
for (const value of array) {
    console.log(value); // 输出 10
    return
}
// 下面代码不会执行,前面已经return
const string = 'kong'
for (const s of string) {
    console.log(s); 
}

throw情况

const array = [10, 20, 30];
for (const value of array) {
  console.log(value); // 输出 10
  throw new Error()
}
// 不执行下面代码,上面已经抛错
const string = "kong";
for (const s of string) {
  console.log(s); 
}

对于for...in的循环,以上的中断方式也适用

总结

  • for...in适用于对象上面可枚举属性的遍历,并且只遍历非Symbol类型键以及对象自身以及原型链上的可枚举属性
  • for...of适用于实现了Iterator接口的对象(也称作可迭代对象)的遍历,其遍历方式由自身实现,例如对于数组是遍历其每个下标对应元素的值,对于Map遍历值为键值对组成的数组

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!

相关文章
|
4月前
|
JavaScript 前端开发
每天一道面试题——JavaScript的this指向【一】
每天一道面试题——JavaScript的this指向【一】
71 0
|
5月前
|
缓存 JavaScript 前端开发
js高频面试题,整理好咯
中级前端面试题,不低于12k,整理的是js较高频知识点,可能不够完善,大家有兴趣可以留言补充,我会逐步完善,若发现哪里有错,还请多多斧正。
|
5月前
|
JavaScript
JS【详解】setTimeout 延时(含清除 setTimeout,计时开始时间,0 秒延时解析,多 setTimeout 执行顺序,setTimeout 应用场景,网红面试题)
JS【详解】setTimeout 延时(含清除 setTimeout,计时开始时间,0 秒延时解析,多 setTimeout 执行顺序,setTimeout 应用场景,网红面试题)
1164 0
|
5月前
|
JavaScript 前端开发
JS进阶篇(前端面试题整合)(三)
JS进阶篇(前端面试题整合)(三)
44 0
|
7月前
|
JavaScript
分享经典面试题:JS数组去重的多种方法
分享经典面试题:JS数组去重的多种方法
|
7月前
|
JavaScript 前端开发 程序员
web前端入门到实战:32道常见的js面试题(1),面试哪些
web前端入门到实战:32道常见的js面试题(1),面试哪些
|
7月前
|
前端开发 JavaScript
web前端JS高阶面试题,2024我的前端大厂面试之旅
web前端JS高阶面试题,2024我的前端大厂面试之旅
|
7月前
|
JavaScript 前端开发
web前端JS高阶面试题(1),高级开发工程师面试
web前端JS高阶面试题(1),高级开发工程师面试
|
7月前
|
JSON JavaScript 前端开发
web前端入门到实战:32道常见的js面试题,2024年最新秋招是直接面试吗
web前端入门到实战:32道常见的js面试题,2024年最新秋招是直接面试吗
|
7月前
|
缓存 前端开发 JavaScript
Javascript模块化开发基础,最新美团点评前端团队面试题
Javascript模块化开发基础,最新美团点评前端团队面试题