Object.defineProperty()来自 ECMAScript 5.1 (ECMA-262) 规范
兼容 Internet Explorer 9+ 等其他现代浏览器
初识Object.defineProperty()
Object.defineProperty() 方法用于在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,该方法允许精确添加或修改对象的属性。
乍看描述,也许会让人不太理解,在对象上定义属性我们通常使用对象字面量语法或者.
操作符来处理,比如
var obj = {}; obj.attr = 1;
// 或是
var obj2 = {attr: 2};
该方法的重要意义在于后面一句,即该方法允许精确添加或修改对象的属性
。
举个栗子,仅用于初步体会该方法的效果。我们有时候会需要遍历一个对象的所有属性,这时可能会使用for in
方法
var obj = {
name: 'sky',
age: 18,
sex: 'male'
}
for (var i in obj) {
console.log(i);
}
// 打印结果
// name
// age
// sex
我们知道for in
方法会遍历对象和其原型上所有可枚举
的属性,我们自己创建的对象的原型是Object.prototype
,该原型上有一个可能常用到的方法Object.prototype.toString()
,通常我们用这个方法准确判断数据的类型。我们发现这个方法并没有被遍历出来,如前所述,这个方法显然是不可遍历的。那么我们如何自己实现为属性赋予这些‘’精确‘’特性的效果呢。这就轮到Object.defineProperty()出场了。
在此之前,我们再看一些“有趣”的东西,先不用理解具体原理,我们在上面程序后面加上一句
console.log(Object.getOwnPropertyDescriptor(obj.__proto__, 'toString'));
打开控制台发现了如下输出
发现得到了一些有趣的东西,enumerable(可枚举的)属性是false,这似乎就和之前的现象产生了联系。
认识Object.defineProperty()
语法:
Object.defineProperty(obj, prop, descriptor)
参数:
obj
要在其上定义属性的对象。
prop
要定义或修改的属性的名称。
descriptor
将被定义或修改的属性描述符。
返回: 被传递给函数的对象
我们发现重点就在第三个参数,属性描述符上面。
属性描述符
在理解属性描述符之前,需要先了解一点前置知识。
getter 和 setter
对象属性是由名字、值和一组特性构成的。在ES5中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter。由getter和setter定义的属性称做 "存取器属性",它不同于 "数据属性",数据属性就是一个简单的值。
var prop = {
a: 0,
get b(){
return 1;
}
};
console.log(prop.a); //0
console.log(prop.b); //1
上面代码中,属性a称为“数据属性”,它只有一个简单的值;像属性b这种用getter和setter方法定义的属性称为“存取器属性”。当一个属性被定义为存取器属性时,JavaScript会忽略它的value和writable特性,取而代之的是set和get(还有configurable和enumerable)特性。
当程序查询存取器属性的值时,JS调用getter方法(无参数)。这个方法的返回值就是属性存取表达式的值。
当程序设置一个存取器属性的值时,JS调用setter方法,将赋值表达式右侧的值当做参数传入setter。从某种意义上讲,这个方法负责 "设置"属性值。可以忽略setter方法的返回值。
如果属性同时具有getter和setter,那么它是一个读/写属性。如果它只有getter方法,那么它是一个只读属性。如果它只有setter方法,那么它是一个只写属性,读取只写属性总是返回undefined。
var prop = {
get b(){
return 1;
}
};
prop.b = 3; // 这个设置是无效的
console.log(prop.b); //1
和数据属性一样,存取器属性是可以继承的。且从 ECMAScript 2015 开始,还可以使用一个计算属性名的表达式绑定到给定的函数。这里不再赘述。
属性描述符的配置
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一,不能同时是两者。
相信在理解了前置知识后,这一段也是很清晰明了的。
以下相关定义摘自MDN
数据描述符
和存取描述符
均具有以下可选键值:
configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
enumerable
当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符
同时具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。
存取描述符
同时具有以下可选键值:
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。
描述符可同时具有的键值
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | Yes |
Yes |
Yes |
Yes |
No |
No |
存取描述符 | Yes |
Yes |
No |
No |
Yes |
Yes |
如果一个描述符不存在value,writable,configurable和enumerable关键字中任意一个,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
实际使用
不要忽视继承
首先需要注意的是用作属性描述符的对象如果继承了某些关键字属性,可能会影响正常的使用,来看一个例子
var a = {};
var descriptor = {
value: 2
};
descriptor.__proto__ = {
writable: true
};
Object.defineProperty(a, 'test', descriptor);
console.log(a.test); // 2
a.test = 3;
console.log(a.test); // 3
在例子中我们为用作属性描述符的descriptor添加了value属性,而如之前所介绍的,如果其他键值没有设置,则默认为false(此时该描述符被认为是一个数据描述符)。这时我们为a.test属性赋值,发现赋值没有按照预期失败,而是成功了。显然是因为descriptor继承了原型上的writable属性,因此属性a变为可写的。
你可能认为这种更改显而易见,但实际项目中往往较为复杂,提前意识到这些问题往往可以省下很多时间。
创建属性
如果对象中不存在指定的属性,Object.defineProperty()
就创建这个属性。当描述符中省略某些字段时,这些字段将使用它们的默认值。拥有布尔值的字段的默认值都是false。value,get和set字段的默认值为undefined。一个没有get/set/value/writable
定义的属性将被当做数据描述符处理。
var obj = {}; // 创建一个新对象
// 为新对象添加属性及其属性描述符(数据描述符)
Object.defineProperty(obj, 'prop', {
value: 10,
writable : true,
enumerable : true,
configurable : true
});
// 此时对象obj拥有了属性prop,值为10,且可修改
console.log(obj.prop); // 10
obj.prop = 20;
console.log(obj.prop); // 20
var obj2 = {}; // 创建一个新对象
// 为新对象添加属性及其属性描述符(存取描述符)
var val;
Object.defineProperty(obj, 'prop', {
get : function(){
return val;
},
set : function(newValue){
val = newValue;
},
enumerable : true,
configurable : true
});
obj2.prop = 20; // 为obj2设置值为20
console.log(obj2.prop); // 20
对于enumerable
和configurable
属性,将在后续详细说明。
修改属性
如果属性已经存在,Object.defineProperty()
将尝试根据描述符中的值以及对象当前的配置来修改这个属性。如果旧描述符将其configurable 属性设置为false,则该属性被认为是“不可配置的”,并且没有属性可以被改变(除了单向改变 writable 为 false
)。当属性不可配置时,不能在数据和访问器属性类型之间切换。
var obj = {}; // 创建一个新对象
// 为新对象添加属性及其属性描述符
Object.defineProperty(obj, 'prop', {
value: 10,
writable : true,
enumerable : true,
configurable : true
});
// 更改该对象的属性描述符
Object.defineProperty(obj, 'prop', {
writable : false
});
console.log(obj.prop); // 10
// Object.getOwnPropertyDescriptor()方法返回指定对象上一个自有属性对应的属性描述符。
//(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
console.log(Object.getOwnPropertyDescriptor(obj, 'prop'));
// {value: 10, writable: false, enumerable: true, configurable: true}
当试图改变不可配置属性(除了单向改变 writable 为 false
)的值时会抛出错误,除非当前值和新值相同。
var obj = {}; // 创建一个新对象
// 为新对象添加属性及其属性描述符
Object.defineProperty(obj, 'prop', {
value: 10,
writable : true,
enumerable : true,
configurable : false
});
// 在configurable为false时重新更改属性描述符
// 注意,如前所述,当将writable从true改为false是允许的
Object.defineProperty(obj, 'prop', {
enumerable : false
});
// Uncaught TypeError: Cannot redefine property: prop
下面具体解释属性描述符的可选键值
例子引用自MSDN:
Writable 属性
当writable属性设置为false时,该属性被称为“不可写”。此时无法为属性赋一个新值。在非严格模式下,试图写入非可写属性不会改变它,也不会引发错误。
var o = {}; // 创建一个新对象
Object.defineProperty(o, 'a', {
value: 37,
writable: false
});
console.log(o.a); // 37
o.a = 25; // 此时不会抛出错误
// 如果在严格模式下,即使重新赋的值与原来相同,仍旧会抛出错误
console.log(o.a); // 37 重新赋值不会起作用
// 严格模式下
(function() {
'use strict';
var o = {};
Object.defineProperty(o, 'b', {
value: 2,
writable: false
});
o.b = 3; // throws TypeError: Cannot assign to read only property 'b' of object '#
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'
}
console.log(Object.keys(o)); // ["a", "d"]
// propertyIsEnumerable() 方法返回一个布尔值,表示指定的属性是否可枚举。
o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
Configurable 属性
configurable属性性表示对象的属性是否可以被删除,以及除writable
属性外的其他属性是否可以被修改。
var o = {};
Object.defineProperty(o, "a", {
get: function () {
return 1;
},
configurable: false
});
// 抛出一个错误
Object.defineProperty(o, "a", { configurable: true });
// 抛出一个错误
Object.defineProperty(o, "a", { enumerable: true });
// 抛出一个错误 (set 熟悉之前未定义)
Object.defineProperty(o, "a", {
set: function () {
}
});
// 抛出一个错误 (即使get属性做了与之前相同的事情)
Object.defineProperty(o, "a", {
get: function () {
return 1;
}
});
// 抛出一个错误
Object.defineProperty(o, "a", { value: 12 });
console.log(o.a); // 1
delete o.a; // 不会起作用,严格模式下抛出错误TypeError: Cannot delete property 'a' of #
如果o.a的configurable属性为true,则不会抛出任何错误,并且该属性将在最后被删除。
set和get
在了解了getter和setterd的原理和使用之后,这里也便理解了get和set属性的用法。
同样使用MSDN上的例子,例子展示了如何实现一个自存档对象。 当设置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 }]
需要注意的地方
使用点.
运算符和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
});
相关的一些api
1.Object.defineProperties()
的用法和Object.defineProperty()
基本一样,不同的是可以为对象的多个属性添加描述符。
var obj = {};
Object.defineProperties(obj, {
'prop1': {
value: 1,
writable: true
},
'prop2': {
value: 2,
writable: false
}
});
2.Object.getOwnPropertyDescriptor()
方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找)。在一开始举例的时候就用到了这个api。
3.Object.getOwnPropertyDescriptors()
方法用来获取一个对象的所有自身属性的描述符。
Object.defineProperty()的重要意义
个人认为理解掌握Object.defineProperty()
对于JS从基础走向进阶具有帮助作用:
1.了解框架原理。如Vue
使用Object.defineProperty
用于构成其响应式系统的一部分。
2.阅读框架源码。Object.defineProperty
是你在阅读许多框架源码无法绕过的一个api。
3.了解babel
语法转换器转译原理。babel
的语法转换经常用到Object.defineProperty
。
4.做更多你想做的事:)