深入JS对象的遍历

概述

在Javascript编程时,经常需要遍历对象的键、值,ES5提供了for...in用来遍历对象,然而其涉及对象属性的“可枚举属性”、原型链属性等,总会让人多少摸不着头脑。
本文将由Object对象本质探寻各种遍历对象的方法,并区分常用方法的特点。

本文所提的对象,特指Object的实例,不包含Set、Map、Array等数据集对象。

剥开Object的“伪装”

Javascript的对象,每一个属性都有其“属性描述符”,主要有两种形式:数据描述符存取描述符

可以通过 Object.getOwnPropertyDescriptorObject.getOwnPropertyDescriptors两个方法获取对象的属性描述符。
以下通过示例说明:

var obj = {
  name: '10',
  _age: 25,
  get age(){
    return this._age;
  },
  set age(age){
    if(age<1){
      throw new Error('Age must be more than 0');
    }else{
      this._age = age;
    }
  }
};

var des = Object.getOwnPropertyDescriptors(obj);
console.log(des);
/**
 * des: {
 *  name: {
 *    configurable: true,
 *    enumerable: true,
 *    value: "10",
 *    writable: true,
 *    __proto__: Object
 *  },
 *  _age: {
 *    configurable: true,
 *    enumerable: true,
 *    value: 25,
 *    writable: true,
 *    __proto__: Object
 *  },
 *  age: {
 *    configurable: true,
 *    enumerable: true,
 *    get: f age(),
 *    set: f age(age),
 *    __proto__: Object
 *  },
 *  __proto__: Object
 * }
*/

可以看到,

  • name、_age拥有 'configurable''enumerable''value''writable'四个属性描述符,统称数据描述符
  • age拥有'configurable''enumerable''get''set'四个属性描述符,统称存取描述符
configurable enumerable value writable get set
数据描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

对象的属性描述符,可以通过Object.definePropertyObject.defineProperties来修改(configurabletrue的条件下)
详细内容可以参考:MDN手册 Object.defineProperty

了解了这个之后,与今天主题相关的,也就是 'enumerable'这个属性描述符啦,其值为 true 时,我们称其为“可枚举的”,属性是否可枚举影响了我们在使用原生方法遍历对象时的结果,在本文后面,将详细说明。

掌握属性描述符,无论是对自己以后的代码编写,还是学习开源框架源码,都是十分基础而且重要的,在此处仅作介绍,并着重关注与本文主题相关的属性。

常用遍历方法

for..in..遍历

遍历自身及原型链上所有可枚举的属性

示例代码:

var Person = function({name='none', age=18, height=170}={}){
  this.name = name;
  this.age = age;
  this.height = height;
}

Person.prototype = {
  type: 'Animal'
}

var qiu = new Person()

// 将height属性设置为 不可枚举
Object.defineProperty(qiu, 'height', {
  enumerable: false
})

for(let n in qiu){
  console.log(n);
}

// output: name age type

如以上代码所示,使用for..in..遍历,会将对象自身及其原型链上的所有可枚举属性全部遍历出来。
而往往我们并不需要将原型链上的属性也遍历出来,因此常常需要如下处理:

for(let n in qiu){
  // 判断是否实例自身拥有的属性
  if(qiu.hasOwnProperty(n)){
    console.log(n)
  }
}

因为for..in..在执行的时候,还进行了原型链查找,当只需要遍历对象自身的时候,性能上会收到一定影响。

Object.keys遍历

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

示例代码:

var Person = function({name='none', age=18, height=170}={}){
  this.name = name;
  this.age = age;
  this.height = height;
}

Person.prototype = {
  type: 'Animal'
}

var qiu = new Person()

// 将height属性设置为 不可枚举
Object.defineProperty(qiu, 'height', {
  enumerable: false
})

var keys = Object.keys(qiu);
console.log(keys)
// output: ['name', 'age']

通过上述代码,我们可以看到,Object.keys仅遍历对象本身,并将所有可枚举的属性组合成一个数组返回。
在很多情况下,其实我们需要的,也就是这样一个功能。
例如以下,将键值类型的查询param转换成url的query,不仅代码量少、逻辑清晰,而且可以通过链式的写法使得整体更加优雅。

