今天看啥  ›  专栏  ›  大束

从另一个角度看 Object

大束  · 掘金  ·  · 2019-08-21 16:23
阅读 22

从另一个角度看 Object

知其然,知其所以然。

有没有想过 JavaScript 中的 Object 到底是个什么东西

在没有探究 Object 之前,我是这样理解 Object 的: 数据的无序集合,里面包含 N 多个字段,每个字段保存对应的信息,而对象就是这些个信息无序集合体,何为无序? 无序 故名思议:没有顺序的,有 “无序”,就存在 “有序”。 我的理解是: 数组是数据的有序集合,N 多个数据按照数组的索引井然有序的排列,我们可以按照 “索引” 获取指定位置的数据。 Object 而言,Object 内部字段是由一个个键值对构成的,键是字段的名字,值也就是该字段的数据,在获取的时候,采用 Object[keyName] 的方式,也就获取到对应的字段存储的数据了。无论字段在对象内部的位置,都是采用 Object.keyName 的方式获取的,也就是说 对象内部是没有顺序的。

// 一个叫 person 的 Object,里面有三个字段,包含不同的数据,他们都属于 person 这个 Object
var person = {
  name: "Zyc",
  age: 18,
  sex: "man"
};
复制代码

Ecmascript 规范

Ecmascript 规范是所有 JavaScript 运行行为的权威来源;
无论是在你的浏览器环境、还是在服务器环境( Node.js )、还是在宇航服上[ NodeJS-NASA ]、或在你的物联网设备上[ JOHNNY-FIVE ];
所有 JavaScript 引擎的开发者都依赖于这个规范来确保他们各种天花乱坠的新特性能够其他 JavaScript 引擎一样,按预期工作。

Ecmascript 规范 绝不仅仅对 JavaScript 引擎开发者有用,
它对普通的 JavaScript 编码人员也非常有用,而你只是没有意识到或者没有用到。

Ps:你所用的 JavaScript,都是由这个规范来规定的,它说什么就是什么。
复制代码

从规范角度看对象

在 ECMAScript 规范中是这样定义对象的:

Object 是一个属性的集合。每个属性既可以是一个命名的数据属性,也可以是一个命名的访问器属性,或是一个内部属性

  • 命名数据属性: 由一个名字与一个 ECMAScript 语言类型值和一个 Boolean 属性集合组成。
  • 命名访问器属性: 由一个名字与一个或两个访问器函数,和一个 Boolean 属性集合组成。访问器函数用于存取一个与该属性相关联的 ECMAScript 语言类型 值。
  • 内部属性: 没有名字,且不能直接通过 ECMAScript 语言操作。内部属性的存在纯粹为了规范的目的;有两种带名字的访问器属性(非内部属性):get 和 put,分别对应取值和赋值。

规范就是规范,晦涩难懂。

属性是什么?

我们已知 Object 是属性的集合。是否有过这个疑问,属性又是什么呢?或许规范已经给了我们答案,告诉了我们属性是怎么构成的。但是,我没明白。。。

好,我们一步步分析下规范,回答下 属性是什么:

答案 1:属性是由一个名字与一个 ECMAScript 语言类型值和一个 Boolean 属性集合组成。

  • 一个名字: 显而易见使我们所熟知的 Key
  • 一个 ECMAScript 语言类型值: 也就是值,语言类型值也就是指 String、Number、Blooean、Object、Function... 等等 JavaScript 的语言类型。
  • 一个 Boolean 属性集合:??????????????

问题来了,什么是 Boolean 属性集合? 是不是从来没见过这个东西?


答案 2:由一个名字与一个或两个访问器函数,和一个 Boolean 属性集合组成。访问器函数用于存取一个与该属性相关联的 ECMAScript 语言类型 值。

  • 一个名字: 相同的
  • 一个或两个访问器函数:??????????????????
  • 一个 Boolean 属性集合:?????????????? 问题又来了,访问器函数? 听都没听说过? 更没见过? 这个答案也有 Boolean 属性集合?

