属性描述符

针对 Object 的 property,数据描述符和访问器描述符

在 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 集合。

描述符
key

数据描述符

enumerable configurable

value writable

访问器描述符

enumerable configurable

get set

1.1 描述符的 key

  1. enumerable 布尔类型。当且仅当在枚举对象的属性列表时该属性需要被显示出来的时候,才会被置为 true。

  2. configurable 布尔类型。如果可以更改该属性的描述符类型(数据描述符 or 访问器描述符),且可以在对象上删除该属性的时候,就置为 true。

  3. value 与属性关联的值,可以是任何有效的 JavaScript 值。

  4. writable 布尔类型。如果可以使用赋值运算符来修改该属性的 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,要么使用对象字面量来显式指定描述符的值。如下:

// 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

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

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

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

以上代码,运行结果如下:

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,而 ... 不会

方法
自身属性
原型链

propertyIsEnumerable()

判断是否可枚举 (String+Symbol)

hasOwnProperty() Object.getOwnPropertyDescriptors() Reflect.ownKeys()

可枚举+不可枚举 (String+Symbol)

Object.getOwnPropertyNames()

可枚举+不可枚举 (仅 String)

Object.getOwnPropertySymbols()

可枚举+不可枚举 (仅 Symbol)

Object.keys()

可枚举 (仅 String 属性)

for..in 语句

可枚举 (仅 String 属性)

同前

in 操作符

可枚举+不可枚举 (String+Symbol)

同前

  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 语句

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

  • 是否可以更改该属性的描述符类型(数据/访问)

  • 是否可以在对象上删除该属性

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. 当属性不可写时

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. 当属性不可写时,严格模式下会报错

(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 普通函数对象

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 原型链

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. 总结

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

key
说明
反转状态

enumerable configurable

是否可枚举 是否可更改描述符类型+删除

DontEnum DontDelete

writable

value

是否可修改值(赋值)

属性的值

Read-only

get set

5. 扩展阅读

Last updated