ES6 Proxy代理器

作者: 生命里那束光 | 来源:发表于2022-05-09 09:45 被阅读0次

    一、概述

    • Proxy 对象是ES6新出的一个特性,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
    • Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。不直接操作对象,而是像代理模式,通过对象的代理对象进行操作,在进行这些操作时,可以添加一些需要的额外操作。

    1.1 为什么Proxy会取代Object.defineProperty:

    在Vue2中双向数据绑定原理(数据劫持)采用Object.defineProperty,而在Vue3中数据劫持原理采用的是Proxy代理。

    1. Object.defineProperty只能劫持对象的属性,不能监听数组。也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。也不能监听新增和删除操作等等。
    2. Proxy可以直接监听整个对象而非属性,可以监听数组的变化,具有多达13中拦截方法。

    1.2 使用:

    let p = new Proxy(target, handler);

    • 参数 target 为包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
    • 参数handler为具体操作,其实就是一个对象,其属性是当执行一个操作时定义代理的行为的函数。就是说里面写各种拦截的函数。不同的拦截方法拦截的是不同的操作。

    二、Proxy 实例的方法

    • 总的来说Proxy代理能够实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

    下面是 Proxy 支持的拦截操作一览,一共 13 种。

    1. get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
    2. set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
    3. has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
    4. deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
    5. ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
    6. getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
    7. defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
    8. preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
    9. getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
    10. isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
    11. setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
    12. apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
    13. construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
    1. get方法:

    get(target, property, receiver)方法用于拦截某个属性的读取操作,可以接受三个参数,分别为目标对象、属性名和 proxy 实例本身。看下面例子能不能拦截:

    let obj = { name: "jack", age: "20" };
    // 给obj设置一个代理
    let p = new Proxy(obj, {
        get(target, property) {
            console.log("我拦截了" + target + "读取" + property);
            console.log("它的值为" + target[property]);
            // 定义你要返回的值
            return target[property];
        },
    });
          
    //读取obj的age属性看看,注意定义代理后得用代理来调用属性或方法
    console.log(p.age);
    

    结果如下,可以发现当我们想输出obj.age时被proxy代理拦截到了,并且我们可以通过代理的get方法自定义实际要返回的是什么。就是说虽然你读取obj.age的值为20,但是我可以通过代理拦截,想返回多少就多少,当然我们上面返回的是真实的值 target[property] :

    2. set方法:

    set(target, property, value, receiver)方法用来拦截某个属性的赋值操作,四个参数依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。看下面例子:

    let obj = { name: "jack", age: "20" };
    let p = new Proxy(obj, {
        set(target, property, value) {
            console.log("要设置对象属性?我拦截到了~");
            console.log("要修改成" + value + "?");
            console.log("我就不给你改,我改成666");
                target[property] = 666;
        },
        get(target, property) {
            return target[property];
        },
    });
    //修改obj.age的值为30;
    p.age = 30;
    //读取obj的age属性看看,注意定义代理后得用代理来调用属性或方法
    console.log(p.age);
    

    可以看到,我想修改obj.age为30,但是没成功,因为我在代理拦截set方法里把age值改为666了,所以age值变成了666:

    3. apply方法:

    apply方法能拦截函数的调用、call和apply操作。apply(target, thisArg, argumentsList) 三个参数,分别是目标对象、被调用时的上下文对象、被调用的参数数组。如下:

    function add(a, b) {
        console.log(a + b);
    }
    //给add函数设置一个代理
    let p = new Proxy(add, {
        apply(target, thisArg, argumentsList) {
            console.log("拦截");
            //正常应该如下设置:
            target.apply(this.Arg, argumentsList);
        },
    });
    p(1, 2);
    p.call(null, 13, 22);
    p.apply(null, [5, 3]);
    
    

    可以看到,函数调用,call调用,apply调用,都执行了代理apply方法里定义的代码:

    4. constructor方法:

    construct(target, argumentsList, newTarget)拦截的是new操作,target目标对象,argumentsList参数列表,newTarget最初被调用的构造函数。简单来说就是拦截new一个构造函数的方法。

    function Animal(name) {
        this.name = name;
    }
    let p = new Proxy(Animal, {
        construct(target, argumentsList, newTarget) {
            //我直接返回一个空对象
            return {};
        },
    });
    //new一个Animal实例
    let dog = new p("dog");
    console.log(dog);
    

    结果如下,拦截成功,并修改返回:

    三、Proxy.revocable() 取消

    Proxy.revocable方法返回一个可取消的 Proxy 实例。

    let target = {};
    let handler = {};
    
    let {proxy, revoke} = Proxy.revocable(target, handler);
    
    proxy.foo = 123;
    proxy.foo // 123
    
    revoke();
    proxy.foo // TypeError: Revoked
    

    Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

    Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

    四、this 问题

    虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。

    主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

    例子1:

    const target = {
      m: function () {
        console.log(this === proxy);
      }
    };
    const handler = {};
    
    const proxy = new Proxy(target, handler);
    
    target.m() // false
    proxy.m()  // true
    
    • 上面代码中,一旦proxy代理targettarget.m()内部的this就是指向proxy,而不是target。所以,虽然proxy没有做任何拦截,target.m()proxy.m()返回不一样的结果。

    例子2:

    下面是一个例子,由于this指向的变化,导致 Proxy 无法代理目标对象。

    const _name = new WeakMap();
    
    class Person {
      constructor(name) {
        _name.set(this, name);
      }
      get name() {
        return _name.get(this);
      }
    }
    
    const jane = new Person('Jane');
    jane.name // 'Jane'
    
    const proxy = new Proxy(jane, {});
    proxy.name // undefined
    
    • 上面代码中,目标对象janename属性,实际保存在外部WeakMap对象_name上面,通过this键区分。由于通过proxy.name访问时,this指向proxy,导致无法取到值,所以返回undefined

    • 此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。

    五、实例:Web 服务的客户端

    Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。

    const service = createWebService('http://example.com/data');
    
    service.employees().then(res => {
      const employees = JSON.parse(res);
      // ···
    });
    

    上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。

    function createWebService(baseUrl) {
      return new Proxy({}, {
        get(target, propKey, receiver) {
          return () => httpGet(baseUrl + '/' + propKey);
        }
      });
    }
    

    同理,Proxy 也可以用来实现数据库的 ORM 层。

    相关文章

      网友评论

        本文标题:ES6 Proxy代理器

        本文链接:https://www.haomeiwen.com/subject/pbyqurtx.html