再来看一下我们日常是怎么使用 Object 的:

// 表达式创建一个对象
var busInfoForm = {
  treeId: "",
  vcName: "",
  parentId: "0",
  unitId: "",
  iBindType: "",
  bindId: ""
};

// 查 - 获取一个属性
console.log(busInfoForm.treeId);

// 改 - 设置一个属性
busInfoForm.vcName = "测试的值";

// 删 - 删除一个属性
delete busInfoForm.bindId;

// 增 - 增加一个属性
busInfoForm["obj"] = {
  a: "this is obj a"
};

// 遍历属性
for (let k in busInfoForm) {
  console.log(k, "-----", busInfoForm[k]);
}
复制代码

我们对于数据的操作,无非 增删改查 定律,上面的例子体现了增删改查,以及枚举对象的属性操作. 我们可以看到对象属性的 名字、值, 但我们没有看到 Boolean 集合,也没有看到访问器函数。

属性描述符

这时候,我们需要知道另一个概念:属性描述符

在 MDN 中对于 属性描述符 的定义如下: 对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。

  • 数据描述符 是一个具有值的属性,该值可能是可写的,也可能不是可写的。
  • 存取描述符 是由 getter-setter 函数对描述的属性。
  • 描述符必须是这两种形式之一;不能同时是两者。

好吧,是不是懵了,属性描述符 又是什么?

继续看 MDN 上的对于属性描述符的介绍:

数据描述符和存取描述符均具有以下可选键值(默认值是在使用 Object.defineProperty()定义属性的情况下):

  • configurable
    • 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
  • enumerable
    • 当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:

  • value
    • 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  • writable
    • 当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。默认为 false。

存取描述符同时具有以下可选键值:

  • get
    • 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。默认为 undefined。
  • set
    • 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。
configurable enumerable value writable get set
数据描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

如果一个描述符不具有 value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value 或 writable)和(get 或 set)关键字,将会产生一个异常。


OK, 属性描述符的所有 "属性" 都罗列在了这里,白话一下: 对于对象中的每一个属性,都有其自己的 “描述符” ,所谓的描述符,也就是用来描述属性的行为的,描述属性是否可以增删改查,以及如何增删改查。描述符就像属性的辅助对象,这个对象和 Object 的属性结构一样由键值对构成,具体包含哪些键值对,如下所示: 数据描述符: 包含 configurable、enumerable、value、writable存取描述符: 包含 configurable、enumerable、get、set 键 可以看到,configurable、enumerable 是都含有的。 而对于 数据描述符 而言,除了公有的,只能存在 value && writable,反过来 存取描述符 只能存在 get、set,这四个属性两两之间是不能共存的。

操作属性描述符

Object.defineProperty()

现在,我们已经有了数据描述符的这个概念,下面来看下 Object.defineProperty() 这个方法

  • 定义:

    • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
  • 语法:

    • Object.defineProperty(obj, prop, descriptor)
  • 参数:

    • obj: 要在其上定义属性的对象
    • prop:要定义或修改的属性的名称
    • descriptor:将定义或修改的属性描述符
  • 返回值

    • 被传递给函数的对象,原对象

描述:

  • 该方法允许精确添加或修改对象的属性。
  • 通过赋值操作添加的普通属性是可枚举的,能够在属性枚举期间呈现出来(for...in 或 Object.keys 方法), 这些属性的值可以被改变,也可以被删除。
  • 这个方法允许修改默认的额外选项(或配置)。
  • 默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改的。

我们要操作属性描述符就得使用这个方法。

Object.getOwnPropertyDescriptor()

defineProperty 这个方法使用来设置属性描述的,对应的 getOwnPropertyDescriptor 就是用来获取属性描述符的。

  • 定义:
    • Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
  • 语法:
    • Object.getOwnPropertyDescriptor(obj, prop)
  • 参数:
    • obj:需要查找的目标对象
    • prop:目标对象内属性名称
  • 返回值:
    • 如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined。

描述:

  • 该方法允许对一个属性的描述进行检索。

上代码

