今日前端小知识——遍历对象的方法

遍历对象的 5 种方法

  • for...in...
  • Object.entries()/Object.keys()/Object.values()
  • Object.getOwnPropertyNames(targetObj)
  • Object.getOwnPropertySymbols(targetObj)
  • Reflect.ownKeys(targetObj)
  • 总结
  • 补充知识点
    • 可迭代对象
    • 可枚举属性
    • Object.defineProperty()

对象是 JavaScript 中一种非常常用的数据类型,由一个个属性名和属性值一 一对应的键值对组成。在实际开发中,我们经常需要遍历对象去获取我们需要的属性或进行其他操作,看了各路大佬撰写的技术博客以及查阅 MDN 之后,想自己也记录一下这 5 种常用的遍历对象的方法。首先定义一个对象,如下。

// 以对象 { protokey: 'proto-key' } 作为原型创建一个新对象
let obj = Object.create({
  protoKey: 'proto-key'
});
// 给新对象自身添加属性
obj.user = 'ostuthere';
obj.age = 20;
obj.gender = 'female';
// 添加Symbol属性
obj[Symbol('mother')] = 'user1';
obj[Symbol('father')] = 'user2';
// 添加不可枚举属性
Object.defineProperty(obj, 'key1', {
  value: 'non-enumerable',
  writable: true,
  enumerable: false
});
console.log(obj);

查看控制台打印,这个简单的对象拥有三个可枚举属性,依次名为 user、age、gender;两个Symbol 属性,分别为 mother、father;一个不可枚举属性,名为 key1。原型上还有一个属性,名为 protoKey。

今日前端小知识——遍历对象的方法_第1张图片

for…in…

for…in… 会以任意顺序遍历出对象自身以及原型链上的可枚举属性(不包括 Symbol 属性)。代码如下。

for (let key in obj) {
  console.log(key);
}
// 结果依次打印出 user age gender protokey

我们发现 for…in… 不仅遍历出了对象自身的三个可枚举属性,还将其原型上的可枚举属性也遍历了出来。如果我只想拿到对象自身的可枚举属性该怎么办呢?这时候 hasOwnProperty() 方法登场了,它能判断对象的自身属性中是否存在指定的属性,若是则返回 true,否则返回 false。我们可以通过 for…in… + hasOwnProperty() 获取到对象本身的可枚举属性,代码如下。

for (let key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key);
  }
}
// 结果依次打印出 user age gender

Object.entries()/Object.keys()/Object.values()

Object.entries(targetObj) 是 ES5 中增加的对象方法,该方法返回一个数组,由对象自身所有可枚举属性(不包括 Symbol 属性)的键值对组成,Object.keys(targetObj)、Object.values(targetObj) 同理,不过返回的数组分别由属性名、属性值组成。由于返回的是数组,所以我们可以使用数组的方法对结果进行遍历获取需要的值,代码如下。

Object.entries(obj).forEach((item)=>{
  console.log(item);
});
// 结果依次打印出 ['user', 'ostuthere'] ['age', 20] ['gender', 'female']
Object.keys(obj).forEach((key)=>{
  console.log(key);
});
// 结果依次打印出 user age gender
Object.values(obj).forEach((value)=>{
  console.log(value);
});
// 结果依次打印出 ostuthere 20 female

Object.getOwnPropertyNames(targetObj)

Object.getOwnPropertyNames(targetObj) 也是 ES5 中增加的对象方法,该方法返回一个数组,由对象自身所有属性(包括不可枚举属性,但不包括 Symbol 属性)属性名组成。同样的,由于返回的是数组,我们可以对返回结果使用数组方法进行遍历或其他操作。

Object.getOwnPropertyNames(obj).forEach((value)=>{
  console.log(value);
});
// 结果依次打印出 user age gender key1

