对象属性配置

属性标志和属性描述符

属性标志

对象的属性除了 value 之外,还有三个其余特性:

  • writable:为true则表示可以被修改,否则为只读;
  • enumerable:为true表示可被循环列出,否则不可被迭代循环展示;
  • configurable:为true表示当前属性可被修改或者删除,否则禁止被操作;

    configurable置为false之前,建议先把writable置为false,避免误修改。

  1. 查询对象属性的完整属性

    obj:需要从中获取信息的对象。

    propertyName:属性的名称。

    let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
    

    示例

    let user = {
    name: "John"
    };
    
    let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
    
    /* 完整的属性描述符:
    {
        "value": "John",
        "writable": true,
        "enumerable": true,
        "configurable": true
    }
    */
    console.info( JSON.stringify(descriptor, null, 2 ) );
    
  2. 修改属性标志

    obj:要应用描述符的对象。

    propertyName:要应用描述符的对象属性。若属性不存在则自动创建,否则进行修改。

    如果是新属性,且属性描述符中未指明的属性则默认为false。

    descriptor:要应用的属性描述符对象。

    Object.defineProperty(obj, propertyName, descriptor)
    
    • 只读 示例

      let user = {
          name: "John"
      };
      
      Object.defineProperty(user, "name", {
          writable: false
      });
      // 仅在严格模式下,非严格模式会自动忽略
      user.name = "Pete"; // Error: Cannot assign to read only property 'name'
      
    • 不可枚举 示例

      配置不可枚举后,Object.keys也会将其排除展示。

      let user = {
          name: "John",
          toString() {
              return this.name;
          }
      };
      // 默认情况下,我们的两个属性都会被列出:
      for (let key in user) console.info(key); // name, toString
      
      Object.defineProperty(user, "toString", {
          enumerable: false
      });
      
      // 现在我们的 toString 消失了:
      for (let key in user) console.info(key); // name
      
    • 不可配置 示例

      不可配置的属性不能被删除,它的特性(attribute)不能被修改。

      这条配置为单行道,一旦设置,无法再次修改

      一般作为内建属性使用。

      但不允许配置的属性,一般是只读的,因此需要提前将writable置为false。

      let user = {
          name: "John"
      };
      
      Object.defineProperty(user, "name", {
          configurable: false
      });
      // 依旧可以正常修改
      user.name = "Pete"; // 正常工作
      delete user.name; // Error
      
  3. 批量修改属性描述符
    语法:

    Object.defineProperties(user, {
        name: { value: "John", writable: false },
        surname: { value: "Smith", writable: false },
        // ...
    });
    
  4. 一次性获取所有属性描述符

    Object.getOwnPropertyDescriptors(obj)
    

    因此我们可以实现对一个对象的完整拷贝
    (包括symbol 类型的和不可枚举的属性在内的 所有 属性描述符)

    let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
    

全局密封对象

1. 禁止扩展(仅防新增)

Object.preventExtensions(obj);   // 设置
Object.isExtensible(obj);        // 测试

2. 密封(禁止新增/删除,全部 configurable→false)

Object.seal(obj);                // 设置
Object.isSealed(obj);            // 测试

3. 冻结(密封 + 全部 writable→false)

Object.freeze(obj);              // 设置
Object.isFrozen(obj);            // 测试

4. 查看属性描述符

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
console.info(JSON.stringify(descriptor, null, 2));

示例

let user = { name: "John" };

// 冻结对象(最严格)
Object.freeze(user);

// 查看描述符
console.info(Object.getOwnPropertyDescriptor(user, 'name'));
// {
//   "value": "John",
//   "writable": false,
//   "enumerable": true,
//   "configurable": false
// }

// 测试状态
console.info(Object.isFrozen(user)); // true
console.info(Object.isSealed(user)); // true
console.info(Object.isExtensible(user)); // false

访问器属性

对象属性存在两种类型:

  • 数据属性:之前的所有属性皆为数据属性
  • 访问器属性:本质上是用于获取和设置值的函数,但外部的表现即为常规属性

Getter 与 Setter

常规使用语法:
get与set并不是需要同时出现,当只允许读取时,可以只设置get,当允许设置,但无需读取时,可以仅设置set。

// 定义
let obj = {
  get propName() {
    // 当读取 obj.propName 时,getter 起作用
  },

  set propName(value) {
    // 当执行 obj.propName = value 操作时,setter 起作用
  }
};

可以像常规属性一样进行使用

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};
// 但不能对fullname进行赋值操作,因为没有set
console.info(user.fullName); // John Smith

访问器描述符

  • get —— 一个没有参数的函数,在读取属性时工作,
  • set —— 带有一个参数的函数,当属性被设置时调用,
  • enumerable —— 与数据属性的相同,
  • configurable —— 与数据属性的相同。

使用 defineProperty 创建一个 fullName 访问器:

let user = {
  name: "John",
  surname: "Smith"
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

console.info(user.fullName); // John Smith

// 未指明的情况下,默认设置false。不进行迭代循环
for(let key in user) console.info(key); // name, surname

注意:一个属性只能是数据属性或者访问器属性,不可能同时具有。

更灵活的getter/setter

利用社区规范,采用内部属性。

从技术上讲,外部代码可以使用 user._name 直接访问 name。
但是,这儿有一个众所周知的约定,即以下划线 "_" 开头的属性是内部属性,
不应该从对象外部进行访问。

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      console.info("Name is too short, need at least 4 characters");
      return;
    }
    this._name = value;
  }
};

user.name = "Pete";
console.info(user.name); // Pete

user.name = ""; // Name 太短了……