Object.defineProperty() 语法
Object.defineProperty() 是 JavaScript 的一个静态方法,它可以为对象定义一个新属性,或修改对象已有属性。
语法:
Object.defineProperty(obj, prop, desc)说明:
Object.defineProperty() 方法接收以下 3 个参数。
obj(必选):对象名。prop(必选):属性名,这是一个字符串。desc(必选):配置对象,用于对属性的描述。
Object.defineProperty() 的第 3 个参数是一个对象,用于对属性进行各种配置。对于这个配置对象来说,它常用的选项如下表 1 和表 2 所示。这些选项又被称之为 “描述符”。
| 属性 | 说明 |
|---|---|
| value | 属性的值,默认为 undefined |
| configurable | 是否允许被删除,默认为 false |
| enumerable | 是否允许被遍历,默认为 false |
| writable | 是否允许被修改,默认为 false |
| 方法 | 说明 |
|---|---|
| get() | 即 getter |
| set() | 即 setter |
表中的默认值,只有在 “定义新属性” 时才会生效。如果是修改 “已有属性”,则未显式声明的配置项会保持原有的值不变。
注意:
- defineProperty() 是一个静态方法,它只能被类名(即 Object)调用,而无法被实例调用。
- Object.defineProperty() 方法一次性只能定义一个属性,而 Object.defineProperties() 方法可以同时定义多个属性。
Object.defineProperty() 摘要
| 属于 | JavaScript Object 对象 |
|---|---|
| 使用频率 | 中 |
| 官方文档 | 查看 |
| MDN | 查看 |
Object.defineProperty() 示例
接下来,我们通过一个简单的例子来讲解 Object.defineProperty() 方法是如何使用的。
示例 1:Object.defineProperty() 基本用法
const person = {
name: "Jack"
};
Object.defineProperty(person, "age", {
value: 20,
writable: false
});
console.log(person.age);运行结果如下。
20分析:
上面例子使用 Object.defineProperty() 方法来为 person 对象定义了一个 age 属性。value: 20 表示 age 属性取值为 20,writable: false 表示 age 属性的值不允许被修改。
如果我们尝试修改 age 属性值,比如执行 person.age=30;,此时:
- 在非严格模式下,赋值操作会被忽略,值保持 20 不变(不会报错)。
- 在严格模式("use strict")下,则会抛出 TypeError 错误。
Object.defineProperty() 的配置对象
我们都知道,Object.defineProperty() 的第 3 个参数是一个对象,用于对属性进行各种配置。
1. configurable 选项
在配置对象中,我们可以使用 configurable 选项来定义属性是否允许被删除。其中 configurable 默认值为 false,也就是不允许被删除。
示例 2:configurable 选项
const person = {};
Object.defineProperty(person, "name", {
value: "Jack",
});
delete person.name;
console.log(person);运行结果如下。
{ name: "Jack" }分析:
这里小伙伴肯定会有这样一个疑问:“默认情况下,我们是可以使用 delete 来删除对象属性的,为什么这里的 delete 没有生效呢?”
这是因为在传统的方式中,我们都是使用点运算符(.)来定义一个属性,这种方式默认情况下是可以直接使用 delete 操作符来删除的,比如:
const person = {};
person.name = "Jack";
delete person.name;
console.log(person); // {}但是在这个例子中,我们却是使用 Object.defineProperty() 方法来定义一个属性。configurable 默认值是 false,也就是不允许 delete 删除。如果想要允许 delete 删除,就应该显式声明 configurable 的值为 true。
const person = {};
Object.defineProperty(person, "name", {
value: "Jack",
configurable: true
});
delete person.name;
console.log(person); // {}2. enumerable 选项
在配置对象中,我们可以使用 enumerable 选项来定义属性是否允许被遍历,也就是是否允许在 for...in 或 Object.keys() 中被枚举。其中 enumerable 属性的默认值为 false,也就是不允许被遍历。
示例 3:enumerable 选项
const person = {};
Object.defineProperty(person, "name", {
value: "Jack",
enumerable: true
});
Object.defineProperty(person, "age", {
value: 24,
enumerable: false
});
// for...in
for(let key in person) {
console.log(key);
}
// Object.keys()
const keyArr = Object.keys(person);
console.log(keyArr);运行结果如下。
name
["name"]分析:
在这个例子中,我们使用 Object.defineProperty() 方法来为 person 对象定义了 name 和 age 这两个属性。其中 name 属性是允许被枚举的,而 age 属性是不允许被枚举的。
3. writable 选项
在配置对象中,我们可以使用 writable 来定义属性是否允许被重新赋值。其中 writable 属性的默认值为 false,也就是不允许被重新赋值。
示例 4:writable 选项
const person = {};
Object.defineProperty(person, "name", {
value: "Jack",
writable: true
});
person.name = "Lucy";
console.log(person);运行结果如下。
{ name: "Lucy" }分析:
在这个例子中,我们使用 Object.defineProperty() 方法来为 person 对象定义了一个 name 属性。由于 writable 的默认值为 false,如果想要使得 name 属性允许被重新赋值,就要显式声明 writable 的值为 true。
4. get() 和 set()
在配置对象中,get() 表示读取属性时会 “自动” 调用的函数,set() 表示写入属性时会 “自动” 调用的函数。
示例 5:get() 和 set()(改进前)
const person = {
_name: "Jack"
};
Object.defineProperty(person, "name", {
get() {
return this._name;
},
set(value) {
this._name = value;
}
});
console.log(person.name);
person.name = "Lucy";
console.log(person.name);运行结果如下。
Jack
Lucy分析:
在这个例子中,我们为 person 对象定义了一个私有属性 _name,私有属性命名一般是以 “_” 开头。接下来,我们再使用 Object.defineProperty() 为 person 对象定义了一个 name 属性。
name 和 _name 这两个属性的值是绑定在一起了,即 person.name = person._name。这样就实现了双向数据绑定。上面实现方式并不是很优雅,我们将其改进一下,请看下面例子。
示例 6:get() 和 set()(改进后)
function Person() {
let _name = "Jack";
Object.defineProperty(this, "name", {
get() {
return _name;
},
set(value) {
_name = value;
}
})
}
const p = new Person();
console.log(p.name);
p.name = "Lucy";
console.log(p.name);运行结果如下。
Jack
Lucy分析:
从本质上来说,上面两种方式是没有什么区别的,但是这个例子更加优雅一些。使用 get() 和 set() 可以实现数据双向绑定,鼎鼎大名的 Vue.js 的 2.x 版本中的双向数据绑定就是使用 Object.defineProperty() 中的 get() 和 set() 来实现的,请看下面例子。
示例 7:双向数据绑定
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script>
window.onload = function() {
const oTxt = document.getElementById("txt");
const oContent = document.getElementById("content");
// 定义一个对象
const obj = {};
Object.defineProperty(obj, "text", {
get() { },
set(value) {
oTxt.value = value;
oContent.innerText = value;
}
});
// 文本框的keyup事件
oTxt.addEventListener("keyup", function(e) {
obj.text = e.target.value;
}, false);
}
</script>
</head>
<body>
<input id="txt" type="text" />
<p id="content"></p>
</body>
</html>默认情况下,浏览器效果如图 1 所示。当我们在文本框输入内容后,浏览器效果如图 2 所示。


