代理与反射(二)

简介: 代理与反射(二)

使用代理模式可以实现一些有用的功能。

捕获操作

通过添加对应捕获器,就可以捕获getsethas等操作,可以监控这个对象何时在何处被访问过,并且能在访问、修改前干想干的事,并通过反射重新实现原功能。

const user = {
  name: 'clz'
}


const proxy = new Proxy(user, {
  get(target, property, receiver) {
    console.log(`访问 ${property}`)
    return Reflect.get(...arguments)
  },
  set(target, property, value, receiver) {
    console.log(`设置 ${property}=${value}`)
    return Reflect.set(...arguments)
  }
})

console.log(proxy.name)
proxy.age = 21

image-20220503205936504

这里有一个需要小小注意的点:通过代理对象的操作才会被捕获,而直接操作目标对象的操作不会被捕获。

const user = {
  name: 'clz'
}


const proxy = new Proxy(user, {
  get(target, property, receiver) {
    console.log(`访问 ${property}`)
    return Reflect.get(...arguments)
  }
})

console.log(proxy.name)
console.log(user.name)

image-20220503205922840

隐藏属性

因为代理的内部实现对于外部的代码来说是不可见的,所以想要隐藏目标对象上的属性也很容易实现。我们上面说过,需要通过反射来实现原功能,但是我们也可以不实现原功能,而是返回其他值。

const user = {
  name: 'clz',
  age: 21
};


const proxy = new Proxy(user, {
  get(target, property, receiver) {
    if (property === 'name') {
      // 隐藏name属性
      return undefined;
    }
    return Reflect.get(...arguments)
  }
});

console.log(user)
console.log(user.name)
console.log(user.age)

console.log('%c%s', 'font-size:24px;color:red', '=================')

console.log(proxy)
console.log(proxy.name)
console.log(proxy.age)

image-20220503210140040

从上图,我们可以知道,直接访问目标对象、目标对象属性以及访问代理对象都能能到一样的结果。但是,通过代理访问 name属性会得到 undefined,因为我们在捕获操作中进行了隐藏属性。

验证

属性验证

因为所有的赋值操作都会触发set捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值,不通过验证,直接return false即可拒绝赋值。

const user = {
  name: 'clz',
  age: 21
};


const proxy = new Proxy(user, {
  set(target, property, value) {
    if (typeof value !== 'number') {
      console.log('不是number类型,拒绝赋值')
      return false;
    } else {
      return Reflect.set(...arguments);
    }
  }
});

proxy.age = 999;
console.log(proxy.age);

proxy.age = '111';
console.log(proxy.age);

image-20220503211445808

当我们返回 false时,即不通过验证,就可以不进行原始行为的实现,

函数参数验证

和验证对象属性类似,也可以对函数的参数进行审查。

首先,函数也是能使用代理的。apply捕获器会在调用函数时被调用。

function fn() { }

const proxy = new Proxy(fn, {
    apply() {
        console.log('调用函数')
    }
});

proxy(1, 2, 3, 4, 5)

所以我们应该在 apply捕获器中进行操作验证。

function mysort(...nums) {
  return nums.sort((a, b) => a - b);
}


// 在`apply`捕获器中进行参数验证
const proxy = new Proxy(mysort, {
  apply(target, thisArg, argumentsList) {
    // target: 目标对象
    // thisArg: 调用函数时的this参数
    // argumentsList: 调用函数时的参数列表

    for (const arg of argumentsList) {
      if (typeof arg !== 'number') {
        throw '函数参数必须为number';
      }
    }

    return Reflect.apply(...arguments);
  }
});

let fin = proxy(2, 1, 6, 3, 4, 3);
console.log(fin);

fin = proxy(2, 1, 'hello', 3, 4, 3);
console.log(fin);

构造函数同理,只是构造函数是通过constructor捕获器来实现代理。

function Person(name) {
  this.name = name
}

const proxy = new Proxy(Person, {
  construct() {
    console.log(123)
    return Reflect.construct(...arguments)
  }
});

const p = new proxy('clz')
console.log(p)

数据绑定

通过代理可以把原本运行中不相关的部分联系到一起。

例子:将被代理的类绑定到一个全局的实例集合,将所有创建的实例都添加到这个集合中。`

const userList = []

class User {
    constructor(name) {
        this.name_ = name;
    }
}

const proxy = new Proxy(User, {
    construct() {
        const newUser = Reflect.construct(...arguments)
        userList.push(newUser)
        return newUser
    }
})


new proxy('clz')
new proxy('czh')

console.log(userList)

事件分发程序

在开始之前,先来一个小问题。

    const nums = []

    const proxy = new Proxy(nums, {
      set(target, property, value, receiver) {
        console.log('setting')

        return Reflect.set(...arguments)
      }
    })

    proxy.push(1)

上面的代码会打印两次setting

这是为什么呢?

让我们把它每一轮修改的属性打印出来,研究一下。

const nums = []

const proxy = new Proxy(nums, {
  set(target, property, value, receiver) {
    console.log('setting')

    console.log(property)

    return Reflect.set(...arguments)
  }
})

proxy.push(1)

image-20220503234749114

我们可以发现,第一次是0,第二次是 length。也就是说,push实际上是分成两个阶段的,第一轮先修改数组,第二轮再修改数组长度,所以会打印两轮。(没有找到权威的解释,实践测试得出的结论,有问题可评论)

回到正题:可以把集合绑定给一个事件分发程序,实现每次插入新实例时,都会发送消息。

const nums = []

function emit(newValue) {
  console.log('有新数据,新数据是', newValue)
}


const proxy = new Proxy(nums, {
  set(target, property, value, receiver) {
    const result = Reflect.set(...arguments)
    // Reflect.set返回boolean值,该值指示该属性是否已成功设置

    if (result) {
      // 如果被成功设置了,调用事件分派函数,把新插入的值作为参数传过去

      emit(Reflect.get(target, property, receiver))
    }

    return result
  }
})

proxy.push(111)
proxy.push(222)
console.log(proxy)

image-20220503235521715

这里可以发现,我们 push两条数据,但是会触发事件分发程序4次,这是为什么?

这就是这一小节上面讲的,push实际上是分成两个阶段的,第一轮先修改数组,第二轮再修改数组长度

所以,我们不应该只是判断是否被成功设置,还应该判断属性是不是 length

if (result && property !== 'length') {
  // 如果被成功设置了,调用事件分派函数,把新插入的值作为参数传过去

  emit(Reflect.get(target, property, receiver))
}

image-20220504000010868

目录
相关文章
|
6月前
|
Java
反射&代理
反射&代理
59 0
|
2月前
|
设计模式 缓存 JavaScript
什么是代理对象
【9月更文挑战第3天】什么是代理对象
102 0
|
4月前
|
Java Spring
AopContext.currentProxy();为什么能获取到代理对象
AopContext.currentProxy();为什么能获取到代理对象
166 0
反射和反射的方法
反射和反射的方法
|
6月前
|
Java Spring
CGLIB代理使用与原理详解
CGLIB代理使用与原理详解
88 0
|
设计模式 Java
反射和代理
反射和代理
71 0
|
缓存 前端开发 JavaScript
一起来学反射(上)
一起来学反射
99 0
|
Dubbo Java 应用服务中间件
没有接口实现类代理
没有接口实现类代理
141 0
没有接口实现类代理
代理与反射(一)
代理与反射(一)
95 0