const searchObj = {
  title: 'javascript',
  author: 'Nicolas',
  publishing: "O'RELLY",
  language: 'cn'
}
let searchStr = Object.keys(searchObj)
                .map(item => `${item}=${searchObj[item]}`)
                .join('&');
let url = `localhost:8080/api/test?${searchStr}`

遍历键值对的数据时,使用Object.keys真是不二之选。

Object.getOwnPropertyNames遍历

返回一个数组,包含对象自身(不含继承)的所有属性名

示例代码:

var Person = function({name='none', age=18, height=170}={}){
  this.name = name;
  this.age = age;
  this.height = height;
}

Person.prototype = {
  type: 'Animal'
}

var qiu = new Person()

// 将height属性设置为 不可枚举
Object.defineProperty(qiu, 'height', {
  enumerable: false
})

var keys = Object.getOwnPropertyNames(qiu);
console.log(keys)
// output: ['name', 'age', 'height']

与Object.keys的区别在于Object.getOwnPropertyNames会把不可枚举的属性也返回。除此之外,与Object.keys的表现一致。

说好的for..of..,为什么无效

在ES6中新增了迭代器与for..of..的循环语法,在数组遍历、Set、Map的遍历上,十分方便。然而当我应用在对象(特指Object的实例 )上时(如下代码),浏览器给我抛了一个异常:Uncaught TypeError: searchObj is not iterable

const searchObj = {
  title: 'javascript',
  author: 'Nicolas',
  publishing: "O'RELLY",
  language: 'cn'
}

for(let n of searchObj){
  console.log(n)
}
// Uncaught TypeError: searchObj is not iterable

没错...这是一个错误的演示,在ES6中,对象默认下并不是可迭代对象,表现为其没有[Symbol.iterator]属性,可以通过以下代码对比:

const searchObj = {
  title: 'javascript',
  author: 'Nicolas'
};
const bookList = ['javascript', 'java', 'c++'];
const nameSet = new Set(['Peter', 'Anna', 'Sue']);

console.log(searchObj[Symbol.iterator]); // undefined
console.log(bookList[Symbol.iterator]); // function values(){[native code]}
console.log(nameSet[Symbol.iterator]); // function values(){[native code]}

// 注,Set、Map、Array的[Symbol.iterator]都是其原型对象上的方法,而非实例上的,这点需要注意

而for..of..循环,实际上是依次将迭代器(或任何可迭代的对象,如生成器函数)的值赋予指定变量并进行循环的语法,当对象没有默认迭代器的时候,当然不可以进行循环,而通过给对象增加一个默认迭代器,即[Symbol.iterator]属性,就可以实现,如下代码:

Object.prototype[Symbol.iterator] = function *keys(){
  for(let n of Object.keys(this)){ // 此处使用Object.keys获取可枚举的所有属性
    yield n
  }
}

const searchObj = {
  title: 'javascript',
  author: 'Nicolas',
  publishing: "O'RELLY",
  language: 'cn',
};

for(let key of searchObj){
  console.log(key)
}
// output: title author publishing language

以上代码确实获得了对象的所有键名,在生成器函数内,我们使用的是Object.keys获得所有可枚举的属性值,然而这并不是所有人都期望的,也许小明期望不可枚举的属性值也被遍历,而小新可能连[Symbol.iterator]也希望遍历出来,于是,这里产生了一些分歧,如何遍历有以下几种因素:
总结起来,对象的property至少有三个方面的因素:

  1. 属性是否可枚举,即其 enumerable属性描述符 的值;
  2. 属性的类型,是字符串类型、还是Symbol类型;
  3. 属性所属,包含原型,还是仅仅包含实例本身;

鉴于各方意见不一,并且现有的遍历方式可以满足,于是标准组没有将[Symbol.iterator]加入。
关于ES6迭代器、生成器的更多知识,可以参考:ES6中的迭代器(Iterator)和生成器(Generator)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容