超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

Object.defineProperty

defineProperty 提供了精准控制一个对象属性的能力。一般情况下,我们为对象添加属性是通过赋值(obj.property = value)来创建对象的属性,这样的属性可以被 for...inObject.keys 枚举,可以被改变,也可以被删除。而使用 Object.defineProperty 则允许改变这些额外细节的默认设置。

Object.defineProperty/Reflect.defineProperty

###接口和参数

Object.defineProperty(obj, prop, descriptor)
Reflect.defineProperty(obj, prop, descriptor)

  • obj:要在其上定义属性的对象,如果参数不是对象,将抛出 TypeError 异常。
  • prop:要定义或修改的属性的名称。
  • descriptor:将被定义或修改的属性的描述符。
  • returnObject.defineProperty 返回传递给它的参数对象,Reflect.defineProperty 返回指示定义是否成功的 bool 值。

对象的属性描述符分为两种类型:数据描述符存取控制符描述符必须是这两者之一,不可同时存在

这两者有共有的属性:configurableenumerable
数据描述符除了这两个共有的属性,还有写控制和属性值两个属性:valuewritable
存取控制符除了这两个共有的属性,还有一对 getter-setter 函数定义数据的读写:getset

configurable

当某属性的 configurable 描述符的值为 true 时,才允许修改其属性描述符。默认为 false

如果描述符的 configurable 特性为 false(即该特性为 non-configurable ),那么除了 writable 外,其他特性都不能被修改,其 writable 特性也只能修改为 false ,并且数据和存取描述符也不能相互切换。

如果尝试修改不允许修改的属性,将会产生一个 TypeError 异常,若当前值与修改值相同,不报错。

var object = {};

// 设置 key1 属性为不可配置
Object.defineProperty(object, "key1", {
configurable: false,
enumerable: true
writable: true
});

// 修改其他属性报错
Object.defineProperty(object, "key1", {
enumerable: false
});
// TypeError: Cannot redefine property: key1

// 修改 writable 从 true 到 false 不报错
Object.defineProperty(object, "key1", {
writable: false
});

// 修改 writable 从 false 到 true 报错
Object.defineProperty(object, "key1", {
writable: true
});
// TypeError: Cannot redefine property: key1

enumerable

当某属性的 enumerable 描述符的值为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false

该描述符影响 for...inObject.keysObject.assign

var object = {};
// 定义可枚举属性 key1
Object.defineProperty(object, "key1", {
enumerable: true
});

// 定义不可枚举属性 key2
Object.defineProperty(object, "key2", {
enumerable: false
});

Object.keys(object); //["key1"]

如果想要获取不可枚举的属性集合,可以使用 Object.getOwnPropertyNames

Object.getOwnPropertyNames(object); // ["key1", "key2"]

writable

当且仅当该属性的 writabletrue 时,该属性才能被赋值运算符改变。**默认为 false**。

该属性控制字段的可写性。

var object = {}; 

Object.defineProperty(object, "key", {
value: 'value',
writable: false
});

console.log(object.key); // 打印 'value'
object.key = 'value2'; // 尝试赋值,没有出错
console.log(object.key); // 打印 'value', 赋值不起作用。

writablefalse 的字段赋值,在非严格模式下会静默失败,在严格模式下会抛出 TypeError 异常:

(function() {
"use strict"
object.key = 'value2';
})();
// TypeError: Cannot assign to read only property 'key' of object '#<Object>'

如果,属性的值是对象,即使 writablefalse 其对象仍然可以修改:

var object = Object.defineProperty({}, "arr", { 
value: [1],
writable: false
});

object.arr.push(2);
console.log(object.arr); // [1, 2]

value

设置该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。**默认为 undefined**。

get 和 set

属性提供 gettersetter 的方法。 getter 方法返回值被用作属性值, setter 方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined

这两个函数提供了对象属性读写的“钩子”,通过它们,可以在属性读值和赋值的时候执行必要的操作,如下,为 key 属性增加了读写的日志记录:

var object = Object.defineProperty(object, 'key', {
set(value) {
console.log("set value: " + value);
this._value = value;
},
get() {
console.log("get value: " + this._value);
return this._value;
}
});

object.key; // get value: undefined
object.key = 5; // set value: 5
object.key; // get value: 5

检查属性定义是否成功

Reflect.definePropertyObject.defineProperty 功能一样,区别在于返回值类型不一样,Object.defineProperty 返回一个对象或如果属性没有成功被定义,抛出一个 TypeError 。 而 Reflect.defineProperty 简单地返回一个 Boolean 表明是否该属性被成功定义了。

// Object 语句
try (Object.defineProperty(target, property, attributes)) {
// 成功
} catche(e) {
// 失败
}

// Reflect 语句
if (Reflect.defineProperty(target, property, attributes)) {
// 成功
} else {
// 失败
}

Object.getOwnPropertyDescriptor/Reflect.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor(obj, prop)
Reflect.getOwnPropertyDescriptor(obj, prop)

返回指定对象上一个自身属性对应的属性描述符。

Reflect.getOwnPropertyDescriptorObject.getOwnPropertyDescriptor 的唯一不同在于如何处理非对象目标。如果方法的第一个参数不是一个对象(一个原始值),Reflect.getOwnPropertyDescriptor 将抛出 TypeError 错误,而 Object.getOwnPropertyDescriptor 会将非对象的第一个参数将被强制转换为一个对象处理。

Reflect.getOwnPropertyDescriptor("foo", 0);
// TypeError: "foo" is not non-null object

Object.getOwnPropertyDescriptor("foo", 0);
// { value: "f", writable: false, enumerable: true, configurable: false }