我们还可以只获取不可枚举的属性,通过 propertyIsEnumerable() 方法可以判断指定的属性是否可枚举,若是则返回 true,否则返回 false。数组的 filter(function(){}) 方法返回一个数组,由所有通过测试函数测试的元素组成。

let nonenum_only = Object.getOwnPropertyNames(obj).filter((value)=>{
  return !obj.propertyIsEnumerable(value);
});
console.log(nonenum_only);
// ['key1']

Object.getOwnPropertySymbols(targetObj)

ES6 中新增了 Symbol 基本数据类型,可以用来标识对象的属性,相应地增加了一个对象方法来获取 Symbol 属性 —— Object.getOwnPropertySymbols(targetObj) 。与上个方法类似,但该方法返回的数组由对象自身所有 Symbol 属性属性名组成。若对象上不存在 Symbol 属性,该方法返回一个空数组。

Object.getOwnPropertySymbols(obj).forEach((value)=>{
  console.log(value);
});
// [Symbol(mother), Symbol(father)]

Reflect.ownKeys(targetObj)

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。与大多数全局对象不同,Reflect 不是一个构造函数,所以不能通过 new 运算符对其进行调用,也不允许将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像Math对象),其中的方法 Reflect.ownKeys(targetObj) 返回一个数组,由对象自身所有属性名(包括 Symbol 属性和不可枚举属性)组成,它的返回值等同于Object.getOwnPropertyNames(targetObj).concat(Object.getOwnPropertySymbols(targetObj))。

Reflect.ownKeys(obj).forEach((value)=>{
  console.log(value);
});
// 结果依次打印出 user age gender key1 Symbol(mother) Symbol(father)

总结

下面通过表格列出上述 5 种方法是否可以遍历出目标对象中的基本属性、不可枚举属性、Symbol 属性以及其原型链中的属性。

遍历方式 基本属性 不可枚举属性 Symbol 属性 原型链上的属性
for…in
Object.keys()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Reflect.ownKeys()

补充知识点

可迭代对象

我们知道还有一种循环 —— for…of 也可以进行遍历,但是它只能对可迭代对象进行遍历,不能遍历普通对象。当时遇到这个知识点是硬背下来的,一直很疑惑到底什么是可迭代对象?看看 MDN 上的解释

一个对象要成为可迭代对象, 必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性,它是一个无参数的函数,其返回值是一个符合迭代器协议的对象

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。一个对象要成为迭代器,需要实现一个符合特定语义的 next() 方法,该方法是一个无参数或有一个传入参数的函数,返回值为一个对象,该对象拥有两个属性: value 和 done。如果迭代器可以产生序列中的下一个值,done 则为 false;如果迭代器已将序列迭代完毕,则为 true。value 是迭代器返回的任何 JavaScript 值,done 为 true 时可省略。

一些内置可迭代对象比如 String、Array、TypedArray、Map 和 Set 等,都有定义自己的 iterator 方法,而 Object 没有定义,所以它不是可迭代对象,也就不能通过 for…of 遍历啦!我们也可以自己实现一个可迭代对象,如下

let rangeNum = {
  start: 0,
  end: 6,
  [Symbol.iterator]: function(){
    return {
      now: this.start,
      to: this.end,
      next: function(){
        if (this.now <= this.to) {
          return {done: false, value: this.now++};
        } else {
          return {done: true};
        }
      }
    }
  }
}
for (let value of rangeNum) {
  console.log(value);
}
// 依次打印 0 1 2 3 4 5 6

可枚举属性

可枚举属性是指那些内部 “可枚举” 标志即 enumerable = true 的属性。对于通过直接的赋值和属性初始化的属性,该标识的值默认为 true;对于通过 Object.defineProperty() 等方法定义的属性,该标识的值默认为 false。可枚举的属性可以通过 for…in 循环以及 Object.keys() 方法进行遍历获取(除非该属性名是一个 Symbol)

Object.defineProperty()

