12. 代理和反射

代理(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. 代理和反射)