Object.defineProperty的三个问题
- 不能监听数组的变化
let arr = [1, 2, 3]
let obj = {}
Object.defineProperty(obj, 'arr', {
get () {
console.log('get arr')
return arr
}
set (newVal) {
console.log('set', newVal)
arr = newVal
}
})
obj.arr.push(4) // 只会打印get arr,不会打印set
obj.arr = [1,2,3,4] // 能正常打印set
数组的push pop shift unshift reverse sort splice 不会触发set操作,Vue定义的这些数组方法为mutation method,指的是会修改原来数组的方法。与之对应的,non-mutating-method,例如filter、slice、concat不会修改原数组,会返回一个新数组。
Vue 重写mutation method
const arrMethods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
const arrayAugmentations = []
arrMethods.forEach(method => {
// 原生的原型方法
let original = Array.prototype[method]
// 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上
// 注意:是实例属性而非原型属性
arrayAugmentations[method] = function () {
console.log('changed')
return original.apply(this, arguments)
}
})
let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 这样就能在调用 push, pop 这些方法时走进我们刚定义的方法,多了一句 console.log
list.__proto__ = arrayAugmentations;
list.push('d'); // 我被改变啦!
// 这个 list2 是个普通的数组,所以调用 push 不会走到我们的方法里面。
let list2 = ['a', 'b', 'c'];
list2.push('d'); // 不输出内容
- 遍历对象的每个属性
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {})
})
- 必须遍历深层嵌套的对象
let obj = {
user: {
name: ''
}
}
Proxy的应用场景
- 针对对象
let obj = {
name: 'Eason',
age: 30
}
let handler = {
get (target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
}
let proxy = new Proxy(obj, handler)
proxy.name = 'Zoe' // set name Zoe
proxy.age = 18 // set age 18
- 支持数组
et arr = [1,2,3]
let proxy = new Proxy(arr, {
get (target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
})
proxy.push(4)
// 能够打印出很多内容
// get push (寻找 proxy.push 方法)
// get length (获取当前的 length)
// set 3 4 (设置 proxy[3] = 4)
// set length 4 (设置 proxy.length = 4)
- 递归调用 Proxy
let obj = {
info: {
name: 'eason',
blogs: ['webpack', 'babel', 'cache']
}
}
let handler = {
get (target, key, receiver) {
console.log('get', key)
// 递归创建并返回
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
}
let proxy = new Proxy(obj, handler)
// 以下两句都能够进入 set
proxy.info.name = 'Zoe'
proxy.info.blogs.push('proxy')
- 多继承
let foo = {
foo () {
console.log('foo')
}
}
let bar = {
bar () {
console.log('bar')
}
}
// 正常状态下,对象只能继承一个对象,要么有 foo(),要么有 bar()
let sonOfFoo = Object.create(foo);
sonOfFoo.foo(); // foo
let sonOfBar = Object.create(bar);
sonOfBar.bar(); // bar
// 黑科技开始
let sonOfFooBar = new Proxy({}, {
get (target, key) {
return target[key] || foo[key] || bar[key];
}
})
// 我们创造了一个对象同时继承了两个对象,foo() 和 bar() 同时拥有
sonOfFooBar.foo(); // foo 有foo方法,继承自对象foo
sonOfFooBar.bar(); // bar 也有bar方法,继承自对象bar
- 隐藏私有变量
function getObject(rawObj, privateKeys) {
return new Proxy(rawObj, {
get (target, key, receiver) {
if (privateKeys.indexOf(key) !== -1) {
throw new ReferenceError(`${key} 是私有属性,不能访问。`)
}
return target[key]
}
})
}
let rawObj = {
name: 'Zoe',
age: 18,
isFemale: true
}
let obj = getObject(rawObj, ['age'])
console.log(obj.name) // Zoe
console.log(obj.age) // 报错
5.对象的属性设定时校验
let person = {
name: 'Eason',
age: 30
}
let handler = {
set (target, key, value, receiver) {
if (key === 'name' && typeof value !== 'string') {
throw new Error('用户姓名必须是字符串类型')
}
if (key === 'age' && typeof value !== 'number') {
throw new Error('用户年龄必须是数字类型')
}
return Reflect.set(target, key, value, receiver)
}
}
let personForUser = new Proxy(person, handler)
personForUser.name = 'Zoe' // OK
personForUser.age = '18' // 报错
- 容错检查
// 故意设置一个错误的 data1,即 response.data = undefined
let response = {
data1: {
message: {
from: 'Eason',
text: 'Hello'
}
}
}
// 也可以根据 key 的不同给出更友好的提示
let dealError = key => console.log('Error key', key)
let isOK = obj => !obj['HAS_ERROR']
let handler = {
get (target, key, receiver) {
// 基本类型直接返回
if (target[key] !== undefined && typeof target[key] !== 'object') {
return Reflect.get(target, key, receiver)
}
// 如果是 undefined,把访问的的 key 传递到错误处理函数 dealError 里面
if (!target[key]) {
if (!target['HAS_ERROR']) {
dealError(key)
}
return new Proxy({HAS_ERROR: true}, handler)
}
// 正常的话递归创建 Proxy
return new Proxy(target[key], handler)
}
}
let resp = new Proxy(response, handler)
if (isOK(resp.data.message.text) && isOK(resp.data.message.from)) {
console.log(`你收到了来自 ${response.data.message.from} 的信息:${response.data.message.text}`)
}
因为我们故意设置了 response.data = undefined,因此会进入 dealError 方法,参数 key 的值为 data。
虽然从代码量来看比上面的 if 检查更长,但 isOK, handler 和 new Proxy 的定义都是可以复用的,可以移动到一个单独的文件,仅暴露几个方法即可。所以实际的代码只有 dealError 的定义和最后的一个 if 而已。
更多应用场景
设置对象默认值 - 创建一个对象,它的某些属性自带默认值。
优化的枚举类型 - 枚举类型的 key 出错时立刻报错而不是静默的返回 undefined,因代码编写错误导致的重写、删除等也可以被拦截。
追踪对象和数组的变化 - 在数组和对象的某个元素/属性发生变化时抛出事件。这可能适用于撤销,重做,或者直接回到某个历史状态。
给对象的属性访问增加缓存,提升速度 - 在对对象的某个属性进行设置时记录值,在访问时直接返回而不真的访问属性。增加 TTL 检查机制(Time To Live,存活时间)防止内存泄露。
支持 in 关键词的数组 - 通过设置 has 方法,内部调用 array.includes。使用的时候则直接 console.log(‘key’ in someArr)。
实现单例模式 - 通过设置 construct 方法,在执行 new 操作符总是返回同一个单例,从而实现单例模式。
Cookie 的类型转换 - document.cookie 是一个用 ; 分割的字符串。我们可以把它转化为对象,并通过 Proxy 的 set 和 deleteProperty 重新定义设置和删除操作,用以对外暴露一个可操作的 Cookie 对象,方便使用。
Object.defineProperty的三个问题
Vue 重写mutation method
Proxy的应用场景
5.对象的属性设定时校验
因为我们故意设置了 response.data = undefined,因此会进入 dealError 方法,参数 key 的值为 data。
虽然从代码量来看比上面的 if 检查更长,但 isOK, handler 和 new Proxy 的定义都是可以复用的,可以移动到一个单独的文件,仅暴露几个方法即可。所以实际的代码只有 dealError 的定义和最后的一个 if 而已。
更多应用场景
设置对象默认值 - 创建一个对象,它的某些属性自带默认值。
优化的枚举类型 - 枚举类型的 key 出错时立刻报错而不是静默的返回 undefined,因代码编写错误导致的重写、删除等也可以被拦截。
追踪对象和数组的变化 - 在数组和对象的某个元素/属性发生变化时抛出事件。这可能适用于撤销,重做,或者直接回到某个历史状态。
给对象的属性访问增加缓存,提升速度 - 在对对象的某个属性进行设置时记录值,在访问时直接返回而不真的访问属性。增加 TTL 检查机制(Time To Live,存活时间)防止内存泄露。
支持 in 关键词的数组 - 通过设置 has 方法,内部调用 array.includes。使用的时候则直接 console.log(‘key’ in someArr)。
实现单例模式 - 通过设置 construct 方法,在执行 new 操作符总是返回同一个单例,从而实现单例模式。
Cookie 的类型转换 - document.cookie 是一个用 ; 分割的字符串。我们可以把它转化为对象,并通过 Proxy 的 set 和 deleteProperty 重新定义设置和删除操作,用以对外暴露一个可操作的 Cookie 对象,方便使用。