美文网首页
12. 代理和反射

12. 代理和反射

作者: 莣忧草_3b53 | 来源:发表于2020-07-01 16:54 被阅读0次

    代理(Proxy)是一种可以拦截并改变底层js引擎操作的包装器。

    12.1 数组问题

    我们操作数组的length属性,可以对数组进行数组元素的增加或者删除。但是在ES5中是不能模拟实现这种行为。

    let a = [1,2,3];
    a[3] = 4;
    
    a.length // 4
    a[3] // 4
    
    a.length = 2;
    a[2] // undefined
    

    12.2 代理和反射

    调用new Proxy() 可创建代替其它目标对象的代理。代理可以拦截js引擎内部目标的底层对象操作,因此该代理与该目标对象表面上可以被当作同一个对象来对待。这些底层操作被拦截后会触发响应特定操作的陷阱函数。
    反射API以reflect对象的形式出现,对象中方法默认特性与相同的底层操作一致,而代理可以复写这些方法。

    12.3 创建一个简单代理

    使用 Proxy 构造器来创建一个代理时,需要传递两个参数:目标对象以及一个处理器(handler),后者是定义了一个或多个陷阱函数的对象。

    let target = {};
    let proxy = new Proxy(target, {});
    
    proxy.name = "proxy";
    console.log(proxy.name, target.name); // proxy proxy
    
    target.name = "target";
    console.log(proxy.name, target.name);// target target
    

    代理只是简单的将操作转发给目标,他不会存储值。在proxy上创建name属性,在target上就会创建name属性。由于proxy.name和target.name都是引用target.name,因此两者读取的值是相同的。

    12.4 使用set陷阱验证对象结构

    创建属性值是数字的对象,一旦属性值不是数字,就抛出错误。应该怎么做?
    set 陷阱函数接受四个参数:

    1. trapTarget :将接收属性的对象(即代理的目标对象);
    2. key :需要写入的属性的键(字符串类型或符号类型);
    3. value :将被写入属性的值;
    4. receiver :操作发生的对象(通常是代理对象)。
    let target = {
      name: "target"
    };
    let proxy = new Proxy(target,{
      set(trapTarget, key, value, receiver) {
        if (!trapTarget.hasOwnProperty(key)) {
          if (isNaN(value)) {
            throw new TypeError("Property must be a number.");
          }
        }
    
        return Reflect.set(trapTarget, key, value, receiver);
      }
    })
    

    12.5 使用get陷阱验证对象结构

    JS 语言读取对象不存在的属性时并不会抛出错误,而会把 undefined 当作该属性的值

    let proxy = new Proxy({}, {
      get(trapTarget, key, receiver) {
        if (!(key in receiver)) {
          throw new TypeError("Property " + key + " doesn't exist.");
        }
        return Reflect.get(trapTarget, key, receiver);
      }
    });
    
    // 添加属性的功能正常
    proxy.name = "proxy";
    console.log(proxy.name); // "proxy"
    

    12.6 使用has陷阱隐藏已有属性

    let target = {
      name: "target",
      value: 42
    };
    let proxy = new Proxy(target, {
      has(trapTarget, key) {
        if (key === "value") {
          return false;
        } else {
          return Reflect.has(trapTarget, key);
        }
      }
    });
    console.log("value" in proxy); // false
    console.log("name" in proxy); // true
    console.log("toString" in proxy); // true
    

    12.7 使用deleteProperty陷阱防止删除属性

    let target = {
      name: "target",
      value: 42
    };
    let proxy = new Proxy(target, {
      deleteProperty(trapTarget, key) {
        if (key === "value") {
          return false;
        } else {
          return Reflect.deleteProperty(trapTarget, key);
        }
      }
    });
    
    // 尝试删除 proxy.value
    console.log("value" in proxy); // true
    let result1 = delete proxy.value;
    console.log(result1); // false
    console.log("value" in proxy); // true
    // 尝试删除 proxy.name
    console.log("name" in proxy); // true
    let result2 = delete proxy.name;
    console.log(result2); // true
    console.log("name" in proxy); // false
    

    12.8 原型代理陷阱

    代理允许你通过 setPrototypeOf 与getPrototypeOf 陷阱函数来对这两个方法的操作进行拦截。 Object 对象上的这两个方法都会调用代理中对应名称的陷阱函数,从而允许你改变这两个方法的行为。
    setPrototypeOf陷阱函数接受三个参数:

    1. trapTarget :需要设置原型的对象(即代理的目标对象);
    2. proto :需用被用作原型的对象。

    Object.setPrototypeOf() 方法与 Reflect.setPrototypeOf() 方法会被传入相同的参数。 getPrototypeOf 陷阱函数只接受 trapTarget 参数,Object.getPrototypeOf() 方法与 Reflect.getPrototypeOf() 方法也是如此

    12.8.1 原型代理陷阱的运行机制
    let target = {};
    let proxy = new Proxy(target, {
      getPrototypeOf(trapTarget) {
        return Reflect.getPrototypeOf(trapTarget);
      },
      setPrototypeOf(trapTarget, proto) {
        return Reflect.setPrototypeOf(trapTarget, proto);
      }
    });
    let targetProto = Object.getPrototypeOf(target);
    let proxyProto = Object.getPrototypeOf(proxy);
    
    console.log(targetProto === Object.prototype); // true
    console.log(proxyProto === Object.prototype); // true
    // 成功
    Object.setPrototypeOf(target, {});
    // 同样成功
    Object.setPrototypeOf(proxy, {});
    
    12.8.2 为什么有两组方法

    Reflect.getPrototypeOf() 方法是对内部的 [[GetPrototypeOf]] 操作的封装(并附加了一些输入验证),而 Reflect.setPrototypeOf() 方法与 [[SetPrototypeOf]] 操作之间也存在类似的关系。

    Reflect.getPrototypeOf() 方法在接收到的参数不是一个对象时会抛出错误,而Object.getPrototypeOf() 则会在操作之前先将参数值转换为一个对象。

    let target1 = {};
    let result1 = Object.setPrototypeOf(target1, {});
    console.log(result1 === target1); // true
    
    let target2 = {};
    let result2 = Reflect.setPrototypeOf(target2, {});
    console.log(result2 === target2); // false
    console.log(result2); // true
    

    Object.setPrototypeOf() 方法将第一个值作为返回值,而Reflect.setPrototypeOf() 方法则返回true/false 。

    12.9 对象可扩展性陷阱

    ES5 通过 Object.preventExtensions()(// 是否成功) 与 Object.isExtensible() (//是否可以操作)方法给对象增加了可扩展性。ES6 通过 preventExtensions 与 isExtensible 陷阱函数允许代理拦截对于底层对象的方法调用

    12.9.1 两个基础示例
    let target = {};
    let proxy = new Proxy(target, {
      isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
      },
      preventExtensions(trapTarget) {
        return Reflect.preventExtensions(trapTarget);
      }
    });
    
    console.log(Object.isExtensible(target)); // true
    console.log(Object.isExtensible(proxy)); // true
    Object.preventExtensions(proxy);
    console.log(Object.isExtensible(target)); // false
    console.log(Object.isExtensible(proxy)); // false
    
    let target = {};
    let proxy = new Proxy(target, {
      isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
      },
      preventExtensions(trapTarget) {
        return false
      }
    });
    console.log(Object.isExtensible(target)); // true
    console.log(Object.isExtensible(proxy)); // true
    Object.preventExtensions(proxy);
    console.log(Object.isExtensible(target)); // true
    console.log(Object.isExtensible(proxy)); // true
    // ,在 Chrome 中却会在Object.preventExtensions(proxy) 这一行抛出错误
    
    12.9.2 可扩展性的重复方法

    Object.isExtensible() 方法与Reflect.isExtensible() 方法几乎一样,只在接收到的参数不是一个对象时才有例外。此时Object.isExtensible() 总是会返回 false ,而Reflect.isExtensible() 则会抛出一个错误。

    12.10 属性描述符陷阱

    代理可以使用 defineProperty(trapTarget, key, descriptor) 与 getOwnPropertyDescriptor 陷阱函数,来分别拦截对于Object.defineProperty() 与 Object.getOwnPropertyDescriptor() 的调用。defineProperty 陷阱函数要求你在操作成功时返回 true ,否则返回 false 。

    12.10.1 阻止 Object.defineProperty()
    let proxy = new Proxy({}, {
      defineProperty(trapTarget, key, descriptor) {
        if (typeof key === "symbol") {
          return false;
        }
        return Reflect.defineProperty(trapTarget, key, descriptor);
      }
    });
    Object.defineProperty(proxy, "name", {
      value: "proxy"
    });
    console.log(proxy.name); // "proxy"
    let nameSymbol = Symbol("name");
    // 抛出错误
    Object.defineProperty(proxy, nameSymbol, {
      value: "proxy"
    });
    
    12.10.2 描述符对象限制

    传递给defineProperty 陷阱函数的描述符对象参数,则只有 enumerable 、 configurable 、value 、 writable 、 get 与 set 这些属性是被许可的。

    let proxy = new Proxy({}, {
      defineProperty(trapTarget, key, descriptor) {
        console.log(descriptor.value); // "proxy"
        console.log(descriptor.name); // undefined
        return Reflect.defineProperty(trapTarget, key, descriptor);
       }
    });
    Object.defineProperty(proxy, "name", {
      value: "proxy",
      name: "custom"
    });
    

    getOwnPropertyDescriptor 陷阱函数有一个微小差异,要求返回值必须是 null 、
    undefined ,或者是一个对象。如果返回值是一个对象,则只允许该对象拥有 enumerable、 configurable 、 value 、 writable 、 get 或 set 这些自有属性。

    12.10.3 重复的描述符方法

    Object.defineProperty() 方法与 Reflect.defineProperty() 方法几乎一模一样,只是返回值有区别。前者返回调用它时的第一个参数,而后者在操作成功时返回 true 、失败时返回false 。
    Object.getOwnPropertyDescriptor() 方法会在接收的第一个参数是一个基本类型值时,将该参数转换为一个对象。另一方面Reflect.getOwnPropertyDescriptor() 方法则会在第一个参
    数是基本类型值的时候抛出错误。

    12.11 ownKeys陷阱

    ownKeys 代理陷阱拦截了内部方法 [[OwnPropertyKeys]] ,可以返回一个数组用于重写该行为。返回的这个数组会被用于四个方法: Object.keys() 方法、
    Object.getOwnPropertyNames() 方法、 Object.getOwnPropertySymbols() 方法与Object.assign() 方法,其中 Object.assign() 方法会使用该数组来决定哪些属性会被复制。

    ownKeys 陷阱函数的默认行为由 Reflect.ownKeys() 方法实现,会返回一个由全部自有属性的键构成的数组,无论键的类型是字符串还是符号。Object.getOwnProperyNames() 方法与Object.keys() 方法会将符号值从该数组中过滤出去;

    12.12 函数代理中的apply和construct陷阱

    在所有的代理陷阱中,只有 apply 与 construct 要求代理目标对象必须是一个函数。
    函数拥有两个内部方法: [[Call]] 与 [[Construct]] 274
    前者会在函数被直接调用时执行,而后者会在函数被使用 new 运算符调用时执行。

    let target = function() { return 42 },
    proxy = new Proxy(target, {
      apply: function(trapTarget, thisArg, argumentList) {
        return Reflect.apply(trapTarget, thisArg, argumentList);
      },
      construct: function(trapTarget, argumentList) {
        return Reflect.construct(trapTarget, argumentList);
      }
    });
    // 使用了函数的代理,其目标对象会被视为函数
    console.log(typeof proxy); // "function"
    console.log(proxy()); // 42
    var instance = new proxy();
    console.log(instance instanceof proxy); // true
    console.log(instance instanceof target); // true
    
    12.12.1 验证函数参数
    // 将所有参数相加
    function sum(...values) {
      return values.reduce((previous, current) => previous + current, 0);
    }
    let sumProxy = new Proxy(sum, {
      apply: function(trapTarget, thisArg, argumentList) {
        argumentList.forEach((arg) => {
          if (typeof arg !== "number") {
            throw new TypeError("All arguments must be numbers.");
          }
        });
        return Reflect.apply(trapTarget, thisArg, argumentList);
      },
      construct: function(trapTarget, argumentList) {
        throw new TypeError("This function can't be called with new.");
      }
    });
    console.log(sumProxy(1, 2, 3, 4)); // 10
    // 抛出错误
    console.log(sumProxy(1, "2", 3, 4));
    // 同样抛出错误
    let result = new sumProxy();
    
    12.12.2 不用new调用构造函数

    假设 Numbers 函数是硬编码的,无法被修改,已知该代码依赖于 new.target ,而你想要在调用函数时避免这个检查。在“必须使用 new ”这一限制已经确定的情况下,你可以使用apply 陷阱函数来规避它

    function Numbers(...values) {
      if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
      }
      this.values = values;
    }
    let NumbersProxy = new Proxy(Numbers, {
      apply: function(trapTarget, thisArg, argumentsList) {
        return Reflect.construct(trapTarget, argumentsList);
      }
    });
    let instance = NumbersProxy(1, 2, 3, 4);
    console.log(instance.values); 
    
    12.12.3 覆写抽象基类构造函数

    你可以进一步指定 Reflect.construct() 的第三个参数,用于给 new.target 赋值。

    class AbstractNumbers {
      constructor(...values) {
      if (new.target === AbstractNumbers) {
        throw new TypeError("This function must be inherited from.");
      }
      this.values = values;
      }
    }
    let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
      construct: function(trapTarget, argumentList) {
        return Reflect.construct(trapTarget, argumentList, function() {});
      }
    });
    let instance = new AbstractNumbersProxy(1, 2, 3, 4);
    console.log(instance.values); // [1,2,3,4]
    
    12.12.4 可调用的类构造函数
    class Person {
      constructor(name) {
        this.name = name;
      }
    }
    let PersonProxy = new Proxy(Person, {
      apply: function(trapTarget, thisArg, argumentList) {
        return new trapTarget(...argumentList);
      }
    });
    let me = PersonProxy("Nicholas");
    console.log(me.name); // "Nicholas"
    console.log(me instanceof Person); // true
    console.log(me instanceof PersonProxy); // true
    

    12.13 可撤销代理

    在被创建之后,代理通常就不能再从目标对象上被解绑。
    Proxy.revocable() 方法来创建一个可被撤销的代理,该方法接受的参数一个目标对象、一个代理处理器,而返回值是包含下列属性的一个对象:

    1. proxy :可被撤销的代理对象;
    2. revoke :用于撤销代理的函数。

    当 revoke() 函数被调用后,就不能再对该 proxy 对象进行更多操作,任何与该代理对象交互的意图都会触发代理的陷阱函数,从而抛出一个错误。

    let target = {
      name: "target"
    };
    let { proxy, revoke } = Proxy.revocable(target, {});
    console.log(proxy.name); // "target"
    revoke();
    // 抛出错误
    console.log(proxy.name);
    

    相关文章

      网友评论

          本文标题:12. 代理和反射

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