前言
接触到这两个概念还是偶然的机会, 因为平常业务开发主要是是基于vue.js,碰到一些问题会忽然僵住,感觉难以入手. 比如:
vue.js的响应式是如何实现的, 为什么它可以做到双向绑定?
对象拦截的话, 如果属性值还是对象呢?
为什么在vue中数组可以检测到改变?
为了解决这些问题, 就引出了Object.defineProperty().
Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性, 或者修改一个对象的现有属性,并返回此对象.
简单的说就是, 对象调用这个方法, 可以在对象上加一个拦截器, 可以对对象属性进行修改.
const obj = {}
Object.defineProperty(obj,'name', {
value: 'aixiao',
writable: false
})
obj.name = 'other' // 严格模式下会报错
console.log(obj.name) // 'aixiao'
value
对象属性对应的值. 默认为undefined
writable
由Object.defineProperty定义的属性的value,默认是不可修改的. 如需要修改则设置 writable为true. 上文的例子中有表现出这一属性.
enumerable
对象属性的可枚举性,enumable默认为false,只有为true时才可被枚举.
const obj = {
name: 'aixiao'
}
Object.defineProperty(obj, 'age', {
value: '18',
enumerable: false
})
const keys = Object.keys(obj)
console.log(keys); // ["name"]
console.log(obj); // {name: "aixiao", age: "18"}
configurable
configurable为true时,对象属性能从对象上删除,被改变.
const obj = {
name: 'aixiao'
}
Object.defineProperty(obj, 'age', {
value: '18',
configurable: false
})
const keys = Object.keys(obj)
delete obj.age;
console.log(obj); // {name: "aixiao", age: "18"}
get
当访问对象的属性时会调用get,如果没有get那么就是undefined;
const obj = {
name: 'aixiao'
}
Object.defineProperty(obj, 'age', {
get() {
console.log('调用属性了')
return 18;
}
})
console.log(obj.age); // 18
set
当修改对象的属性时会调用set,如果没有set那么就是undefined;
const obj = {
name: 'aixiao',
age: 18
}
Object.defineProperty(obj, 'newAge', { // 这里用一个新的属性来做数据同步
get() {
return this.age;
},
set(newVal) {
this.age = newVal
}
})
console.log(obj.age); // 18
obj.age = 17
console.log(obj.age); // 17
了解了这些概念之后, 那么回过头来看上面的问题.毋庸置疑, vue2数据的双向绑定就是通过Object.defineProperty()来劫持对象实现的, 对每个对象的每个属性进行遍历.
那如果属性值还是对象就需要深度遍历,直到每个对象(属性)都调用了Object.defineProperty().相应的问题,在使用vue开发过程中,直接通过this.xx去声明并赋值一个属性, 那它肯定不是响应式的,需要通过Vue.set() 这个方法来更新视图.
为什么数组可以直接通过数组方法去操作来达到数据更新呢?
因为 vue把push();pop();shift();unshift();splice();sort();reverse()进行了重写从而实现数组的劫持.像filter();concat();slice()这些都不会改变原数组, 会返回一个新数组.
proxy
接下来看看vue3中使用的proxy, proxy是ES6的一个新特性(这个时间点应该大家都融会贯通了...),也可以用来做到Object.defineProperty()所做的事情,对属性做获取修改操作.
先看一个例子:
const obj = {
name: 'aixiao',
age: 18
}
function observe(obj) {
return new Proxy(obj, {
get(target, key) {
console.log('获取属性值')
return target[key]
},
set(target, key, value) {
console.log('设置属性值')
target[key] = value
}
})
}
const result = observe(obj)
console.log(result.age); // 获取属性值 // 18
result.age = 17 // 设置属性值
console.log(result.age); // 获取属性值 // 17
Proxy充当了一个拦截器的角色,在读取/修改对象属性,获取对象属性的时候去拦截对象, 然后此时自定义这些行为.
Proxy有两个参数
target:要兼容(拦截的对象), 可以是对象,数组,函数等等
handler: 它是个对象,里面包含了自定义要拦截对象的行为函数,上面例子中的 get 与 set就是.它会返回一个新的对象proxy, 要触发handler里的函数必须要通过返回值.
handler的方法
handler.get() :读取对象属性操作的函数.
handler.set() :设置对象属性操作的函数.
handler.has() :in操作符会触发的函数.
handler.deleteProperty() :delete 操作符会触发的函数.
handler.ownKeys() :当通过Object.getOwnPropertyNames(), Object.getownPropertySymbols(),Object.keys()方法去操作对象的时候会触发的函数
handler.apply() :当拦截的是一个函数的时候会触发的函数
handler.construct() :使用new操作符的时候触发的函数
handler.getPrototypeOf() :当读取对象prototype的时候会触发的函数
handler.setPrototypeOf() :当设置对象prototype的时候会触发的函数
handler.isExtensible() :当调用Object.isExtensible()判断对象可否添加新书行的时候会触发的函数
handler.preventExtensions() :当调用Object.preventExtensions()设置不可修改属性的时候会触发的函数
handler.getOwnPropertyDescriptor() :当调用Object.getOwnPropertyDescriptor()获取对象description的时候会触发的函数
handler.defineProperty() :当调用Object.defineProperty()的时候会触发的函数
Proxy的这些handler 解决了vue2中delete所做的事情, 因为Object.defineProperty无法监听新的属性,所以要用delete. 那再Proxy中可以通过set监听新增属性, 通过deleteProperty监听到删除 所以delete就没用了.
总结
这篇文章以我自己使用vue.js踩到的坑为例子,一步步发掘Object.defineProperty(),再到Proxy().用自己的理解诠释了一些问题以及问题的原因.Proxy中handler的方法没有一一举例, 可以在MDN上找到相对完整的讲义.旨在不断学习,加深印象.