概述
在Javascript编程时,经常需要遍历对象的键、值,ES5提供了for...in用来遍历对象,然而其涉及对象属性的“可枚举属性”、原型链属性等,总会让人多少摸不着头脑。
本文将由Object对象本质探寻各种遍历对象的方法,并区分常用方法的特点。
本文所提的对象,特指Object的实例,不包含Set、Map、Array等数据集对象。
剥开Object的“伪装”
Javascript的对象,每一个属性都有其“属性描述符”,主要有两种形式:数据描述符和存取描述符。
可以通过 Object.getOwnPropertyDescriptor
与 Object.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.defineProperty
和Object.defineProperties
来修改(configurable
为true
的条件下)
详细内容可以参考: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至少有三个方面的因素:
- 属性是否可枚举,即其 enumerable属性描述符 的值;
- 属性的类型,是字符串类型、还是Symbol类型;
- 属性所属,包含原型,还是仅仅包含实例本身;
鉴于各方意见不一,并且现有的遍历方式可以满足,于是标准组没有将[Symbol.iterator]加入。
关于ES6迭代器、生成器的更多知识,可以参考:ES6中的迭代器(Iterator)和生成器(Generator)