通常情况下,直接通过赋值运算符添加的对象属性都是可枚举的,可以对这些属性进行修改、删除的操作。有时候我们想更精确地控制或修改一个对象中属性的额外配置,Object.defineProperty() 登场了,该方法会直接在一个对象上定义一个新属性或者修改一个对象的现有属性,并返回此对象。语法如下。

Object.defineProperty(obj, property, descriptor)
// obj 是要对其属性进行操作的对象
// property 是要添加或修改的属性名或 Symbol
// descriptor 是要定义或修改的属性描述符

这里产生了一个疑惑,什么是属性描述符?在 JavaScript 中,对象的属性也可以用一些关键字来修饰,表示当前属性是否可写、是否有默认值、是否可枚举等,这些关键字就是属性描述符,它其实就是一个内部对象,用来描述对象的属性的特性。可以理解为给属性加了一层权限,规定能做什么不能做什么。
目前属性描述符分为两种:数据描述符与存取描述符。属性描述符只能是这二者其一,不能同时是两种类型。这两种描述符有各自可配置的属性,也有共享的可配置属性。各个描述符键值具体介绍如下(默认值指的是使用 Object.defineProperty() 方法时)

configurable :表示属性是否能被删除以及 value 和 writable 之外的键值是否能被修改
enumerable :表示属性是否可枚举(能否被 for…in 和 Object.keys() 遍历出来)
writable :表示属性的值是否可通过赋值运算符修改
value :属性的值,可以是任意 js 数据类型
get:定义属性的 getter 函数,属性被访问时触发,返回值作为属性的值
set:定义属性的 setter 函数,属性被修改时触发,接收一个参数作为属性的新值
注意:configurable: false 情况下不允许修改属性描述符,但如果 writable: true,属性值仍然可修改

键值 默认值 数据描述符 存取描述符
configurable false
enumerable false
writable false
value undefined
get undefined
set undefined

如果一个描述符有 value 或 writable,且不具有 get 或 set,或者这4个都没有,那么它就是一个数据描述符
如果一个描述符有 get 或 set,且不具有 value 或 writable,那么它就是一个存取描述符
如果数据描述符和存取描述符的键同时存在,浏览器会报错:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute

接下来实践一下,通过 Object.defineProperty() 给对象添加一些有特定描述符的属性看看效果。此外我们可以通过 Object.getOwnPropertyDescriptor() 方法查看指定对象上一个自有属性对应的属性描述符。先定义一个对象,通过点操作符添加一个属性,如下。

let obj_test = {};
obj.first_key = 'first key';
// 直接通过点操作符添加的属性相当于下面的操作
/* Object.defineProperty(obj, 'first_key', {
  value: 'first key',
  configurable: true,
  enumerable: true,
  writable: true
}); */
Object.getOwnPropertyDescriptor(obj, 'first_key');
// {value: 'first key', writable: true, enumerable: true, configurable: true}

接着通过 defineProperty 方法添加一个可配置、可枚举、可修改的 all_key 属性,如下。

Object.defineProperty(obj_test, 'all_key', {
  value: 'hello',
  configurable: true,
  enumerable: true,
  writable: true
});

// 可通过 Object.keys() 遍历出 all_key 属性
console.log(Object.keys(obj_test));	// ['first_key', 'all_key']

// 可成功修改 all_key 属性值
console.log(obj_test.all_key);	// 'hello'
obj_test.all_key = 'hello!ostuthere';
console.log(obj_test.all_key);	// 'hello!ostuthere'

// 可成功删除 all_key 属性
delete obj_test.all_key;
console.log(Object.keys(obj_test));	// ['first_key']
console.log(Object.getOwnPropertyNames(obj_test));	// ['first_key']

重新添加一个可配置、可枚举,但是不可修改的 all_key 属性,如下

Object.defineProperty(obj_test, 'all_key', {
  value: 'unmodifiable key',
  configurable: true,
  enumerable: true,
  writable: false
});

