JavaScript对象的属性,看起来是一个很简单的问题,但是往细了想有很多细节需要注意。比如,可枚举属性与不可枚举属性的区别是什么,当我们打印一个对象的时候,打印的是对象本身的属性,还是包含继承的属性?我们去遍历一个对象的时候,多种方法之间又有什么区别?在这里我们做一个全面的分析和总结。
属性的设置
在搞清楚对象的属性之前,先看看我们一般如何设置一个对象的属性。总的来说有两种方法:
-
直接访问对象属性进行设置,这是最常用的
// 1.直接设置属性,无论通过点还是中括号访问,效果都是一样的 var obj = {}; obj.a = 1; obj['b'] = 2; // 2. 在构造函数中设置,本质上也是通过a访问。 function Person (a) { this.age = a; } var a = new Person(11)
-
通过
Object.defineProperty()
方法。Object.defineProperty
提供更强大的功能,他通过属性描述符来对属性进行更大的配置。属性描述符又分为数据描述符和存取描述符两类:var obj = {}; Object.defineProperty(obj, a, { /***数据描述符***/ // 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。 configurable: true, // 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。 enumerable: false, // 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined value: 1, // 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false writable: true, /***存取描述符***/ // 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行 get: function() { console.log('获取属性a的值') return 1; }, // 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。 set: function(val) { console.log('设置属性a的值') a = val;// 这里直接写属性名就行 } })
属性的分类
我们从3个维度来对JavaScript属性进行分类,这是后面进行分析的基础。
可枚举与不可枚举
js对象的属性分为可枚举属性和不可枚举属性。通过上面说的第一种方法设置的属性都是可枚举属性,在第二种方法中,如果将数据描述符enumerable
设置为false,则属性时不可枚举。对于大多数情况,我们使用的都是可枚举属性。
如果你去搜索可枚举和不可枚举属性的区别是什么,很多博客都是说会影响for...in
等方法的行为。其实说白了,这些都是可枚举属性和不可枚举属性在表现上的区别,是果,不是因。关于具体的区别,我们将在下面介绍API时具体分析,在这里我们只用记住,可枚举与不可枚举是怎么来的就行。
继承与自身
js对象是一大特点就是可继承性,所以我们常常提到原型链。如果属性设置在对象本身,我们就说这是自身属性,如果来自于原型链,我们就说这是继承属性。
function Parent() {
this.p1 = 'p1';
}
var parent = new Parent();
function Son () {
this.s1 = 's1';
}
Son.prototype = parent;
var son = new Son();
console.log(son.p1) // 继承属性
console.log(son.s1) // 自身属性
Symbol属性与非Symbol属性
ES6引入了Symbol,于是又给属性分类多添了一种。
var skey = Symbol.for('symbolKey');
function Son() {
this.s1 = 's1';// s1是非Symbol属性
this[skey] = 's3';// skey是Symbol属性
}
Symbol属性与非Symbol属性只是key值的不同而已。设置Symbol属性往往是为了更加安全,不轻易暴露出来。所以在访问上,一般来说Symbol属性隐藏更深。
8种分类与4种分类
从3个维度分类,每个维度有2种分类,所以排列组合一下,我们一共将js属性分为8类:
- 自身可枚举Symbol属性
- 自身可枚举非Symbol属性
- 自身不可枚举Symbol属性
- 自身不可枚举非Symbol属性
- 继承可枚举Symbol属性
- 继承可枚举非Symbol属性
- 继承不可枚举Symbol属性
- 继承不可枚举非Symbol属性
很复杂,有点多是不是。其实,Symbol属性只是key值的类型为Symbol而已,在一般情况下时作为私有属性,隐藏的更深,Symbol属性一般不会访问到。所以,为了简单起见,我们不单独考虑这种,默认都是访问不到的,只有在能访问到的时候才单独说明。所以,我们还是按照4种分类:
- 自身可枚举属性
- 自身不可枚举属性
- 继承可枚举属性
- 继承不可枚举属性
我们构造一个例子,包含上面这8种属性。下面的所有分析都在这个例子的基础上:
var skeyEnumerable = Symbol.for('skeyEnumerable');
var skeyNoEnumerable = Symbol.for('skeyNoEnumerable');
var pkeyEnumerable = Symbol.for('pkeyEnumerable');
var pkeyNoEnumerable = Symbol.for('pkeyNoEnumerable');
function Parent() {
this.p1 = 'p1';
this[pkeyEnumerable] = 'pSymbolEnumerable';
}
var parent = new Parent();
Object.defineProperty(parent, 'p2', {
enumerable: false,
value: 'p2'
});
Object.defineProperty(parent, pkeyNoEnumerable, {
enumerable: false,
value: 'pSymbolNoEnumerable'
});
function Son() {
this.s1 = 's1';
this[skeyEnumerable] = 'sSymbolEnumerable';
}
Son.prototype = parent;
var son = new Son();
Object.defineProperty(son, 's2', {
enumerable: false,
value: 's2'
});
Object.defineProperty(son, skeyNoEnumerable, {
enumerable: false,
value: 'sSymbolNoEnumerable'
});
在这个例子中:
- s1是自身可枚举属性
- s2是自身不可枚举属性
- p1是继承可枚举属性
- p2是继承不可枚举属性
除此之外,还有4个Symbol属性:
- skeyEnumerable是自身可枚举Symbol属性
- skeyNoEnumerable是自身不可枚举Symbol属性
- pkeyEnumerable是继承可枚举Symbol属性
- pSymbolNoEnumerable是继承不可枚举Symbole属性
为了简单起见,我们只考虑上面4种。下面的4中Symbol属性,只有在会展示的地方,才会特殊说明。
如何区分分类
根据上面说的两种分类,是解释来源,这里介绍如何鉴定区分。
区分是否可枚举
通过实例方法propertyIsEnumerable
来区分是否可枚举:
console.log(son.propertyIsEnumerable('s1'));// true
console.log(son.propertyIsEnumerable('s2'));// false
console.log(son.propertyIsEnumerable('p1'));// false
console.log(son.propertyIsEnumerable('p2'));// false
根据上面例子中的设置,p1是继承的可枚举属性,但是这里输出false。有点小遗憾,这个propertyIsEnumerable
方法只是对自身的属性才有用,对继承的属性统一输出为false,无论是可枚举还是不可枚举。
区分是否继承
通过实例方法hasOwnProperty
来区分是否为继承属性。
console.log(son.hasOwnProperty('s1'));// true
console.log(son.hasOwnProperty('s2'));// true
console.log(son.hasOwnProperty('p1'));// false
console.log(son.hasOwnProperty('p2'));// false
打印对象
直接打印
如果我们直接将son对象打印出来,我们会得到什么?这4中属性里面,有哪些属性会被打印访问到呢?
console.log(son);
// 在Chrome中,打印的结果是Son {s1: "s1", s2: "s2"}
// 在node中,打印的结果是Parent { s1: 's1' }
可以看出打印出来的结果并不一致。这是因为,我们常用的console.log
函数,并不是一个标准的方法,每个浏览器或引擎都有自己的一套实现方法,也并不是简单地调用对象的toString方法(如果是toString应该返回[object Object]),比如在Chrome中会打印可枚举和不可枚举,而在node中只会打印可枚举。需要提到的是,son.p1
和son.p2
是可以访问,只是这里没有打印出来。
至于Symbol属性,由于个平台实现不一致,有的能打印出来,有的不能,不多纠结。
JSON.stringify方法
虽然console.log
跨引擎实现是不一样的,但也不是很重要,毕竟这只是一个debug用的方法。换一种更通用的方法,通过JSON.stringfy序列化成字符换,最后的结果又是怎样呢
console.log(JSON.stringify(son)); // {"s1":"s1"}
可以看出,JSON.stringify
会将所有的自身可枚举属性序列化成字符串。
遍历对象属性
前面介绍这么多,都是基础。在实际应用中,我们最多的还是去遍历一个对象。接下来就把所有遍历对象属性的方法做一个分析比较。
遍历对象的所有属性,就要先得到该对象的所有属性名。只有先得到所有的属性名,才能遍历每个属性的值。JS提供了很多API供我们得到所有的属性名,往往是放在一个数组中,然后遍历数组中的每一个key,访问对象对应的key值。
1. Object.keys()
Object.keys(son).forEach(key => {
console.log(son[key]);// s1
});
该方法返回的是对象的所有自身可枚举属性。这跟JSON.stringify
其实是一样的。
2. Object.getOwnPropertyNames()
Object.getOwnPropertyNames(son).forEach(key => {
console.log(son[key]);// s1 s2
});
该方法返回的是对象自身的所有属性,包含可枚举属性和不可枚举属性。
3. Object.getOwnPropertySymbols()
前面提到,一般Symbol属性都是私有的,隐藏的。而通过该方法可以访问到对象自身的所有Symbol属性,包括自身可枚举的Symbol属性和自身不可枚举Symbol属性。
Object.getOwnPropertySymbols(son).forEach(key => {
console.log(son[key]); //sSymbolEnumerable sSymbolNoEnumerable
})
4. Reflect.ownKeys()
Reflect.ownKeys(son).forEach(key => {
console.log(son[key]); // sSymbolEnumerable sSymbolNoEnumerable s1 s2
})
这个方法就更强了,返回的是对象自身的所有属性,无论是否可枚举,也无论是否为Symbol属性,即包含:
- 自身可枚举Symbol属性
- 自身可枚举非Symbol属性
- 自身不可枚举Symbol属性
- 自身不可枚举非Symbol属性
到目前为止,介绍的几种方法都是访问对象自身的属性。Object.keys()
是最弱的,只能访问自身可枚举属性,而Object.getOwnPropertyNames()
就强一些,能访问自身可枚举和不可枚举属性;当然这些都不包含Symbol属性,需要使用Object.getOwnPropertySymbols
来访问。可以说,Obejct.getOwnPropertySymbols
和Object.getOwnPropertyNames
是并列关系,而Reflec.ownKeys=Object.getOwnPropertyNames + Object.getOwnPropertySymbols
。如下图所示:
5. for…in
for (var key in son) {
console.log(son[key]);// s1 p1
}
该方法访问的是对象的自身和继承的可枚举属性。
如果想用for…in
来访问对象自身的可枚举属性,应该怎么弄呢。结合前面说的hasOwnProperty方法,做一个区分即可:
for (var key in son) {
if (son.hasOwnProperty(key)) {
console.log(son[key]); // s1
}
}
小结
为了方便区分记忆,我是按照从弱到强的方法来介绍各种方法的。除了Object.getOwnPropertySymbols和Reflect.ownKeys,其他方法都是访问不到Symbol属性的,这也是对于Symbole属性不多加区分的原因。大多数情况下的遍历,都是相对于对象自身的属性而言的。所以可以看到,这里提供的方法大多也都是针对对象自身属性的。如果像遍历继承属性,for...in
的能力其实很弱。最简单的方法就是用前面的4种方法直接访问Son.prototype
或者Object.getPrototypeOf(son)
即可,比如:
Reflect.ownKeys(Object.getPrototypeOf(son)).forEach(key => {
console.log(son[key]); // p1 p2 pSymbolEnumerable pSymbolNoEnumerable
})
复制对象时的属性
ES6提供的…
和Object.assign
方法,又是如何复制属性的呢。
扩展运算符
console.log(Reflect.ownKeys({...son}));// [ 's1', Symbol(skeyEnumerable) ]
复制的对象自身的可枚举属性,包含Symbol属性和非Symbol属性
Object.assign
console.log(Reflect.ownKeys(Object.assign({}, son)));// [ 's1', Symbol(skeyEnumerable) ]
复制的对象自身的可枚举属性,包含Symbol属性和非Symbol属性
小结
按照优先级,最高的应是自身可枚举属性,其次是自身不可枚举属性,再往后是Symbol属性。对于继承属性最不重要,因为继承属性就是prototype自身的属性而已。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。