JavaScript的对象遍历--每天进步一点点

113 阅读6分钟

前言

  • ECMAScript将对象的属性分为两种: 数据属性访问器属性。 每一种属性内部都有一些特性,这里我们只关注对象属性的[[Enumerable]]特征,它表示是否通过 for-in 循环返回属性,也可以理解为:是否可枚举

  • 根据具体的上下文环境的不同,我们又可以将属性分为:原型属性实例属性。原型属性是定义在对象的原型(prototype)中的属性,而实例属性一方面来自己构造函数中,然后就是构造函数实例化后添加的新属性。

属性的可枚举和不可枚举

  1. 对象内部的属性[[Enumerable]],为true则是可枚举属性。

  2. 对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true。但是对于通过 Object.defineProperty 等定义的属性,该标识值默认为 false。

    Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

    const object1 = {};
    
    Object.defineProperty(object1, 'property1', {
      value: 42,
      writable: false
    });
    
    object1.property1 = 77;
    // throws an error in strict mode
    
    console.log(object1.property1);
    // expected output: 42
    
    

    应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

  3. js中基本包装类型的原型属性是不可枚举的,如Object, Array, Number等。

  4. 判断是否是可枚举属性:

    • 使用 for...in 访问到的是,不能访问的是不可枚举。
    • propertyIsEnumerable(),判断此对象是否包含某个属性,并且这个属性是否可枚举。

遍历对象的方法

  1. for … in(常用) 循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。

    这里需要注意的是使用for-in返回的属性因各个浏览器厂商遵循的标准不一致导致对象属性遍历的顺序有可能不是当初构建时的顺序。

  2. Object.keys() 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。

    同 for..in 一样不能保证属性按对象原来的顺序输出。

    • ES5 环境,如果传入的参数不是一个对象,而是一个字符串,那么它会报 TypeError。在 ES6 环境,如果传入的是一个非对象参数,内部会对参数作一次强制对象转换,如果转换不成功会抛出 TypeError。
      // 在 ES5 环境
      Object.keys('xuxu'); // TypeError: "xuxu" is not an object
    
      // 在 ES6 环境
      Object.keys('xuxu'); // ["0", "1", "2", "4"]
    
    • Object.keys(obj).length 判断对象是否为空。

    • Object.values() 与keys一样,但返回属性值。

    • Object.entries() 与keys一样,但返回键值对数组,如 [ [key1, value1], [key2, value2], ..., [keyN, valueN] ]。

    1. 通过数组解构赋值方式访问键和值就非常方便。

      let [key, value] of Object.entries(obj)
      
    2. 普通对象要转换成 Map 时Object.entries() 就很有用,因为Object.entries() 返回的格式与Map构造函数接受的格式完全相同。

      Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

    3. 对象缺少数组存在的许多方法,例如 map 和 filter 等。如果我们想应用它们,那么我们可以使用 Object.entries,然后使用 Object.fromEntries。

  3. Object.getOwnPropertyNames(obj) 返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。

  4. Reflect.ownKeys(obj) 返回一个数组,包含对象自身的所有属性,不管属性名是Symbol,也不管是否可枚举。

    Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

对象属性的顺序

  1. JS 对象是简单的键值映射,因此,对象中属性的顺序是微不足道的。

  2. ES5和早期标准中,没有指定属性的顺序。

  3. ES6,属性的顺序是基于一个特殊的规则。上述两个方法:Object.getOwnPropertyNamesReflect.ownKeys 返回是按照这个顺序的。

    • 数字:当属性的类型时数字类型时,会按照数字的从大到小的顺序进行排序;
    • 字符串:当属性的类型是字符串时,会按照时间的先后顺序进行排序;
    • Symbol:当属性的类型是Symbol时,会按照时间的先后顺序进行排序。
  4. 如果需要有序集合,建议将数据存储到数组或Set中。

Array、Map、Set 迭代(iterable)类型的遍历

ES6引入了iterable类型,Array,Map,Set都属于iterable类型。

Map和Set对象

  1. 使用上面对象遍历方法遍历:

    let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']])
    
    for ... in // []
    Object.keys(m) // []
    Object.getOwnPropertyNames(m) // []
    Reflect.ownKeys(m) // []
    
    
  2. for...of 为ES6新增遍历方法。其可遍历所有具有 iterator 接口的数据结构。for...of循环内部调用的是数据结构的Symbol.iterator方法(generator函数)。Symbol.iterator方法返回的是一个遍历器,当用for-of去遍历的时候,自动调用里面的next方法。

    let s = new Set(['A', 'B', 'C'])
    for (let x of s) { // 遍历Set
     console.log(x)
    }
    // 'A', 'B', 'C'
    
    let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']])
    for (let x of m) { // 遍历Map
     console.log(x)
    }
    // [1, 'x'], [2, 'y'], [3, 'z']
    
  3. iterable内置的 forEach 方法:

    let s = new Set(['A', 'B', 'C'])
    s.forEach((value, key, set) => { // 遍历Set
      console.log(key, value)
    })
    // 'A','A'
    // 'B','B'
    // 'C','C'
    
    let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']])
    m.forEach((value, key, map) => { // 遍历Map
      console.log(key, value)
    })
    // 1, 'x'
    // 2, 'y'
    // 3, 'z'
    

数组

我的看法,数组是一个:key为下标,value为数组元素值的对象。

  1. 使用上面对象遍历方法遍历:

    let m =  ['A', 'B', 'C']
    
    for ... in // 0 1 2
    Object.keys(m) // ["0", "1", "2"]
    Object.getOwnPropertyNames(m) // ["0", "1", "2", "length"]
    Reflect.ownKeys(m) // ["0", "1", "2", "length"]
    
    
  2. 使用 for...in 遍历的问题

    • 扩展了原生的Array,那么扩展的属性输出
     let a = ['A', 'B', 'C'];
     a.name = 'Hello';
     for (let x in a) {
       console.log(x); // '0', '1', '2', 'name'
     }
    
    • for..in 遍历数组时下标类型是字符串。

    • 对于不存在的数组项的处理差异,对于数组来讲,我们知道如果将其length属性设置为大于数组项数的值,则新增的每一项都会取得undefined值。

     let colors = ['red', 'green', 'blue'];
     // 将数组长度变为10
     colors.length = 10;
     // 再添加一个元素的数组末尾
     colors.push('yellow');
    
     for (let i in colors) {
       console.log(i); // 0 1 2 10
     }
    
     for (let j = 0; j < colors.length; j++) {
       console.log(j); // 0 1 2 3 4 5 6 7 8 9 10
     }
    
  3. 可以使用forEach 和 for...of 来遍历。

参考文献

  1. 属性的可枚举性和所有权
  2. JS中的可枚举属性与不可枚举属性的学习以及扩展
  3. js可枚举属性和不可枚举属性
  4. JS中轻松遍历对象属性的几种方式
  5. JS遍历对象的方式
  6. js es6遍历对象的6种方法(应用中推荐前三种)
  7. JavaScript中in操作符(for..in)、Object.keys()和Object.getOwnPropertyNames()的区别
  8. Object.defineProperty()