Object.definePropety() 方法详解
Object.definePropety() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象
语法 Object.defintPropety(obj, prop, descriptor)
参数
obj
需要定义属性的对象prop
需被定义或修改的属性名descript
需被定义或修改的属性的描述符
描述
该方法允许精确添加或修改对象的属性,一般情况下,我们为对象添加属性是通过赋值来创建并显示在属性枚举中(for...in
或Object.keys
方法),但这种方式添加的属性值可以被改变或被删除。而使用Object.definePropety()
添加的属性是默认不可改变的
对象里目前存在的属性描述参数(descriptor)有两种主要形式
- 数据描述符(data decriptor):是以恶搞包含属性的值,并说明这个值可读或不可读的对象
- 访问器描述符(accessor descriptor):是由一对getter-setter方法的对象。
一个完整的属性描述必须是两种形式之一;不能同时是两者
数据描述符和访问器描述符必须含有有以下键值对
configurable
:仅当该属性的configurable
为true
时,该属性才能够被修改或可以通过delete
来删除该属性,默认为false
enumerable
:仅当该属性的enumerable
为true
时,该属性才能够出现在对象的枚举属性中,默认false
数据描述符可以包含以下可选键值对
value
:该属性对应的值,可以是任何有效的javascript值(数值、对象、函数等)。默认为undefined
writable
:仅当该属性的writable
为true
时,该属性才能被赋值运算改变,默认为false
访问器描述符可以包含以下可选键值对
get
:一个给属性提供getter
的方法,如果没有getter
则为undefined
,该方法返回值被用作属性值,undefined
set
:一个给属性提供setter
的方法,如果没有setter则为undefined
,该方法将接受唯一参数 ,并将该参数的新值分配给该属性,默认为undefined
这些描述符的属性并不是必须的,从原型链继承而来的属性也可填充,为了保证这些描述符属性被填充为默认值,使用形如预先冻结Object.prototype、明确设置每个描述符属性的值、使用Object.create(null)来获取空对象等方式
代码片段:
//使用 _proto_
Object.defineProperty(obj,'key',{
_proto_:null, // 没有继承的属性
value:'static' // 没有enumerable
// 没有configurable
// 没有writable
// 作为默认值
})
//或者使用另一种方法 _proto_
var obj = {}
var descriptor = Object.create(null)
//所有描述符的属性被设置为默认值
descriptor.value = 'static'
Object.defineProperty(obj,'key',descriptor)
// 明确设置每个描述符的属性
Object.defineProperty(obj,'key',{
enumerable: false,
configurable: false,
writable: false,
value: 'static'
})
// 循环使用同一对象
function withValue(value){
var d = withValue.d || (
withValue.d = {
enumerable: false,
writable: false,
configurable: false,
value: null
}
);
d.value = value;
return d;
}
Object.definePropety(obj,'key',withValue('static'));
//如果freeze可用,防止代码添加或删除对象原型的属性
//(value,get,set,enumerable,writable,coonfigurable)
(Object.freeze||Object)(Object.prototype)
使用示例
一、创建一个属性
如果当前对象不存在我们要设置的属性,Object.defineProperty()会根据方法设置为对象创建一个新的属性,如果描述符参数缺失,则会被设置为默认值,所有布尔类型描述符属性会被默认为false。而value,get,set会被默认设置为undefined,一个未设置get/set/value/writable的属性被称为一个’原生属性(generic)’,并且他的描述符(descriptor)会被’归类’为一个数据描述符(data descriptor)
var o = {};//创建一个对象
//使用数据描述符来为对象添加属性
Object.defineProperty(o,'a'{
value: 37,
writable: true,
enumerable: true,
configurable: true
});
//属性a被设置到对象o上,并且值为37
//使用访问器描述符来为对象添加属性
var bValue = 38;
Object.defineProperty(o,'b',{
get:function(){return bValue;},
set:function(newValue){bValue = newValue},
enumerable: true,
configurable: true
})
o.b; //38
//属性b被设置到对象o上,并且值为38
现在o.b的值指向bValue变量,除非o.b被重新定义
不能混合数据、访问器两种描述符
Object.defineProperty(o,'conflict',{
value :323232,
get:function(){return 323232}
});
//会抛出一个错误 value appears only in data descriptors, get appears only in accessor descriptors(value只出现在数据描述符中,get只出现在访问器描述符中)
二、修改一个属性
当某个属性已经存在了,Object.defineProperty()会根据对象的属性配置(configuration)和新设置的值来尝试修改该属性,如果该属性的configurable被设置为false,则该属性无法被修改(这种情况下有个特殊情况:如果之前的writable设置为true,则我们仍可以将writable设置为false,一旦这么做之后,任何描述符属性变的不可设置),如果属性的configurable设置为false,则我们无法将属性的描述符在数据描述符和访问描述符之间转换
- 可写特性-writable
当一个属性的writable被设置为false,这个属性就成为不可写的(non-writable),该属性不可被重新赋值
var o = {}
Object.defineProperty(o,'a',{
value:38,
writable:false
})
console.log(o.a) //37
o.a = 25;//没有抛出错误
//在严格模式下会抛出错误
console.log(o.a) //仍然是37 赋值操作无效
- 可枚举特性-enumerable
属性的enumerable值定义对象的属性是否会出现在枚举器(for…in循环和Object.keys())中
var o = {}
Object.defineProperty(o,‘a’,{
value: 1,
enumerable: true
})
Object.defineProperty(o,‘b’,{
value: 2,
enumerable: false
})
Object.defineProperty(o,‘c’,{
value: 3
})//enumerable默认设置为false
o.d = 4;//通过直接设置属性的方式,enumerable将被设置为true
for (var i in o) {
console.log(i)
}
//打印出来'a'和'd'
o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
- 可配置特性-configurable
属性的configurable
值控制一个对象的属性可否被delete删除,同事也控制该属性描述符的配置可否改变(除了前文所描述在configurable为false时,若writable为true,则仍可以进行一次修改将writable改变为false)
var o = {}
Object.defineProperty(o,'a',{
get: function(){return 1},
configurable: false
})
Object.defineProperty(o,'a',{
configurable: true
})//抛出错误
Object.defineProperty(o,'a',{
enumerable: true
})//抛出错误
Object.defineProperty(o,'a',{
set:function(){}
})//抛出错误(set之前被设置为undefined)
Object.defineProperty(o,'a',{
get:function(){return 1}
})//抛出错误(set之前被设置为undefined)
Object.defineProperty(o,'a',{
value: 12
})//抛出错误
console.log(o.a) //1
delet o.a; //什么都不发生
console.log(o.a) //1
注意: 如果o.a属性的configurable为true,就不会有任何错误抛出,并且o.a在最后delete操作中会被删除
- 添加属性时的默认值
考虑描述符特性的默认值如何被应用是非常重要的,正如下面实例所示,简单的使用.
符号来设置一个属性和使用Obkect.defineProperty()是有很大区别的
var o = {};
o.a = 1;
//等同于
Object.defineProperty(o,'a',{
value: 1,
writable: true,
configurable: true,
enumerable: true
})
//另一方面
Object.defineProperty(o,'a',{value:1});
//等同于
Object.defineProperty(o,'a',{
value:1,
writable: false,
configurable: false,
enumerable: false
})
- 定制的Setters和Getters
下面的示例展示了如何实现以一个’自存档(self-archiving)'的对象,当temperature属性被设置时,archive数组就会添加一个日志记录
function Archiver(){
var temperature = null;
var archive = [];
Object.defineProperty(this,'temperature',{
get: function(){
console.log('get');
return temperature;
},
set: function(){
temperature = value;
archive.push({val:temperature})
}
})
this.getArchive = function(){return archive}
}
var arc = new Archiver()
arc.temperature //'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); //[{val:11},{val:13}]
补充知识点
Object.defineProperty()方法被许多现代前端框架(Vue.js,React.js)用于数据双向绑定的实现,当我们在框架Model层设置data时,框架将会通过Object.defineProperty()方法来绑定所有数据,并在数据变化的同事需i该虚拟节点,最终修改页面的DOM结构。在这个实现的过程中需要注意以下几点
- 延迟发生变化
现代框架为了避免密集的Dom修改操作,对绑定的数据修改后将会设置一个极小(通常为1ms)的setTimeout延迟在应用变化。也就是说虚拟节点和页面Dom树的变化和数据的变化中间会存在一个空闲期。
假如我们想实现一个功能:在某项数据变化后,页面立即发生变化,并且下一步开发者获取这个变化的DOm,这样的功能通过现代前端框架是无法完成的,当然,那些框架也为我们提供了许多应对方法,例如Vue的nextTick()方法等等
- 数组的变化
对数组变化的跟踪情况
var a = {}
bValue = 1;
Object.defineProperty(a,'b',{
set:function(value){
bValue = value;
console.log('setted')
},
get:function(){
return bValue
}
});
a.b; //1
a.b = []; // setted
a.b = [1,2,3]; // setted
a.b[1] = 10; // 无输出
a.b.push(4);// 无输出
a.b.length = 5; //无输出
a.b; // [1,10,3,4,undefined]
可以看到,当a.b被设置为数组后,只要不是重新赋值一个新的数组对象,任何对数组的修改都不会触发setter方法的执行,这一点非常重要,因为基于Object.defineProperty()
方法的现代前端框架实现的数据双向绑定也同样无法识别这样的数组变化。一次第一点,如果想要触发数据双向绑定,我们不要使用arr[1] = newValue这样的语句来实现;第二点,框架也提供了许多方法来实现数组的双向绑定。
对于框架如何实现数组变化的监测,大多数情况下,框架会重写Array.prototype.push方法,并生成一个新的数组赋值个数组,这样数据双向绑定就睡触发。但是这样实现的数组修改 会消耗更多的内存
configurable和writable
原文中描述过一种特殊情况:当configurable为false时,我们唯一能改变的属性就是将设置为true的writable设置为false,对此进行以下验证(Chrome和IE下运行论证,输出结果相同)
var a = {}
Object.defineProperty(a,'o',{
configurable: false,
value: 10,
writable: true
})
console.log(a.o) //10
a.o = 12 // 不报错
console.log(a.o) //12
Object.defineProperty(a,'o',{
configurable: false,
value: 14,
writable: true
})
console.log(a.o) // 14
Object.defineProperty(a,'o',{
configurable: false,
value: 14,
writable: false
})
a.o = 16 //不报错
console.log(a.o) //14
Object.defineProperty(a,'o',{
configurable: false,
value: 16,
writable: false
}) // 报错
以上可以得出结论,对于描述符(descriptor)为数据描述符(data descriptor)的情况:
- 使用
.
操作符来设置属性的值永远不会报错,仅当writable为false时无效 - 只要writable为true,不论configurable是否为false,都可以通过Object.defineProperty()来修改value的值