obj_test.all_key = 'hello';
console.log(obj_test.all_key);	// 'unmodifiable key'

Object.getOwnPropertyDescriptor(obj_test, 'all_key');
// {value: 'unmodifiable key', writable: false, enumerable: true, configurable: true}

将 all_key 属性修改为可配置、可修改,但是不可枚举,如下

// 修改 all_key 属性的属性描述符,注意:除非要使用默认值,否则都需要指明取值
Object.defineProperty(obj_test, 'all_key', {
  value: 'unenumreable key',
  configurable: true,
  enumerable: false,
  writable: true
});

// 无法通过 Object.keys() 遍历出 all_key 属性
console.log(Object.keys(obj_test));	// ['first_key']
// 但是通过 Object.getOwnPropertyNames() 可以遍历到
console.log(Object.getOwnPropertyNames(obj_test));	// ['first_key', 'all_key']

Object.getOwnPropertyDescriptor(obj_test, 'all_key');
// {value: 'unenumreable key', writable: true, enumerable: false, configurable: true}

接着修改为不可配置、可枚举、可修改的属性,如下

Object.defineProperty(obj_test, 'all_key', {
  value: 'unconfigurable key',
  configurable: false,
  enumerable: true,
  writable: true
});

// 可以通过 Object.keys() 遍历出 all_key 属性
console.log(Object.keys(obj_test));	// ['first_key']

// 可成功修改 all_key 属性值
console.log(obj_test.all_key);	// 'unconfigurable key'
obj_test.all_key = 'hello!';
console.log(obj_test.all_key);	// 'hello!'

// 无法通过 delete 操作符删除 all_key 属性
delete obj_test.all_key;
console.log(Object.keys(obj_test));	// ['first_key', 'all_key']

// 无法再次修改 all_key 属性的属性描述符
// 下面的操作在浏览器中会报错 TypeError: Cannot redefine property: all_key
Object.defineProperty(obj_test, 'all_key', {
  value: 'hello',
  configurable: true,
  enumerable: false,
  writable: false
});

Object.getOwnPropertyDescriptor(obj_test, 'all_key');
// {value: 'hello!', writable: true, enumerable: true, configurable: false}

通过上面的例子可以发现,在 configurable: false 情况下不允许修改属性描述符,也不允许删除该属性,但如果 writable: true,属性值仍然是可修改的。以上都是定义了属性的数据描述符,存取描述符又该如何使用呢?通过定义 get 或 set 指定 getter 或 setter 函数,当访问属性值时触发 getter 函数,修改属性值时触发 setter 函数。

// 定义一个 getter 函数
Object.defineProperty(obj_test, 'getter_key', {
  configurable: false,
  enumerable: true,
  get: function () {
    console.log('getting the getter_key!');
    return 'getter key';
  }
});

console.log(obj_test.getter_key);
// 获取 getter_key 属性值触发 getter 函数,执行函数中的 console.log,再将返回值赋给 getter_key 属性
// 先打印 'getting the getter_key!',接着打印 getter_key 的属性值 'getter key'


// 定义一个 setter 函数
obj_test.__setter_key = 'setter key'
Object.defineProperty(obj_test, 'setter_key', {
  configurable: false,
  enumerable: true,
  set: function (newValue) {
    console.log('setting the setter_key!');
    // this 指向当前被修改属性所属的对象即 obj_test
    this.__setter_key = newValue;
    console.log(obj_test.__setter_key);
  }
});

obj_test.setter_key = 'after setted setter_key';
// 设置 setter_key 属性值触发 setter 函数,执行函数中的 console.log,再将新值赋给 __setter_key 属性
// 先打印 'setting the setter_key!',接着打印 __setter_key 的属性值 'after setted setter_key'

get 和 set 可以同时设置,也可以只设置其中一个,不设置的话默认为 undefined。

你可能感兴趣的:(JavaScript,前端,javascript,js)