关于对象属性

75 阅读6分钟

引言

在探究 Vue 响应式原理之前,笔者想先弄懂 Object.defineProperty 和 Proxy,毕竟它们是 Vue3 和 Vue2 响应式的一大区别。初学 JS 时,笔者对这些概念一知半解,现在该弄明白了

对象属性的分类

JS 对象属性有 数据属性(Data Property)访问器属性(Accessor Property) 两种

  • 数据属性:就是我们平常最常见的那种,有一个具体的值
  • 访问器属性:没有固定的值,而是通过函数(getter / setter)动态计算

属性描述符

对象属性具有 属性描述符,之所以对象属性会有这两类,归根结底是属性描述符造成的。属性描述符有两种,数据描述符访问器描述符。关于这些,MDN 中有较为详尽的描述:Object.defineProperty() - JavaScript | MDN。笔者将 MDN 的主要部分整理如下:

  • 数据描述符:是一个具有可写或不可写值的属性
  • 访问器描述符:属性值不是直接存储,而是通过 getter / setter 函数动态计算或设置

描述符可以通过 Object.defineProperty 的第三个参数设置,也可以通过 Object.defineProperties 批量设置

描述符都是对象,只能是两种类型之一,不能同时为两者。它们共享以下可选键:

  • configurable

    默认值为 false

    当设置为 false 时:

    • 该属性的类型不能在数据属性和访问器属性之间更改
    • 该属性不可被删除
    • 其描述符的其他属性也不能被更改(但是,如果它是一个可写的数据描述符,则 value 可以被更改,writable 可以更改为 false)。
  • enumerable

    默认值为 false

    false 时,该属性不可枚举,用 for...in、Object.keys 是遍历不出来的。(关于可枚举与可迭代,可以查看本篇的 可枚举与可迭代

此外,对于 数据描述符,有以下可选键值:

  • value

    默认值为 undefined

    与属性相关联的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)

  • writable

    默认值为 false

    如果与属性相关联的值可以使用 赋值运算符 更改,则为 true

访问器描述符还具有以下可选键值:

  • get 默认值为 undefined

    用作属性 getter 的函数。当访问该属性时,将不带参地调用此函数,并将 this 设置为通过该属性访问的对象。返回值将被用作该属性的值

  • set

    默认值为 undefined

    用作属性 setter 的函数。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this 设置为通过该属性分配的对象

如果描述符没有 valuewritableget 和 set 键中的任何一个,它将被视为数据描述符。如果描述符同时具有 [value 或 writable] 和 [get 或 set] 键,则会抛出异常

它们是可以转换的,通过提供不同类型的描述符,可以在数据属性和访问器属性之间切换。例如,如果新描述符是数据描述符(带有 value 或 writable),则原始描述符的 get 和 set 属性都将被删除

关于属性访问器的行为

// 读取
obj.f // get f

// 写入
obj.f = 's' // set f

// 删除
delete obj.f

可以发现:

  • 单纯的读取时,只有 get 被触发了
  • 写入时,只有 set 属性被触发了。笔者一开始还以为会先触发 get 再触发 set,结果不是

笔者注:这涉及到 JS 的赋值机制。可以把赋值式等号左边的叫左值,右边的叫右值。左值提供槽位,右值提供数据。在执行赋值时,只需要知道左值那个槽位,直接覆盖,而不需要知道它原来的值,所以不会触发 getter

  • 删除时,谁也不触发,只是会检查 configurable 属性

实践

我们可以打印一下试试,由于省略的选项默认值是 false,笔者就直接不写了。以下操作均在浏览器控制台进行

const obj = {}
let _name = '7'

Object.defineProperties(obj, {

    a: {
        configurable: true,
        writable: true,
        value: 'a',
        enumerable: true
    },
    
    // 不可配置,但可枚举
    b: {
        writable: true,
        value: 'b',
        enumerable: true
    },
    
    // 不可配置,不可枚举,不可写
    c: {
        value: 'c'
    },
    
    // 可配置,可枚举,不可写
    d: {
        configurable: true,
        value: 'd',
        enumerable: true
    },
    
    // 访问器属性,可枚举
    e: {
            get() {
              console.log('get e')
            },
            set() {
              console.log('set e')
            },
            configurable: true,
            enumerable: true
    },
    
    // 访问器属性,不可配置
    f: {
            get: () => console.log('get f'),
            set: () => console.log('set f'),
            enumerable: true
    },
    
    // 访问器属性,可配置,不可枚举
    g: {
            get() {
               console.log('get name')
               return _name
            },
            set(v) {
              console.log('set name', v)
              _name = v
            },
            configurable: true
    }
})

我们来删除不可配置的属性,发现返回 false,删除失败

delete c // false

来遍历这个对象的键,发现不可枚举的遍历不出来

for (key in obj) console.log(key) // a, b, d, e, f

来试着写入不可写入的属性,发现写入不成功

obj.c = 'cc'
console.log(obj.c) // 'c'

来调用一下访问器属性

obj.f = '4' // set f
// 此处调用了 setter,但没有改变任何数据。这与 configurable 无关,是它的 setter 本来就没做什么造成的

obj.e = '5' // 似乎没有奇怪的事情发生

obj.g = '2' // set name, '2'。此后 _name = '2',g 返回 '2'

obj // 输出里面有一个 g: (...),点开会调用 getter,然后打印 'get name',变为 g: '2'

delete obj.g // true,因为 g 可配置

可枚举与可迭代

可枚举(Enumerable)

当我们定义一个对象的属性时,可以通过设置 enumerable 属性描述符来控制该属性是否可枚举。默认情况下,通过字面量或直接赋值创建的属性都是可枚举的

可以使用 propertyIsEnumerable 方法来判断一个属性是否可枚举,或者使用 Object.getOwnPropertyDescriptor 获取属性描述符查看 enumerable 的值

有一些操作与属性的可枚举性有关:

  1. for...in:遍历对象自身和继承到的可枚举字符串属性
  2. Object.keys() :返回对象自身的可枚举字符串属性的数组
  3. JSON.stringify() :只序列化对象自身的可枚举属性
  4. Object.assign() :只复制对象自身的可枚举属性

可迭代(Iterable)

可迭代是指一个对象或者它原型链上的某个对象有一个 Symbol.iterator 属性,该属性是一个函数,返回一个迭代器对象,这称为可迭代协议。迭代器对象有一个 next 方法,每次调用返回一个包含 value(当前迭代的值) 和 done(布尔值,表示迭代是否结束) 属性的对象,这称为迭代器协议。迭代器涉及的知识太多,笔者推荐一篇好文:Js 中迭代器、生成器详解!

JS 中有一些常见的可迭代对象,如 ArrayStringMapSetarguments 对象NodeList 对象

笔者注:可能有人会觉得 arguments 对象和 NodeList 对象是数组,其实不是的,它们是类数组对象,有数组的某些特性(arguments 和 NodeList 都有 length 属性,但 arguments 不能被 forEach 遍历,需要转换成 Array 才行;NodeList 因为原型链上自己实现了 forEach,就可以被 forEach 遍历了),但本质上不是数组

有一些操作与对象的可迭代性有关:

  1. for...of:用于遍历可迭代对象
  2. 展开语法(...) :可以将可迭代对象展开
  3. Array.from() :可以将可迭代对象或类数组转换为数组
  4. Map、Set、WeakMap、WeakSet 的构造函数:可以接受可迭代对象

总结可枚举 影响属性遍历;可迭代 影响对象本身迭代能力

笔者有点累了,关于 Proxy 的知识明日再补充