对象的属性描述符是一个初学者容易忽略但是非常重要的特性,像是vue的数据双向绑定就是用它做文章。且关于它的方法和属性也很多,今天我来总结一下。
属性描述符概述
对象的每个属性都具备了属性描述符,它描述了属性的一些特性。一共有四个特性,分别为value(值)、writable(可写)、enumerable(可枚举)和configurable(可配置)。只设置属性值的属性描述符也被称为数据描述符。
这里我们介绍两个方法,Object.getOwnPropertyDescriptor()以及Object.defineProperty()。
//Object.getOwnPropertyDescriptor()第一个参数为某个对象,第二个参数为某个对象上的某个属性,返回这个属性的属性描述符。
//默认writable、enumerable和configurable都为true
let obj = {
foo: 123,
bar: 456
}
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
输出为:
{
value: 123,
writable: true,
enumerable: true,
configurable: true
}
复制代码
//Object.defineProperty()可以给某个对象添加一个新属性或者修改某个对象的一个已有属性(前提是configurable为true)
Object.defineProperty(obj, 'baz', {
value: 789,
writable: true,
enumerable: true,
configurable: true
})
console.log(obj.baz) //789
复制代码
writable
writable决定是否可以修改属性的值,这个不可修改是指通过.或者[]在对象上修改该属性是会出错的,但是通过属性描述符还是可以修改的。
let obj = {}
Object.defineProperty(obj, 'baz', {
value: 1,
writable: false,
enumerable: true,
configurable: true
})
console.log(obj) //{ baz: 1 }
Object.defineProperty(obj, 'baz', {
value: 2,
writable: false,
enumerable: true,
configurable: true
})
console.log(obj) //{ baz: 2 }
obj.baz = 3 //这样是无法修改baz属性的
console.log(obj) //{ baz: 2 }
复制代码
configurable
configurable决定属性是否可以通过Object.defineProperty方法来修改属性描述符。configurable一旦设置是不可逆的,设置之后它除了不允许配置属性描述符以外,还不允许通过delete删除该属性。
let obj = {}
Object.defineProperty(obj, 'baz', {
value: 1,
writable: true,
enumerable: true,
configurable: false
})
console.log(obj) //{ baz: 1 }
obj['baz'] = 3 //还是可以通过这种方式来修改属性的值
console.log(obj) //{ baz: 3 }
delete obj.baz
console.log(obj) //{ baz: 3 }没删掉,但前一句不会报错
Object.defineProperty(obj, 'baz', { //TypeError: Cannot redefine property: baz
value: 2,
writable: true,
enumerable: true,
configurable: false
})
复制代码
enumerable
enumerable被称为可枚举性,如果该属性为false,则表示某些操作会忽略当前属性。es5有三个操作:for...in、Object.keys、JSON.stringify,再加上es6的Object.assign方法,一共四个操作会忽略不可枚举的属性。
属性的遍历
1. for...in:循环遍历对象自身的和继承的可枚举的属性。
2. Objects.keys(obj):返回一个数组,包括对象自身(不含继承)的所有可枚举属性。所以说对象的遍历一般用该方法。
3. Object.getOwnPropertyNames(obj):返回一个数组,包括对象自身(不含继承)的所有属性。包含不可枚举的属性。
let proto = { y: 1, z: 2 };
let obj = { x: 10 }
Object.defineProperty(obj, 'foo', {
value: 3,
writable: true,
enumerable: false,
configurable: false
})
Object.setPrototypeOf(obj, proto);
Object.getOwnPropertyNames(obj).forEach(key => console.log(obj[key])) //10,3
for (let key in obj) {
console.log(obj[key]); //10,1,2
}
Object.keys(obj).forEach(key => console.log(obj[key])) //10
复制代码
4. Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性。
5. Reflect.ownKeys(obj):返回一个数组,包含自身所有属性,包括Symbol属性和不可枚举属性。
[[Get]]和[[Put]]
属性的[[Get]]操作会从该对象自身属性开始,沿着它的原型链寻找是否有名称相同的属性,如果找到就返回该属性的值,如果自始至终都没找到值,就返回undefined。
[[Put]]操作则涉及两种情况。一个是已经存在该属性,如果存在且有setter就调用setter,如果writable为false,则静默失败,如果这两者都不是,将该值设置为属性的值。如果对象中不存在该属性,就涉及到原型链的问题,下一节在原型及原型链中总结。
getter和setter方法可以部分改写默认的[[Get]]和[[Put]]操作,getter在获取属性值时调用,setter在设置属性值时调用。当给一个属性设置getter和setter之后,这个属性的属性描述符也被称作访问描述符,Js会忽略掉它的writable和value特性,用get来取代value,用set取代writable。
getter和setter有两种定义方式,一种在对象中直接定义,一种通过Object.defineProperty定义。
let val = 2;
let obj = {
get a() {
return val;
},
set a(newVal) {
if (val === newVal) {
return;
}
val = newVal * 2;
}
}
obj.a = 4;
console.log(obj.a); //8
obj.a = 3;
console.log(obj.a); //6
复制代码
相当于:
let val = 2;
let obj = {};
Object.defineProperty(obj, 'a', {
get: function() {
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal * 2;
}
})
obj.a = 4;
console.log(obj.a);//8
obj.a = 3;
console.log(obj.a);//6
复制代码
存在性
例如'a' in myobj操作符和myobj.a/myobj[a]都是在整个原型链上查找查找该对象是否有该属性。相比之下,myobj.hasOwnProperty('a')方法只会在myobj这个对象中查找该属性,而不会去原型链中查找。
hasOwnProperty是Object.prototype上的方法,通过Object.create(null)创建的对象是无法使用该方法的。
let myobj = {
a: 1
}
let proto = {
b: 2
}
Object.setPrototypeOf(myobj, proto);
console.log('a' in myobj); //true
console.log('b' in myobj); //true
console.log(myobj.hasOwnProperty('a')) //true
console.log(myobj.hasOwnProperty('b')) //false
复制代码
部分内容参考《你不知道的JavaScript上》和《ES6标准入门》。