2

代理与反射(二)

 1 year ago
source link: https://www.clzczh.top/2022/05/18/%E4%BB%A3%E7%90%86%E4%B8%8E%E5%8F%8D%E5%B0%84-%E4%BA%8C/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

代理与反射(二)

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

通过添加对应捕获器,就可以捕获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
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
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
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
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
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
image-20220503235521715

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

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

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

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

  emit(Reflect.get(target, property, receiver))
}
image-20220504000010868
image-20220504000010868

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK