defineProperty
提供了精准控制一个对象属性的能力。一般情况下,我们为对象添加属性是通过赋值(obj.property = value
)来创建对象的属性,这样的属性可以被 for...in
或 Object.keys
枚举,可以被改变,也可以被删除。而使用 Object.defineProperty
则允许改变这些额外细节的默认设置。
Object.defineProperty/Reflect.defineProperty
###接口和参数
Object.defineProperty(obj, prop, descriptor)
Reflect.defineProperty(obj, prop, descriptor)
- obj:要在其上定义属性的对象,如果参数不是对象,将抛出
TypeError
异常。 - prop:要定义或修改的属性的名称。
- descriptor:将被定义或修改的属性的描述符。
- return:
Object.defineProperty
返回传递给它的参数对象,Reflect.defineProperty
返回指示定义是否成功的bool
值。
对象的属性描述符分为两种类型:数据描述符 和 存取控制符 ,描述符必须是这两者之一,不可同时存在
这两者有共有的属性:configurable
和 enumerable
。
数据描述符除了这两个共有的属性,还有写控制和属性值两个属性:value
、writable
。
存取控制符除了这两个共有的属性,还有一对 getter-setter
函数定义数据的读写:get
、set
。
configurable
当某属性的 configurable
描述符的值为 true
时,才允许修改其属性描述符。默认为 false。
如果描述符的 configurable
特性为 false
(即该特性为 non-configurable
),那么除了 writable
外,其他特性都不能被修改,其 writable
特性也只能修改为 false
,并且数据和存取描述符也不能相互切换。
如果尝试修改不允许修改的属性,将会产生一个 TypeError
异常,若当前值与修改值相同,不报错。
var object = {}; |
enumerable
当某属性的 enumerable
描述符的值为 true
时,该属性才能够出现在对象的枚举属性中。默认为 false。
该描述符影响 for...in
、 Object.keys
、Object.assign
var object = {}; |
如果想要获取不可枚举的属性集合,可以使用 Object.getOwnPropertyNames
。
Object.getOwnPropertyNames(object); // ["key1", "key2"] |
writable
当且仅当该属性的 writable
为 true
时,该属性才能被赋值运算符改变。**默认为 false
**。
该属性控制字段的可写性。
var object = {}; |
给 writable
为 false
的字段赋值,在非严格模式下会静默失败,在严格模式下会抛出 TypeError
异常:
(function() { |
如果,属性的值是对象,即使 writable
为 false
其对象仍然可以修改:
var object = Object.defineProperty({}, "arr", { |
value
设置该属性对应的值。可以是任何有效的 JavaScript
值(数值,对象,函数等)。**默认为 undefined
**。
get 和 set
属性提供 getter
和 setter
的方法。 getter
方法返回值被用作属性值, setter
方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined
。
这两个函数提供了对象属性读写的“钩子”,通过它们,可以在属性读值和赋值的时候执行必要的操作,如下,为 key
属性增加了读写的日志记录:
var object = Object.defineProperty(object, 'key', { |
检查属性定义是否成功
Reflect.defineProperty
和 Object.defineProperty
功能一样,区别在于返回值类型不一样,Object.defineProperty
返回一个对象或如果属性没有成功被定义,抛出一个 TypeError
。 而 Reflect.defineProperty
简单地返回一个 Boolean
表明是否该属性被成功定义了。
// Object 语句 |
Object.getOwnPropertyDescriptor/Reflect.getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor(obj, prop)
Reflect.getOwnPropertyDescriptor(obj, prop)
返回指定对象上一个自身属性对应的属性描述符。
Reflect.getOwnPropertyDescriptor
与 Object.getOwnPropertyDescriptor
的唯一不同在于如何处理非对象目标。如果方法的第一个参数不是一个对象(一个原始值),Reflect.getOwnPropertyDescriptor
将抛出 TypeError
错误,而 Object.getOwnPropertyDescriptor
会将非对象的第一个参数将被强制转换为一个对象处理。
Reflect.getOwnPropertyDescriptor("foo", 0); |
Object.defineProperties
Object.defineProperties(obj, props)
方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。
var obj = {}; |
Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptor(obj, prop)
返回对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象,Object.getOwnPropertyDescriptor
的加强版。
var obj = { |
浅拷贝(shallow copy
)和浅合并(shallow merge
)
上面提到 Object.assign
方法只能拷贝源对象的可枚举的自身属性,无法拷贝源对象的原型。使用该方法配合 Object.create
方法可以实现对象的浅拷贝(shallow copy
)。
const shallowClone = (object) => Object.create( |
当然也可以用来浅合并对象(shallow merge
)
const shallowMerge = (target, source) => Object.defineProperties( |
利用它实现 Mixins
利用它实现 Mixins
也非常方便:
let mix = (object) => ({ |
Object.assign 的改进
Object.assign
方法只会拷贝源对象自身的并且可枚举的属性到目标对象身上。该方法使用源对象的 [[Get]]
和目标对象的 [[Set]]
,所以它会调用相关 getter
和 setter
,而且访问器属性会被转换成数据属性,这样的拷贝是有副作用的。
这里我们对此做一个改进,使用 [[DefineOwnProperty]]
/[[GetOwnProperty]]
这样就不会调用相关的 [[Get]]
/[[Set]]
,让它可以拷贝所有的属性,并不导致副作用。
var completeAssign = (target, ...sources) => |
polyfill
这个函数的支持度较差,如果需要可以使用如下的 polyfill:
if (!Object.hasOwnProperty('getOwnPropertyDescriptors')) { |
和赋值运算符比较
要注意,使用赋值运算符(=
) 为对象的属性赋值和使用 defineProperty
的描述符并不一样:
var object = {}; |
注意看,两种赋值方法的省略属性的默认值是不同的。
应用
阻止属性被遍历
有一些属性,我们并不想被遍历,这样可以修改其可枚举性,最常见的例子就是数组的 length
字段,根据ECMAScript® 2018 Language Specification,数组的 length
实际上就是一个不可枚举的字段,它具有这样的属性描述符: { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }
,我们也可以很容易定义一个这样的字段。
设置不可写的私有字段
比如我有一个 math.js
库,里面有一些字段并不想被改变,虽然有很多方法,最简单的方法是让这个属性不可写:
var circle = { |
注意 configurable
要设置为 false
, 以免 writable
的值被修改。
双向数据绑定
在一些类库,比如 Angular
和 Vue
都有着视图和数据模型之间的双向的数据绑定。当更新视图的时候,数据会发生变动,当更改数据的时候,视图也会随之变动。
这里我们来利用 defineProperty
实现这个一个功能:为对象的属性定义 get
和 set
方法,当调用 get
方法的时候,会从视图中获取值,当调用对象的 set
方法的时候,会把值更新到视图中,代码如下:
1.定义 html
结构:
<label> |
2.定义数据的 get
和 set
操作:
let info = {}; |
这样就把 input
和 info.name
字段绑定到了一起。
属性校验
通过访问控制符可以轻松设置属性赋值时候的校验:
var person = {}; |
参考
MDN-defineProperty
tc39/proposal-object-getownpropertydescriptors