JavaScript Object.defineProperty() 方法

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...inObject.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 的源码,就变得非常轻松了。

上一篇: Object.create()

下一篇: Object.defineProperties()

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号