前提概要
上一篇编写的是ES6中的数值的扩展,链接:juejin.cn/post/702877… ,这次写的是ES6中Proxy,介绍Proxy的一些基础知识和应用场景。如有不对的或者不准确的地方,欢迎大家提出😄,我也积极修改。下边开始正文:
基本语法
ES5中对对象的属性进行拦截方法如下:
let obj = {}
let newObj = '';
Object.defineProperty(obj,'name',{
get(){
console.log('get')
return newVal
},
set(val){
console.log('set')
// 这么赋值会报错,原因是我们调用set方法是给name赋值,但既然是赋值就会调用set方法,因此会形成一个死循环,
// this.name = val
// 所以我们要声明一个变量newVal,用于接收这个变量
newVal = val
}
})
console.log(obj)
obj.name = 'xs'
console.log(obj.name)
console.log(obj)
运行结果:{} set get xs {}
ES6 中引出 Proxy 拦截
语法
let p = new Proxy(target, handler)
解释
参数 | 含义 | 必选 |
---|---|---|
target | 用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理) | 是 |
handler | 一个对象,其属性是当执行一个操作时定义代理的行为的函数 | 是 |
第一个参数 target 就是用来代理的“对象”,被代理之后它是不能直接被访问的,第二个参数 handler 就是实现代理的过程。
常用拦截操作
get()
get()
拦截对象属性的读取,比如proxy.foo和proxy['foo']。
get(target, propKey, receiver)
接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
对于数组类型的拦截
let arr = [2, 3, 4]
arr = new Proxy(arr, {
get (target, prop) {
console.log(target, prop)
return prop in target ? target[prop] : 'error'
}
})
console.log(arr[1]) // (3) [2, 3, 4] '1' 3
console.log(arr[5]) // (3) [2, 3, 4] '5' error
对于对象类型的拦截
let dist = {
'hello': '你好',
'world': '世界'
}
dist = new Proxy(dist, {
get (target, prop) {
return prop in target ? target[prop] : prop
}
})
console.log(dist['hello']) // 你好
console.log(dist['xs']) // xs
set()
set()
拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值;
set(target, propKey, value, receiver)
可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
let arr = []
arr = new Proxy(arr, {
set (target, prop, value) {
if (typeof value === 'number') {
target[prop] = value
// return true
} else {
throw new Error('请传入Number类型的元素')
// return false
}
return true
}
})
arr.push(5)
console.log(arr[0]) // 5
arr.push(6)
console.log(arr[1]) // 6
console.log(arr.length) // 2
arr.push('1') // Uncaught Error: 请传入Number类型的元素
has()
has()
判断对象是否具有某个属性,如:拦截propKey in proxy的操作,返回一个布尔值。
has(target, propKey)
方法可以接受两个参数,分别是目标对象、需查询的属性名。
// 例如:有一个范围,从1开始到5结束,我想判断传进来的值是否在这个范围当中
let range = {
start: 1,
end: 5
}
range = new Proxy(range, {
has (target, prop) {
return prop >= target.start && prop <= target.end
}
})
console.log(2 in range) // true
console.log(8 in range) // false
ownKeys()
ownKeys(target)
拦截Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。
ownKeys(target)
返回目标对象 所有 自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的 可遍历属性。
let obj = {
name: 'xs',
[Symbol('like')]: 'es6'
}
console.log(Object.getOwnPropertyNames(obj)) // ['name']
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(like)]
console.log(Object.keys(obj)) // ['name']
for (let key in obj) {
console.log(key) // name
}
案例:假如有一个包含用户的一些信息的对象,对开头带有下划线_
的私有属性不被遍历出来
let useInfo = {
name: 'xs',
age: 18,
_password: '******'
}
useInfo = new Proxy(useInfo, {
ownKeys (target) {
return Object.keys(target).filter(key => !key.startsWith('_'))
}
})
for (let key in useInfo) {
console.log(key) // name age
}
console.log(Object.keys(useInfo)) // ['name', 'age']
注意:
ownKeys()
方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
deleteProperty()
deleteProperty(target, propKey)
拦截delete proxy[propKey]的操作,返回一个布尔值。
// 删除开头带有下划线`_`的属性
let obj = {
name: 'xs',
age: 18,
_password: '******'
}
console.log(Object.keys(obj)) // ['name', 'age', '_password']
obj = new Proxy(obj, {
deleteProperty (target, prop) { // 拦截删除
if (prop.startsWith('_')) {
delete target[prop]
return true
} else {
throw new Error('对象中没有以下划线开头的属性')
}
},
})
delete obj._password
for (let key in obj) {
console.log(key) // name age
}
console.log(Object.keys(obj)) // ['name', 'age']
try {
delete obj.age
} catch (e) {
console.log(e.message) // 对象中没有以下划线开头的属性
}
注意
目标对象自身的不可配置(configurable)的属性,不能被
deleteProperty
方法删除,否则报错。
apply()
apply(target, object, args)
拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
apply(target, object, args)
方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
// 对一个组数字的进行累加,再将结果扩大2倍
let sum = (...args) => {
let num = 0
args.forEach(item => {
num += item
})
return num
}
sum = new Proxy(sum, {
apply (target, ctx, args) {
return target(...args) * 2
}
})
console.log(sum(1, 2)) // 6
console.log(sum.call(null, 1, 2, 3)) // 12
console.log(sum.apply(window, [1, 2, 3, 4])) // 20
construct()
construct()
方法用于拦截new命令(实例化对象的时候)。
construct(target, args, newTarget)
方法可以接受三个参数,依次是目标对象(必须是函数)、构造函数的参数数组、创造实例对象时new命令作用的构造函数(下面的NewUser)。而且结果必须返回一个对象。
let User = class {
constructor(name) {
this.name = name
}
}
// 注意,construct()方法中的this指向的是handler,而不是实例对象。
let NewUser = new Proxy(User, {
construct (target, args, newTarget) {
console.log(this, 'this') // {construct: ƒ} 'this'
console.log(target, 'target') // 指的是class类
console.log(args, 'args') // ['xs'] 'args'
console.log(newTarget, 'newTarget') // Proxy {length: 1, name: 'User', prototype: {…}} 'newTarget'
return new target(...args)
}
})
console.log(new NewUser('xs')) // User {name: 'xs'}
拦截操作的场景
场景一
我们在读取一个对象的key-value时,
let obj = {
name:'xs',
age:18,
}
console.log(obj.name) // xs
console.log(obj.age) // 18
console.log(obj.like) // undefined
当这个key不存在的时候,会返回undefined
,我们常规解决办法是console.log(obj.like || 'es')
在 ES6 的 Proxy 中可以这么写来解决这个问题
let obj = {
name:'xs',
age:18,
}
let p = new Proxy(obj,{
get(target,prop){
return Reflect.has(target,prop) ? target[prop] : 'es'
}
})
console.log(p.like) // es
这个代码的意思是如果obj对象有这个key-value,则直接返回,如果没有一律返回 es
,当然这里是自定义,我们可以根据自己的实际情况来写适合自己业务的规则。
上面讲述的是对数据的“读操作”进行了拦截,接下来我们描述下“写操作”进行拦截。
场景二
从服务端获取的数据希望是只读,不允许在任何一个环节被修改。
// 在ES5中只能通过遍历把所有属性设置为只读
let response = {
data:{
name:'xs',
age:18
}
}
for(let key of Object.keys(response.data)){
Object.defineProperty(response.data,key,{
writable: false,
})
}
response.data.name = 'xiaoming'
console.log(response.data.name) // xs
// 在ES6中就可以使用Proxy
let data = new Proxy(response.data,{
set(target,prop,value){
return false
}
})
data.age = 20
console.log(data.age) // 18
场景三
对数据进行校验
// Validator.js
export default (obj,key,value) => {
if(Reflect.has(obj,key) && value > 18){
obj[key] = value
}
}
import Validator from './Validator.js'
let data = new Proxy(response.data,{
set: Validator
})
场景四
对读写进行监控
let validator = {
set(target,prop,value){
if(prop === 'age'){
if(typeof value !== 'number' || Number.isNaN(value)){
throw new Error('Age must be a number')
}
if(value <= 0){
throw new Error('Age must be a positive number')
}
if(!Number.isInteger(value)){
throw new Error('Age must be an integer')
}
}
target[prop] = value
return true
}
}
let p = new Proxy({},validator)
p.age = 18 // 18
p.age = NaN // Uncaught Error: Age must be a number
p.age = null // Uncaught Error: Age must be a number
p.age = 16.66 // Uncaught Error: Age must be an integer
p.age = -1 // Uncaught Error: Age must be a positive number