Object.defineProperties

Object.defineProperties(obj, props)

方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。

var obj = {};
Object.defineProperties(obj, {
"key1": {
value: true,
writable: true
},
"key2": {
value: "Hello",
writable: false
}
});
console.log(obj.key2) // "Hello"

Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptor(obj, prop)

返回对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象,Object.getOwnPropertyDescriptor 的加强版。

var obj = {
a: 1,
b: 2
}
Object.getOwnPropertyDescriptors(obj);
//{
// a: {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: true
// },
// b: {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
//}

浅拷贝(shallow copy)和浅合并(shallow merge

上面提到 Object.assign 方法只能拷贝源对象的可枚举的自身属性,无法拷贝源对象的原型。使用该方法配合 Object.create 方法可以实现对象的浅拷贝(shallow copy)。

const shallowClone = (object) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);

当然也可以用来浅合并对象(shallow merge

const shallowMerge = (target, source) => Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);

利用它实现 Mixins

利用它实现 Mixins 也非常方便:

let mix = (object) => ({
with: (...mixins) => mixins.reduce(
(c, mixin) => Object.create(
c, Object.getOwnPropertyDescriptors(mixin)
), object)
});

// multiple mixins example
let a = {a: 'a'};
let b = {b: 'b'};
let c = {c: 'c'};
let d = mix(c).with(a, b);

Object.assign 的改进

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象身上。该方法使用源对象的 [[Get]] 和目标对象的 [[Set]],所以它会调用相关 gettersetter,而且访问器属性会被转换成数据属性,这样的拷贝是有副作用的。

这里我们对此做一个改进,使用 [[DefineOwnProperty]]/[[GetOwnProperty]] 这样就不会调用相关的 [[Get]]/[[Set]],让它可以拷贝所有的属性,并不导致副作用。

var completeAssign = (target, ...sources) =>
sources.reduce((target, source) => {
let descriptors = Object.getOwnPropertyDescriptors(source);
// 去掉不可枚举的属性,如果想要使其可以拷贝枚举属性,则去掉下面代码
Reflect.ownKeys(descriptors).forEach(key => {
if (!descriptors[key].enumerable) {
delete descriptors[key];
}
});
return Object.defineProperties(target, descriptors);
}, target);

polyfill

这个函数的支持度较差,如果需要可以使用如下的 polyfill

if (!Object.hasOwnProperty('getOwnPropertyDescriptors')) {
Object.defineProperty(
Object,
'getOwnPropertyDescriptors',
{
configurable: true,
writable: true,
value: function getOwnPropertyDescriptors(object) {
return Reflect.ownKeys(object).reduce((descriptors, key) => {
return Object.defineProperty(
descriptors,
key,
{
configurable: true,
enumerable: true,
writable: true,
value: Object.getOwnPropertyDescriptor(object, key)
}
);
}, {});
}
}
);
}

和赋值运算符比较

要注意,使用赋值运算符(=) 为对象的属性赋值和使用 defineProperty 的描述符并不一样:

var object = {};
// 使用赋值运算符为对象赋值
object.key1 = 'value1';
// 使用 defineProperty 为对象赋值
Object.defineProperty(object, 'key2', {
value:'value2'
});

console.log(Object.getOwnPropertyDescriptor(object, 'key1'));
// Object {value: "value1", writable: true, enumerable: true, configurable: true}

console.log(Object.getOwnPropertyDescriptor(object, 'key2'));
// Object {value: "value2", writable: false, enumerable: false, configurable: false}

注意看,两种赋值方法的省略属性的默认值是不同的。

应用

阻止属性被遍历

有一些属性,我们并不想被遍历,这样可以修改其可枚举性,最常见的例子就是数组的 length 字段,根据ECMAScript® 2018 Language Specification,数组的 length 实际上就是一个不可枚举的字段,它具有这样的属性描述符: { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false },我们也可以很容易定义一个这样的字段。

设置不可写的私有字段

比如我有一个 math.js 库,里面有一些字段并不想被改变,虽然有很多方法,最简单的方法是让这个属性不可写:

var circle = {
PI: 3.14,
area: function(radius) {
return this.PI * radius * radius;
}
}

Object.defineProperty(circle, "PI", {
configurable: false,
writable: false
});

注意 configurable 要设置为 false , 以免 writable 的值被修改。

双向数据绑定

在一些类库,比如 AngularVue 都有着视图和数据模型之间的双向的数据绑定。当更新视图的时候,数据会发生变动,当更改数据的时候,视图也会随之变动。

这里我们来利用 defineProperty 实现这个一个功能:为对象的属性定义 getset 方法,当调用 get 方法的时候,会从视图中获取值,当调用对象的 set 方法的时候,会把值更新到视图中,代码如下:

1.定义 html 结构:

<label>
<span>Name:</span>
<input type="text" id="name" value="" />
</label> 

2.定义数据的 getset 操作:

let info = {};

Object.defineProperty(info, 'name', {
get: function() {
return document.getElementById('name).value;
}
set: function(val) {
document.getElementById('name).value = val;
}
});

这样就把 inputinfo.name 字段绑定到了一起。

属性校验

通过访问控制符可以轻松设置属性赋值时候的校验:

var person = {};
Object.defineProperty(person, 'age', {
set(age) {
if (age < 0 || age > 100) {
throw Error(`${age} is not a valid value`);
}
this._age = age;
},
get() {
return this._age;
}
});erson.age = 1000; // Error: 1000 is not a valid value
person.age = 50;
console.log(person.age); // 50

参考

MDN-defineProperty
tc39/proposal-object-getownpropertydescriptors