JavaScript Object.defineProperty 属性描述符详解
前置知识
对象中方法的定义
看如下代码对象中的方法定义是可以简写的
const obj = {
fnn() {
return 'fn-->fnn';
},
testOne: function () {
return 'fn-->testOne';
},
testTwo: () => {
return 'fn-->testTwo';
},
async fnns() {
return 'fn-->fnns';
},
testOnes: async function () {
return 'fn-->testOnes';
},
testTwos: async () => {
return 'fn-->testTwos';
},
['test' + 'three']() {
return `fn-->${['test' + 'three']}`;
},
};
console.log('fnn:', obj.fnn());
console.log('testOne:', obj.testOne());
console.log('testTwo:', obj.testTwo());
console.log('fnns:', obj.fnns());
console.log('testOnes:', obj.testOnes());
console.log('testTwos:', obj.testTwos());
console.log('test' + 'three', obj['test' + 'three']());
console.log('test' + 'three', obj.testthree());
getter 与 setter
get:将对象属性绑定到查询该属性时将被调用的函数。
- 语法:
get [prop]() { ... } prop: 要绑定到给定函数的属性名。
set:将对象属性绑定到要调用的函数。
- 语法:
set [prop](val) { ... } prop: 要绑定到给定函数的属性名。val: 用于保存尝试分配给prop的值的变量的一个别名。
使用示例
const obj = {
a: '我是 a 初始的值,稍后我将改变',
// 相当于一个普通函数
get() {
console.log('调用了此对象的 get 方法');
return 'get';
},
// getter 绑定了 test 属性
get testGetter() {
return this.a;
},
set testGetter(name) {
this.a = name;
},
};
console.log('第一次获取 testGetter:', obj.testGetter);
obj.a = '第一次获取 testGetter 后 a 的值变成此文字';
console.log('第二次获取 testGetter:', obj.testGetter);
obj.testGetter = '这是一个新值';
console.log('第三次获取 testGetter:', obj.testGetter);
Object.defineProperty 作用
直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法: Object.defineProperty(obj, prop, descriptor)
obj:要在其上定义属性的对象。prop:要定义或修改的属性的名称。descriptor:将被定义或修改的 属性描述符
返回值: 被传递给函数的对象。
属性描述符
JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”
对象里目前存在的属性描述符有两种主要形式:数据描述符 和 存取描述符。
数据描述符和存取描述符均具有以下可选键值
configurable:当且仅当该属性的configurable为true时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false(直接声明对象时默认值:true)。enumerable:当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为false(直接声明对象时默认值:true)。
数据描述符同时具有以下可选键值:
value:该属性对应的值。可以是任何有效的JavaScript值(数值,对象,函数等)。默认为undefined。writable:当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为false(直接声明对象时默认值:true)。
存取描述符同时具有以下可选键值:
get:一个给属性提供getter的方法,如果没有getter则为undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为undefined。set:一个给属性提供setter的方法,如果没有setter则为undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为undefined。
如果一个描述符不具有
value,writable,get和set任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(
value或writable)和(get或set)关键字,将会产生一个异常。拥有布尔值的字段的默认值都是
false。value,get和set字段的默认值为undefined。当描述符中省略某些字段时,这些字段将使用它们的默认值。
存储描述符示例
const obj = {
firstName: 'A',
lastName: 'B',
};
Object.defineProperty(obj, 'fullName', {
// 当读取对象此属性值时自动调用, 将函数返回的值作为属性值, this 为 obj
get() {
return this.firstName + '-' + this.lastName;
},
// 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this 为 obj
set(value) {
const names = value.split('-');
this.firstName = names[0];
this.lastName = names[1];
},
});
console.log(obj.fullName); // A-B
obj.fullName = 'w-fly';
console.log('fullName:', obj.fullName); // w-fly
console.log('firstName:', obj.firstName); // w
console.log('lastName:', obj.lastName); // fly
使用 Object.defineProperty 创建属性
如果对象中不存在指定的属性,Object.defineProperty() 就创建这个属性。
var o = {}; // 创建一个新对象
// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, 'a', {
value: 37,
writable: true,
enumerable: true,
configurable: true,
});
// 对象 o 拥有了属性 a,值为 37
console.log(o);
// 在对象中添加一个属性与存取描述符的示例
var bValue;
Object.defineProperty(o, 'b', {
get: function () {
return bValue;
},
set: function (newValue) {
bValue = newValue;
},
enumerable: true,
configurable: true,
});
// 对象 o 拥有了属性 b,值为 38
// o.b 的值现在总是与 bValue 相同,除非重新定义 o.b
o.b = 38;
// 数据描述符和存取描述符不能混合使用
Object.defineProperty(o, 'conflict', {
value: 'aaa',
get: function () {
return 'bbb';
},
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors
修改属性
- 如果属性已经存在,
Object.defineProperty()将尝试根据描述符中的值以及对象当前的配置来修改这个属性。 - 如果旧描述符将其
configurable属性设置为false,则该属性被认为是“不可配置的”,并且没有属性可以被改变(除了单向改变writable为false)。 - 当属性不可配置时,不能在数据和访问器属性类型之间切换。
- 当试图改变不可配置属性(除了
value和writable属性之外)的值时会抛出TypeError,除非当前值和新值相同。 Writable属性:当writable属性设置为false时,该属性被称为“不可写”。它不能被重新分配。
如示例所示,试图写入非可写属性不会改变它,也不会引发错误。
var o = {}; // Creates a new object
Object.defineProperty(o, 'a', {
value: 37,
writable: false,
});
console.log(o.a); // 37
o.a = 25; // 不会报错,但是修改不会生效
console.log(o.a); // 37
目前,有四个操作会忽略 enumerable 为 false 的属性。
for...in循环:只遍历对象自身的和继承的可枚举的属性。Object.keys():返回对象自身的所有可枚举的属性的键名。JSON.stringify():只串行化对象自身的可枚举的属性。Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
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 defaults to false
o.d = 4; // 如果使用直接赋值的方式创建对象的属性,则这个属性的 enumerable 为 true
for (var i in o) {
// 'a' 和 'd'
console.log(i);
}
Object.keys(o); // ["a", "d"]
o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
Configurable 特性:configurable 特性表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。如果 o.a 的 configurable 属性为 true ,则不会抛出任何错误,并且该属性将在最后被删除。
var o = {};
Object.defineProperty(o, 'a', {
get: function () {
return 1;
},
configurable: false,
});
// throws a TypeError
Object.defineProperty(o, 'a', { configurable: true });
// throws a TypeError
Object.defineProperty(o, 'a', { enumerable: true });
// throws a TypeError (set was undefined previously)
Object.defineProperty(o, 'a', {
set: function () {},
});
// throws a TypeError (even though the new get does exactly the same thing)
Object.defineProperty(o, 'a', {
get: function () {
return 1;
},
});
// throws a TypeError
Object.defineProperty(o, 'a', { value: 12 });
console.log(o.a); // 1
delete o.a; // false
console.log(o.a); // 1
添加多个属性和默认值:考虑特性被赋予的默认特性值非常重要,通常,使用点运算符和 Object.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:下面的例子展示了如何实现一个自存档对象。 当设置temperature属性时,archive数组会获取日志条目。
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get: function () {
console.log('get!');
return temperature;
},
set: function (value) {
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 }]
存取器的写法区别
写法一
var obj = Object.defineProperty({}, 'p', {
get: function () {
return 'getter';
},
set: function (value) {
console.log('setter: ' + value);
},
});
obj.p; // "getter"
obj.p = 123; // "setter: 123"
写法二
var obj = {
get p() {
return 'getter';
},
set p(value) {
console.log('setter: ' + value);
},
};
上面两种写法,虽然属性 p 的读取和赋值行为是一样的,但是有一些细微的区别。
- 第一种写法,属性
p的configurable和enumerable都为false,从而导致属性p是不可遍历的 - 第二种写法,属性
p的configurable和enumerable都为true,因此属性p是可遍历的
Object.defineProperties
可以一次性定义或修改多个属性
var obj = Object.defineProperties(
{},
{
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: {
get: function () {
return this.p1 + this.p2;
},
enumerable: true,
configurable: true,
},
},
);
obj.p1; // 123
obj.p2; // "abc"
obj.p3; // "123abc"
Object.getOwnPropertyDescriptor()
可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名
只能用于对象自身的属性,不能用于继承的属性。
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p');
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.getOwnPropertyDescriptor(obj, 'toString'); // undefined
Object.getOwnPropertyNames()
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
var obj = Object.defineProperties(
{},
{
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false },
},
);
Object.getOwnPropertyNames(obj);
// ["p1", "p2"]
和 Object.keys 的行为不同,Object.keys 只返回对象自身的可遍历属性的全部属性名。
Object.keys([]); // []
// 数组自身的length属性是不可遍历的
Object.getOwnPropertyNames([]); // [ 'length' ]
Object.keys(Object.prototype); // []
// Object.prototype 自身的属性都是不可遍历的
Object.getOwnPropertyNames(Object.prototype);
// ['hasOwnProperty',
// 'valueOf',
// 'constructor',
// 'toLocaleString',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString']
Object.getOwnPropertySymbols(obj)
返回一个数组,包含对象自身的所有 Symbol 属性的键名。
Object.prototype.propertyIsEnumerable
返回一个布尔值,用来判断某个属性是否可遍历(可枚举),这个方法只能用于判断对象自身的属性,对于继承的属性一律返回 false。
var obj = {};
obj.p = 123;
obj.propertyIsEnumerable('p'); // true
obj.propertyIsEnumerable('toString'); // false
Object.preventExtensions()
使得一个对象无法再添加新的属性
var obj = new Object();
Object.preventExtensions(obj);
Object.defineProperty(obj, 'p', {
value: 'hello',
});
// TypeError: Cannot define property:p, object is not extensible.
obj.p = 1;
obj.p; // undefined
Object.isExtensible()
用于检查一个对象是否使用了 Object.preventExtensions 方法。也就是说,检查是否可以为一个对象添加属性。
var obj = new Object();
Object.isExtensible(obj); // true
Object.preventExtensions(obj);
Object.isExtensible(obj); // false
Object.seal()
使得一个对象既无法添加新属性,也无法删除旧属性。
var obj = { p: 'hello' };
Object.seal(obj);
delete obj.p;
obj.p; // "hello"
obj.x = 'world';
obj.x; // undefined
Object.seal实质是把属性描述对象的configurable属性设为false,因此属性描述对象不再能改变了。
var obj = {
p: 'a',
};
// seal方法之前
Object.getOwnPropertyDescriptor(obj, 'p');
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.seal(obj);
// seal方法之后
Object.getOwnPropertyDescriptor(obj, 'p');
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: false
// }
Object.defineProperty(obj, 'p', {
enumerable: false,
});
// TypeError: Cannot redefine property: p
Object.seal 只是禁止新增或删除属性,并不影响修改某个属性的值。
var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p; // 'b'
上面代码中,Object.seal 方法对 p 属性的 value 无效,是因为此时 p 属性的可写性由 writable 决定。
Object.isSealed()
用于检查一个对象是否使用了 Object.seal 方法。
var obj = { p: 'a' };
Object.seal(obj);
Object.isSealed(obj); // true
Object.isExtensible 方法返回 false。
var obj = { p: 'a' };
Object.seal(obj);
Object.isExtensible(obj); // false
Object.freeze()
使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。
var obj = {
p: 'hello',
};
Object.freeze(obj);
obj.p = 'world';
obj.p; // "hello"
obj.t = 'hello';
obj.t; // undefined
delete obj.p; // false
obj.p; // "hello"
Object.isFrozen()
用于检查一个对象是否使用了 Object.freeze 方法
var obj = {
p: 'hello',
};
Object.freeze(obj);
Object.isFrozen(obj); // true
使用 Object.freeze 方法以后,Object.isSealed 将会返回 true,Object.isExtensible 返回 false。
var obj = {
p: 'hello',
};
Object.freeze(obj);
Object.isSealed(obj); // true
Object.isExtensible(obj); // false
如何在使用了上面的方法后,给对象添加属性
上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。
var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
proto.t = 'hello';
obj.t;
// hello
上面代码中,对象 obj 本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在 obj 上读到。
如何解决
把 obj 的原型也冻结住
var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);
proto.t = 'hello';
obj.t; // undefined
另一个问题是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。
var obj = {
foo: 1,
bar: ['a', 'b'],
};
Object.freeze(obj);
obj.bar.push('c');
obj.bar; // ["a", "b", "c"]

还没有留言,来发表第一条吧~