分析:
上面实现了一个极简版的双向数据绑定。首先我们定义了一个 obj 对象,然后使用 Object.defineProperty() 方法为这个对象定义了一个 text 属性。从 set() 中可以看到,当我们给 obj.text 属性设置一个新值时,会同时改变 input 元素以及 p 元素的值。
实际上,上面的 obj 就像是一个中间代理。这里只是演示了一下双向数据绑定的原理,我们简单看一下就行。Vue 2.x 是使用 Object.defineProperty() 来实现双向数据绑定,但是 Vue 3.x 却是使用更为强大的 Proxy 对象来实现。对于 Proxy,我们在后续章节会详细介绍。
数据属性和访问器属性
在 JavaScript 中,对象的属性可以分为 2 种:数据属性和访问器属性,两者的区别如下。
1. 数据属性
数据属性是包含数据值的,它的值是通过 value 和 writable 来进行配置。其中,数据属性只能使用 configurable、enumerable、value、writable 这几种描述符。
2. 访问器属性
访问器属性是不包括数据值的,它的值是通过 get() 和 set() 来进行配置。其中,访问器属性只能使用 configurable、enumerable、get()、set() 这几种描述符。
示例 8:数据属性和访问器属性
const person = {
_lastName: "Mo"
};
// 数据属性
Object.defineProperty(person, "firstName", {
configurable: true,
enumerable: true,
value: "Jack",
writable: true
});
// 访问器属性
Object.defineProperty(person, "lastName", {
configurable: true,
enumerable: true,
get() {
return this._lastName;
},
set(value) {
this._lastName = value;
}
});
console.log(person.firstName);
console.log(person.lastName);运行结果如下。
Jack
Mo分析:
在这个例子中,我们使用 Object.defineProperty() 方法为 person 对象定义了 firstName 和 lastName 这 2 个属性。firstName 是一个数据属性,而 lastName 是一个访问器属性。
此外我们要注意一点,数据属性的 value、writable 和访问器属性的 get()、set() 不能同时使用,否则就会报错。
示例 9:同时使用所有数据属性
const person = {};
// 数据属性
Object.defineProperty(person, "name", {
configurable: true,
enumerable: true,
value: "Jack",
writable: true,
get() {},
set() {}
});
console.log(person.name);运行结果如下。
(报错)Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>分析:
从控制台可以看出来,get()、set() 这两个和 value、writable 是不能共存的,否则就会报错。之所以不能共存,那是因为数据属性只能通过 value、writable 来配置属性值,而访问器属性只能通过 get()、set() 来配置属性值。
Object.defineProperty() 与点运算符的区别
最后我们来深入了解一下 Object.defineProperty() 和点运算符(.),看看两者到底有什么区别。
示例 10:Object.defineProperty() vs 点运算符
const person = {};
person.name = "Jack";
// 允许被重新赋值
person.name = "Lucy";
// 允许被遍历
for(let key in person) {
console.log(key);
}
// 允许被删除
delete person.name;
console.log(person);运行结果如下。
name
{}分析:
点运算符定义的属性,默认是允许被重新赋值、允许被遍历、允许被删除。也就是说,下面 2 种方式是等价的。
// 方式1
person.name = "Jack";
// 方式2
Object.defineProperty(person, "name", {
configurable: true,
enumerable: true,
value: "Jack",
writable: true
});在这一节中,我们花了那么大的篇幅来介绍 Object.defineProperty(),那么它到底有什么用呢?实际上很多框架的源码都用到了这个方法,比如 Vue 中的数据劫持。这里给小伙伴们详细梳理一遍,后面再去接触 Vue 或 React 的源码,就变得非常轻松了。
