anjia / blog

博客,积累与沉淀
106 stars 4 forks source link

JavaScript 的 Object #93

Open anjia opened 2 years ago

anjia commented 2 years ago

目录

  1. Object 的属性描述符
    • 描述符:
      • enumerable
        • 可枚举 + 不可枚举
        • String属性 + Symbol属性
        • 自身属性 + 原型属性
      • configurable
      • value, writable 数据描述符
      • get, set 访问器描述符
    • 相关方法
      • Object.defineProperty()
      • Object.getOwnPropertyDescriptor()
      • Object.getOwnPropertyDescriptors()
anjia commented 2 years ago

一. Object 的属性描述符

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

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

有一部分特殊的 attributes 是 property 的描述符,我们可以在 Object.defineProperty() 方法中直观地看到,此方法允许我们精准地添加或修改对象的属性及其描述符。

Object.defineProperty()

静态方法 Object.defineProperty() 可以在对象上定义新的属性,也可以修改已有的属性,它会返回本对象。

格式:Object.defineProperty(obj, prop, descriptor)

两种描述符

对象中的属性描述符有两种形式:数据描述符和访问器描述符。数据描述符是具有值的属性(可写或不可写),访问器描述符是由一对 getter-setter 函数描述的属性。

data descriptors
accessor descriptors

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

数据描述符和访问器描述符都是对象,即 key:value 的集合,它们都有下面的两个 key:

数据描述符还可以有以下 key:

访问器描述符还可以有以下 key:

如果描述符既没有 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'
});

默认值

Object.defineProperty() 定义的描述符的默认值分别是:

也就是说,通过此方法添加的属性,默认是不可枚举、不可变的。而通过赋值添加的普通属性,默认是可枚举、可删除可修改的。

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

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

enumerable

是否可枚举

可枚举属性就是指该属性的 enumerable 描述符为 true 的属性。当我们说一个属性是可枚举的,就意味着:

方法 自身属性 原型链
检测 propertyIsEnumerable() ✔️ 判断是否可枚举(String 和 Symbol)
hasOwnProperty() ✔️ 可枚举+不可枚举(String 和 Symbol)
in 操作符 ✔️ 可枚举+不可枚举(String 和 Symbol) ✔️ 同自身
检索 Object.getOwnPropertyDescriptors()
Reflect.ownKeys()
✔️ 可枚举+不可枚举(String 和 Symbol)
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
✔️ 可枚举+不可枚举(仅 String 属性)
✔️ 可枚举+不可枚举(仅 Symbol 属性)
Object.keys() ✔️ 可枚举(仅 String 属性)
迭代/枚举 for..in 语句 ✔️ 可枚举(仅 String 属性) ✔️ 同自身
  • detecting object properties
    • Object.prototype.propertyIsEnumerable()
    • Object.prototype.hasOwnProperty()
  • retrieving .. ..
  • iterating/enumerating .. ..
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));

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"; }
});

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>'
})();

看几个例子

eg1. 普通函数对象

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);

eg2. 带继承的(原型链)

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

总结

本文重点介绍了对象属性的描述符,内容如下:

描述符 key 说明 反转状态
数据描述符
访问器描述符
enumerable
configurable
是否可枚举
是否可更改描述符类型+删除
DontEnum
DontDelete
数据描述符 value
writable
属性的值
是否可修改值(赋值)
 
Read-only
访问器描述符 get
set

扩展阅读