为什么Object.defineProperty不能监听到数组长度的变化?

引言

我们都知道在vue3内使用proxy去代替了Object.defineProperty。今天不赘述其区别和替换的原因,而是从监听数组的角度来分析后者的“缺陷”。

简单介绍Object.defineProperty

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

  • 语法
    Object.defineProperty(obj, prop, descriptor)

    • object 必需。要在其上添加或修改属性的对象。这可能是一个本机JavaScript对象(即用户定义的对象或内置对象)或 DOM 对象。
    • propertyname 必需。 一个包含属性名称的字符串。
    • descriptor 必需。属性描述符。它可以针对数据属性访问器属性
  • 小知识点:对象属性类型
    属性类型可分为数据属性访问器属性,具体解释见JavaScript对象的数据属性与访问器属性

    • 相同点:
      • [[Configurable]] 字面理解是表示属性是否可配置——能否修改属性;能否通过delete删除属性;能否把属性修改为访问器属性。
      • [[Enumerable]] 能否通过for-in循环返回该属性。
    • 区别:
      • 数据属性
        • [[Writable]] 是否可写
        • [[Value]] 属性的值
      • 访问者属性
        • [[Get]] 取值函数
        • [[Set]] 赋值函数

    接着来看属性创建的区别

    // 方式1
    var object1 = new Object()
    object1.name = 'a'
    
    // 方式2
    var object2 = {}
    object2.name = 'b'
    
    // 方式3
    var object3 = {}
    Object.defineProperty(object3, 'name', {
      enumerable: true,
      configurable: true,
      get() {
        return 'c'
      },
      set() {
        // do
      }
    })
    
    • 第1、第2种对于属性的赋值是一样的,不同的是创建对象的方式。在使用object.name赋值的时候,我们其实是对数据属性[[Value]]赋值,取值也是一样
    • 通过第3种创建的对象,在对object.name取值赋值时,是通过访问器属性的[[Get]][[Set]]函数

数组长度和数组索引

vue2通过对数组方法的重写达到了目的,其本质原因是Object.defineProperty不能检测到数组长度length的变化。

我们需要理解两个概念,数组长度与数组索引。

  • 数组的length属性,被初始化为:
enumberable: false
configurable: false
writable: true

也就是说,试图去删除和修改(非赋值)length属性是行不通的。
在这里插入图片描述

  • 数组索引是访问数组值的一种方式,如果拿它和对象来比较,索引就是数组的属性key。

我们通过数组的方法给数组增加长度后,同步会给length赋值。

length和数字下标之间的关系—— JavaScript 数组的 length 属性和其数字下标之间有着紧密的联系。数组内置的几个方法(例如 join、slice、indexOf 等)都会考虑 length 的值。另外还有一些方法(例如 push、splice 等)还会改变 length 的值。

这几个内置的方法在操作数组时,都会改变length的值,分两种情况

  • 减少值
    当我们shift一个数组时,你会发现它会遍历数组,此时数组的索引对应的值得到了相应的更新,这种情况下defineProperty是可以监测到的,因为有属性(索引)存在。

  • 增加值

    • push值时,此时数组的长度会+1,索引也会+1,但是此时的索引是新增的,虽然defineProperty不能监测到新增的属性,但是在vue中,新增的对象属性可以显示的调用vm.$set来添加监听。
    • 手动赋值length为一个更大的值,此时长度会更新,但是对应的索引不会被赋值,也就是对象的属性没有,defineProperty再牛逼也没办法处理对未知属性的监听。

验证数组的几个内部方法对索引的影响

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
     get: function defineGet() {
      console.log(`get key: ${key} val: ${val}`)
      return val
    },
     set: function defineSet(newVal) {
      console.log(`set key: ${key} val: ${newVal}`)
      val = newVal
    }
  })
}
function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key])
  })
}

let test = [1, 2, 3]
// 初始化
observe(test)

console.log(test)时,我们会发现在打印的过程中是遍历这个数组的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ImTjNwvH-1658970295821)(./2.png)]

打印的过程可以理解为两步:

  • 找到test变量指向的索引存为一个数组,长度为3并打印,此时并不知道索引对应的值是多少
  • 遍历索引获取对应值,触发get方法

接下来我们做如下操作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yfjYxJJw-1658970295822)(./3.png)]

总结

对于defineProperty来说,处理数组与对象是一视同仁的,只是在初始化时去改写getset达到监测数组或对象的变化,对于新增的属性,需要手动再初始化。对于数组来说,只不过特别了点,push、unshift值也会新增索引,对于新增的索引也是可以添加observe从而达到监听的效果;pop、shift值会删除更新索引,也会触发definePropertygetset。对于重新赋值length的数组,不会新增索引,因为不清楚新增的索引有多少,根据ecma规范定义,索引的最大值为2^32 - 1,不可能循环去赋值索引的。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值