ES6 - 代理Proxy

308 阅读4分钟

代理的定义

提供一些机制对外界的访问(对象或函数)进行过滤或重写,具体是将对象/方法的一些操作拦截过来,使用代理中相关的拦截方法去进行自定义的逻辑操作

Proxy基本使用方法

使用new Proxy定义代理的时候需要传2个参数

let p = new Proxy(target, handler)
  • 参数1: 当前将要拦截/包装的对象或方法
  • 参数2: 代理的配置,定义一些拦截的钩子函数
// 当前的Proxy对obj进行了代理
let obj = {}
let  p = new Proxy(obj,{})
p.name = "ES6"
console.log(obj.name);                              // ES6
for(let key in obj){
    console.log(key);                              // name
}

常用拦截操作

Object.defineProperty

ES5中提供了一个方法可以对对象的属性进行拦截

let obj = {}
let newValue = ''
// 参数1:拦截的对象;参数2:拦截的对象的属性,参数3:是个对象,配置拦截的方法
Object.defineProperty(obj,'name',{
    get(){
        console.log('get')
        return newValue
    },
    // 不能这样写,obj.name = 'ES6'时调用set,set的时候this.name = val,给name赋值的时候又会调用get,进入死循环
    // set(val){
    //     console.log('set');
    //     this.name = val
    // }
    set(val){
        console.log('set');
        newValue = val
    }
})
// 存 ==> 调用set方法
obj.name = 'ES6'
// 取 ==> 调用get方法
console.log(obj.name);

get

拦截对象属性的读取,比如proxy.foo和proxy['foo']。当我们想获取代理对象的值的时候,就会自动的调用get钩子函数去处理一些内容,在get里面可以进行一些业务逻辑的操作

get接受2个参数,第一个参数是将要拦截的对象,第2个参数是要传的参数,eg:

let arr = [1,2,3,4]
arr = new Proxy(arr,{
    get(target,prop){
        console.log(target,prop);
    }
})

console.log(arr[1]);                                 //[1, 2, 3, 4] '1'

eg: 使用get拦截数组,判断数组的某个下标下是否有值,有值就返回相应的值,没值就抛出一个error

// 对数组的代理
let arr = [1,2,3,4]
arr = new Proxy(arr,{
    get(target,prop){
        return prop in target ? target[prop] : '下标没有对应的值'
    }
})

console.log(arr[1]);         //2

eg: 实现一个字典的功能

// 对对象的代理 
let dict = {
    'hello' : '你好',
    'world' : '世界'
}
dict = new Proxy(dict,{
    get(target,prop){
        return prop in target ? target[prop] : '当前词典无此词汇'
    }
})

console.log(dict['hello']);                    // 你好

set

拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。接收三个参数,第一个参数:拦截的目标对象,第二个参数是将要拦截的属性,第三个参数是将要拦截对象的属性对应的值

eg: 对数组进行设置值的时候进行代理和拦截,只能存储number类型,如果往数组中添加其他类型的值就会报错

let arr = []
arr = new Proxy(arr, {
    set(target, prop, val) {
        if (typeof val === 'number') {
            target[prop] = val
            return true
        } else {
            return false
        }
    }
})

arr.push(5)
arr.push(6)
console.log(arr[0]);                    // 5
console.log(arr[1]);                    // 6

使用对象的代理,并不会破坏对象原有的方法,如:使用数组的push方法也会被set这个拦截钩子拦截

has

拦截propKey in proxy的操作,返回一个布尔值

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(9 in range);                 // false

ownKeys

循环遍历对象属性的时候使用,拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。

eg: 复习下不同循环遍历方法之间的差别

let obj = {
    name: 'lee',
    [Symbol('es')]: 'ES6'
}
// 用于遍历key不是synmol类型的key
console.log(Object.getOwnPropertyNames(obj));       // ['name']
// 只能遍历symbol类型的key
console.log(Object.getOwnPropertySymbols(obj));    // [Symbol(es)]
console.log(Object.keys(obj));                     // ['name']
for(let key in obj){
    console.log(key);                             // name
}

eg; 不想让别人遍历到以下划线开头的私有属性

let userInfo = {
    username: 'lee',
    age: 32,
    _password: '****'
}

userInfo = new Proxy(userInfo,{
    ownKeys(target) {
        return Object.keys(target).filter( key => !key.startsWith('_'))
    }
})

for(let key in userInfo) {
    console.log(key);                      //  username  age
}
console.log(Object.keys(userInfo));       // ['username', 'age']

deleteProperty

拦截删除操作(delete)

拦截delete proxy[propKey]的操作,返回一个布尔值。

eg: 以下划线开头的属性作为私有属性,需要用Proxy阻止对下划线开头的属性的任何访问,包括获取、设置、删除、循环遍历,因此我们就要使用get、set、deleteProperty代理的钩子函数

let user = {
    name: 'lee',
    age: 32,
    _password: 'xxx'
}
user = new Proxy(user,{
    // 拦截获取属性
    get(target,prop) {
        if(prop.startsWith('_')){
            throw new Error('私有属性不可访问')
        }else{
            return target[prop]
        }
    },
    // 拦截设置属性,对于set一定要返回布尔类型的值
    set(target,prop,val) {
        if(prop.startsWith('_')){
            throw new Error('私有属性不可访问')
        }else{
            target[prop] = val
            return true
    },
    // 拦截删除属性
     deleteProperty(target,prop) {
        if (prop.startsWith('_')) {
            throw new Error('不可删除')
        } else {
            delete target[prop]
            return true
        }
    },
    // 拦截遍历属性
      ownKeys(target) {
        return Object.keys(target).filter(item => !item.startsWith('_'))
    }
})

console.log(user._password);   // 私有属性不可访问
console.log(user.name);        // lee

user.age = 18
console.log(user.age);           // 18
try {
    user._password = 8888 
} catch (e) {
    console.log(e.message);    // 私有属性不可访问
}

try {
    delete user.age
} catch (e) {
    console.log(e.message);    
}
console.log(user.age);           // undefined    

try {
    delete user._password
} catch (e) {
    console.log(e.message);     // 不可删除
}

for(key in user) {
    console.log(key);             // name age
}
 

apply

接收三个参数,第一个参数是拦截的目标,第二个参数是当前的上下文,第三个参数是目标对象参数的数组

拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。

eg: 对求和的结果进行拦截,输出的结果为和的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(null,[1,2,3]));              // 12
 

construct

拦截new,接收三个参数,第一个参数是拦截对象,第二个参数是class内部constructor的参数列表,第三个参数是创建实例的时候new作用的构造函数

拦截 Proxy 实例作为构造函数调用的操作,返回一个对象,比如new proxy(...args)。

let User = class {
    constructor(name){
        this.name = name
    }
}
User = new Proxy(User,{
    construct(target,args,newTarget) {
        return new target(...args)
    }   
})

console.log(new User('lee'));    // {name: 'lee'}