引言
我们都知道在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)
时,我们会发现在打印的过程中是遍历这个数组的。
打印的过程可以理解为两步:
- 找到test变量指向的索引存为一个数组,长度为3并打印,此时并不知道索引对应的值是多少
- 遍历索引获取对应值,触发get方法
接下来我们做如下操作:
总结
对于defineProperty
来说,处理数组与对象是一视同仁的,只是在初始化时去改写get
和set
达到监测数组或对象的变化,对于新增的属性,需要手动再初始化。对于数组来说,只不过特别了点,push、unshift值也会新增索引,对于新增的索引也是可以添加observe从而达到监听的效果;pop、shift值会删除更新索引,也会触发defineProperty
的get
和set
。对于重新赋值length的数组,不会新增索引,因为不清楚新增的索引有多少,根据ecma
规范定义,索引的最大值为2^32 - 1
,不可能循环去赋值索引的。