> For the complete documentation index, see [llms.txt](https://anjia1.gitbook.io/web/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://anjia1.gitbook.io/web/js/data-type/object/property-descriptors.md).

# 属性描述符

在 JavaScript 中， 对象（Object）可以看作是属性（property）的集合，属性是 `key:value` 的形式。`key` 可以是字符串也可以是 Symbol，`value` 可以是任何类型（包括其它对象）。

对象的每个属性（property）都有对应的 attributes（属性），由于 attributes 是在 JavaScript 引擎内部使用的，所以我们并不能直接访问到它们。为了区分两者，通常情况下 `[property]` 是用单方括号，`[[attribute]]` 是用双方括号。

有一部分特殊的 attributes 是 property 的描述符，我们可以在静态方法 `Object.defineProperty()` 中直观地看到，它允许我们精准地添加和修改对象的属性及其描述符。此方法可以在对象上定义新的属性，也可以修改已有的属性，格式是 `Object.defineProperty(obj, prop, descriptor)`。

## 1. 两种描述符

对象中的属性描述符有两种形式：数据描述符和访问器描述符。

* data descriptors，数据描述符是具有值的属性（可写或不可写）
* accessor descriptors，访问器描述符是由一对 getter-setter 函数描述的属性

属性描述符要么是数据描述符，要么是访问器描述符，不能两者兼而有之。

不论是数据描述符还是访问器描述符，它们都是对象，即 `key:value` 集合。

<table><thead><tr><th width="151">描述符</th><th>key</th></tr></thead><tbody><tr><td>数据描述符</td><td><p><code>enumerable</code><br><code>configurable</code></p><p><code>value</code><br><code>writable</code></p></td></tr><tr><td>访问器描述符</td><td><p><code>enumerable</code><br><code>configurable</code></p><p><code>get</code><br><code>set</code></p></td></tr></tbody></table>

### 1.1 描述符的 key

1. `enumerable` 布尔类型。当且仅当在枚举对象的属性列表时该属性需要被显示出来的时候，才会被置为 true。
2. `configurable` 布尔类型。如果可以更改该属性的描述符类型（数据描述符 or 访问器描述符），且可以在对象上删除该属性的时候，就置为 true。
3. `value` 与属性关联的值，可以是任何有效的 JavaScript 值。
4. `writable` 布尔类型。如果可以使用[赋值运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators#assignment_operators)来修改该属性的 `value`，那就置为 true。
5. `get` 用作属性的 getter 函数，如果没有则为 undefined。当访问该属性的值时，就会调用此函数（不带参数），并将 `this` 设置为访问该属性的对象，返回值将被用作是该属性的值。
6. `set` 用作属性的 setter 函数，如果没有则为 undefined。当设置（assign）该属性的值时，就会调用此函数（带一个参数，即分配给属性的值），并将 `this` 设置为给该属性分配值的对象。

如果描述符既没有 `value`, `writable` 也没有 `get`, `set`，就会将其视为数据描述符。如果描述符既有 `value/writable` 也有 `get/set`，则会抛出异常。

注意：这些 attributes 不一定是描述符自身的 properties，也可能是继承来的 properties。所以保险起见，要么使用 `Object.create(null)` 让描述符指向 `null`，要么使用对象字面量来显式指定描述符的值。如下：

```javascript
// 1. Object.create(null)
let obj = {};
let descriptor = Object.create(null); // no inherited properties
descriptor.value = 'static';
Object.defineProperty(obj, 'key1', descriptor);

// 2. 用对象字面量，显式指定
Object.defineProperty(obj, 'key2', {
    enumerable: false,
    configurable: false,
    writable: false,
    value: 'static'
});
```

### 1.2 描述符的默认值

用 `Object.defineProperty()` 定义的描述符，默认值分别是：

* `enumerable`, `configurable`, `writable` 均默认是 false
* `value`, `get`, `set` 均默认是 undefined

也就是说，通过此方法添加的属性，默认是不可枚举、不可变的。

而通过赋值添加的普通属性，默认是可枚举、可删除可修改的。

```javascript
let person = {
    name: "David"
};
person.age = 34;
Object.defineProperty(person, "sex", { value: "male" });
console.log(Object.getOwnPropertyDescriptors(person)); // 详见下方截图
```

以上代码，运行结果如下：

![](https://user-images.githubusercontent.com/7055639/153361921-3b257f7c-5b05-4bb5-9374-7c2c8c542de9.png)

## 2. 重点介绍

### 2.1 enumerable

可枚举属性，是指该属性的 `enumerable` 描述符为 true 的属性。

当我们说一个属性是可枚举的，就意味着：

1. 对于非 Symbol 属性，可枚举的属性能出现在 `for...in` 循环和 `Object.keys()` 中
2. `Object.assign()` 和 spread 运算符 `...` 能访问到
   * `Object.assign()` 会将所有可枚举的自身属性从一个或多个源对象复制到目标对象，然后返回修改后的目标对象。它在源对象上使用 `[[Get]]`，在目标对象上使用 `[[Set]]`，它调用的是 getter 和 setter，因此它是分配（assign）属性而不是复制或定义新属性。
     * 如果还想复制属性的描述符，可以使用 `Object.defineProperty()` 和 `Object.getOwnPropertyDescriptor()`。
   * `...` 对于对象字面量，会将自己的可枚举属性复制到新对象
     * 现在可以使用比 `Object.assign()` 更短的语法进行对象的浅克隆或合并（不包含原型链）
     * `Object.assign()` 会触发 setter，而 `...` 不会

<table><thead><tr><th width="364.5128205128205">方法</th><th width="285">自身属性</th><th>原型链</th></tr></thead><tbody><tr><td><code>propertyIsEnumerable()</code></td><td>判断是否可枚举 (String+Symbol)</td><td>❌</td></tr><tr><td><code>hasOwnProperty()</code><br><code>Object.getOwnPropertyDescriptors()</code><br><code>Reflect.ownKeys()</code></td><td>可枚举+不可枚举 (String+Symbol)</td><td>❌</td></tr><tr><td><code>Object.getOwnPropertyNames()</code></td><td>可枚举+不可枚举 (仅 String) </td><td>❌</td></tr><tr><td><code>Object.getOwnPropertySymbols()</code></td><td>可枚举+不可枚举 (仅 Symbol)</td><td>❌</td></tr><tr><td><code>Object.keys()</code></td><td>可枚举 (仅 String 属性)</td><td>❌</td></tr><tr><td><code>for..in</code> 语句</td><td>可枚举 (仅 String 属性)</td><td>同前</td></tr><tr><td><code>in</code> 操作符</td><td>可枚举+不可枚举 (String+Symbol)</td><td>同前</td></tr></tbody></table>

1. detecting object properties, 检测
   1. `Object.prototype.propertyIsEnumerable()`
   2. `Object.prototype.hasOwnProperty()`
   3. `in` 操作符
2. retrieving ... ..., 检索
   1. `Object.getOwnPropertyDescriptors()`
   2. `Reflect.ownKeys()`
   3. `Object.getOwnPropertyNames()`
   4. `Object.getOwnPropertySymbols()`
   5. `Object.keys()`
3. iterating/enumerating ... ..., 迭代/枚举
   1. `for..in` 语句

```javascript
let obj = {
    1: "111"
};
obj["2"] = "222";
Object.defineProperty(obj, "3", { value: "333" });
Object.defineProperty(obj, "4", {
    value: "333",
    enumerable: true
});
Object.defineProperty(obj, "5", {
    get() { return "555" }
});
Object.defineProperty(obj, "6", {
    get() { return "666" },
    enumerable: true
});

const mySymbol = Symbol("7");
obj[mySymbol] = "777";

const mySymbol2 = Symbol("8");
Object.defineProperty(obj, mySymbol2, { value: "888" });

obj["9"] = "999";

console.log(Object.keys(obj)); // ['1', '2', '4', '6', '9']
console.log(Object.getOwnPropertyNames(obj));   // ['1', '2', '3', '4', '5', '6', '9']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(7), Symbol(8)]
for (let i in obj) {
    console.log(i); // 1, 2, 4, 6, 9
}

console.log(obj.propertyIsEnumerable(1)); // true
console.log(obj.propertyIsEnumerable(mySymbol));  // true
console.log(obj.propertyIsEnumerable(mySymbol2)); // false

console.log(obj);
console.log(Object.getOwnPropertyDescriptors(obj));
```

### 2.2 configurable

* 是否可以更改该属性的描述符类型（数据/访问）
* 是否可以在对象上删除该属性

```javascript
let person = {};
Object.defineProperty(person, "job", {
    get() { return "programmer"; },
    configurable: true
});
Object.defineProperty(person, "play", {
    value: "football",
    configurable: false
});
Object.defineProperty(person, "child", {
    configurable: true
});

delete person.job;  // 会生效
delete person.play; // 不会生效

Object.defineProperty(person, "child", {  // 会生效
    get() { return "Lily"; }
});
```

### 2.3 writable

是否可以使用赋值运算符修改该属性的 value。

eg. 当属性不可写时

```javascript
let person = {};
Object.defineProperty(person, "sex", {
  value: "male",
  writable: false
});
console.log(person.sex); // "male"
person.sex = "female"; // No error thrown
console.log(person.sex); // "male"
```

eg. 当属性不可写时，严格模式下会报错

```javascript
(function() {
  "use strict";
  let person = {};
  Object.defineProperty(person, "sex", {
    value: "male",
    writable: false
  });
  console.log(person.sex);
  person.sex = "female"; // TypeError: Cannot assign to read only property 'sex' of object '#<Object>'
})();
```

## 3. 例子

### 3.1 普通函数对象

```javascript
function Person(name, initSalary) {
    let salary = initSalary;

    Object.defineProperty(this, "name", { value: name });
    Object.defineProperty(this, "sex", {
        value: "male",
        writable: false,
        enumerable: true,
        configurable: false
    });
    Object.defineProperty(this, "salary", {
        get() {
            return salary;
        },
        set(newValue) {
            salary = newValue;
        },
        enumerable: true,
        configurable: true
    });
}

let person1 = new Person('David', 1000);
let person2 = new Person('John', 2000);
```

### 3.2 原型链

```javascript
function Person() { }

// 访问器属性，需要存在新定义的变量上，否则就在原型链上（所有后代共享）
Object.defineProperty(Person.prototype, "eat", {
    get() {
        return this._eat;
    },
    set(val) {
        this._eat = val;
    }
});

// 数据属性，本就在对象自身上（而不在原型链上）
Object.defineProperty(Person.prototype, "hair", {
    value: "black",
    writable: true
});

let p1 = new Person();
let p2 = new Person();

// 访问器属性
console.log(p1.eat, p2.eat); // undefined undefined
p1.eat = "rice";
console.log(p1.eat, p2.eat); // rice undefined

// 数据属性
console.log(p1.hair, p2.hair); // black black
p1.hair = "red";
console.log(p1.hair, p2.hair); // red black
```

## 4. 总结

这部分重点介绍了对象属性的描述符，内容如下：

<table><thead><tr><th width="183.33333333333331">key</th><th width="246">说明</th><th>反转状态</th></tr></thead><tbody><tr><td><code>enumerable</code><br><code>configurable</code></td><td>是否可枚举<br>是否可更改描述符类型+删除</td><td><code>DontEnum</code><br><code>DontDelete</code></td></tr><tr><td><p><code>writable</code></p><p><code>value</code></p></td><td><p>是否可修改值（赋值）</p><p>属性的值</p></td><td><p><code>Read-only</code></p><p>  </p></td></tr><tr><td><code>get</code><br><code>set</code></td><td></td><td></td></tr></tbody></table>

## 5. 扩展阅读

* <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty>
* <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties>