创建属性

  • 如果对象中不存在指定的属性,Object.defineProperty()就创建这个属性。
  • 当描述符中省略某些字段时,这些字段将使用它们的默认值。
  • 拥有布尔值的字段的默认值都是 false。value,get 和 set 字段的默认值为 undefined。
  • 一个没有 get/set/value/writable 定义的属性被称为“通用的”,并被“键入”为一个数据描述符。
// 使用表达式的方式创建一个新对象
var person = {
  name: "Zyc"
};

// 使用 getOwnPropertyDescriptor 获取 name 的属性描述符
console.log(Object.getOwnPropertyDescriptor(person, "name")); // =>
// {value: "Zyc", writable: true, enumerable: true, configurable: true}
复制代码
  • 可以看到 通过赋值操作添加的普通属性,其属性描述符包含 configurable、enumerable、value、writable
  • 默认为 数据描述符
  • 且这些值除 value 外,皆为 true,说明该属性可以被 删除、修改、枚举
// 使用 defineProperty 在对象中添加一个属性 - 属性描述符取默认
Object.defineProperty(person, "sex", {});

console.log(person); // => {name: "Zyc", sex: undefined}

console.log(Object.getOwnPropertyDescriptor(person, "sex")); // =>
// {value: undefined, writable: false, enumerable: false, configurable: false}
复制代码
  • 使用 defineProperty 为 person 添加了一个 'sex' 属性
  • 第三个参数,传入了一个空对象
  • 可见该属性的 属性描述符 都取了默认值,且该属性默认为 数据描述符
person.sex = "man";
delete person.sex;

console.log(person); // {name: "Zyc", sex: undefined}
复制代码
  • 由于在 sex 在创建时,属性描述符都取的默认值,writable 为 false,故不能写入
  • 同样它也不可被枚举、删除
// 使用 defineProperty 在对象中添加一个属性 - 配置属性描述符
Object.defineProperty(person, "age", {
  value: 18,
  writable: true,
  enumerable: true,
  configurable: true
});
console.log(person); // {name: "Zyc", age: 18, sex: undefined}

person.age = 16;
console.log(person); // {name: "Zyc", age: 16, sex: undefined}

delete person.age;
console.log(person); // {name: "Zyc", sex: undefined}
复制代码
  • 使用 defineProperty 为对象添加了一个 'age' 属性。且配置了属性描述符
  • 可以看到对象多了一个 键为 "age" -- 值为 18 的属性
  • 同时,这个值可以被修改、删除、枚举
// 在对象中添加一个 属性与存取描述符
var hobbyValue = [];
Object.defineProperty(person, "hobby", {
  get: function() {
    return hobbyValue;
  },
  set: function(newValue) {
    hobbyValue.push(newValue);
  },
  enumerable: true,
  configurable: true
});

console.log(person.hobby); // => []

person.hobby = "吃饭";
person.hobby = "睡觉";
person.hobby = "码";
console.log(person.hobby); // => ["吃饭", "睡觉", "码"]
复制代码
  • 使用 defineProperty 为 person 对象添加了一个 名为 hobby 的属性
  • 属性的 属性描述符 包含 set、get,为存取描述符
  • get 在获取属性时调用该函数,返回值将作为属性获取的结果
  • set 在设置属性时调用该函数,接收一个参数为要设置的 值
  • configurable、enumerable 是通用的
// 数据描述符和存取描述符不能混合使用
Object.defineProperty(person, "conflict", {
  value: 0x9f91102,
  get: function() {
    return 0xdeadbeef;
  }
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors
复制代码

修改属性

  • 如果属性已经存在,Object.defineProperty()将尝试根据描述符中的值以及对象当前的配置来修改这个属性。
  • 如果旧描述符将其 configurable 属性设置为 false,则该属性被认为是“不可配置的”,并且没有属性可以被改变(除了单向改变 writable 为 false)。
  • 当属性不可配置时,不能在 数据访问器 属性类型之间切换。

2019/8/21 待续...




原文地址:访问原文地址
快照地址: 访问文章快照