持续对红宝书进行输出(六)

114 阅读9分钟

1. 代理与反射

  1. 内容
    1. 代理基础
    2. 代码捕获器与反射方法
    3. 代理模式

1.1.1 创建空代理

  1. 最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。
  2. 代理是使用Proxy构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺少其中任何一个参数都会抛出TypeError。
const target = {
  id: 'target'
}
const handler = {}
const proxy = new Proxy(target,handler)
// id属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target

// 给目标属性赋值会反映在两个对象上
target.id = 'foo'
console.log(target.id); // foo
console.log(proxy.id); // foo

// 给代理属性赋值会反映在两个对象上,因为两个对象访问的是同一个值
proxy.id = 'bar'
console.log(target.id); // bar
console.log(proxy.id); // bar

// hasOwnProperty()方法在两个地方,都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty("id")); // true

// Proxy.prototype是undefined,因此不能使用instanceof操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy);  // TypeError: Function has non-object prototype 'undefined' in instanceof check

// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false

1.1.2 定义捕获器

  1. 使用代理的主要目的是可以定义捕获器。捕获器就是在处理程序中定义的 "基本操作的拦截器"。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
const target = {
  id: 'foo'
}
const handler = {
  // 捕获器在处理程序对象中以方法名作为键
  get(){
    return 'handler override'
  }
}

const proxy = new Proxy(target,handler)
console.log(target.id); // foo
console.log(proxy.id);  // handler override
/**
 *  解析:当通过代理对象执行get()操作时,就会触发定义的get()捕获器。这个操作在javascript代码中
 *  可以通过多种形式触发并被get()捕获器拦截到, proxy[property]、proxy.property或object.create(proxy)[property]
 *  等操作都会触发基本的get()操作以获取属性。因此所有这些操作只要发生在代理对象上,就会触发get()捕获器
 *  
 */
console.log(target['id']); // foo
console.log(proxy['id']); // handler override

console.log(Object.create(target)['id']); // foo
console.log(Object.create(proxy)['id']);  // handler override

1.1.3 捕获器参数和反射API

  1. 所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如get()捕获器会接收到目标对象、要查询的属性、代理对象三个参数。

const target = {
  id: 'foo'
}

const handler = {
  get(trapTarget,property,trapProxy){
    console.log(trapTarget == target);
    console.log(trapProxy == proxy);
    console.log(property);
  }
}
var proxy = new Proxy(target,handler)
proxy.id
// 返回结果 true true id

// 有了这些参数,就可以重建被捕获方法的行为

const handler2 = {
  get(trapTarget,property,trapProxy){
    return trapTarget[property]
  }
}
var proxy2 = new Proxy(target,handler2)
console.log(proxy2.id); // foo
console.log(target.id); // foo

  1. 通过调用全局Reflect对象上的同名方法重建原始操作。处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。
const target = {
  id: 'foo'
}
const handler = {
  get(){
    return Reflect.get(...arguments)
  }
}

var proxy = new Proxy(target,handler)
console.log(proxy.id); // foo
console.log(target.id); // foo

// 也可以写得更加简洁
const handler2 = {
  get(){
    return Reflect.get
  }
}
  1. 实际上,如果真想创建一个可以捕获所有方法,然后将每个方法转发给对应反射API的空代理。那么甚至不需要定义处理程序对象。
const target = {
  id: 'foo'
}
var proxy = new Proxy(target,Reflect)
console.log(target.id); // foo
console.log(proxy.id);  // foo
  1. 反射API对返回值进行修饰
const target = {
  foo: 'bar',
  baz: 'zxc'
}

const handler = {
  get(trapTarget,property,trapProxy){
    let deraction = ''
    if(property == 'foo'){
      deraction = '!!!'
    }
    return Reflect.get(...arguments) + deraction
  }
}
var proxy = new Proxy(target,handler)
console.log(proxy.foo); // bar!!!
console.log(target.foo); // bar

1.1.4 捕获器不变式

  1. 使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。比如,如果目标对象有一个不可配置属性且不可写的属性,那么在捕获器返回一个与该属性不同的值时,就会报错。
const target = {
  id: '123'
}

Object.defineProperty(target,'name',{
  configurable: false,
  enumerable: false,
  writable: false,
  value: 'Nicholas'
})

const handler = {
  get(){
    return 'qux'
  }
}
var proxy = new Proxy(target,handler)
console.log(proxy.name); // TypeError

1.1.5 可撤销代理

  1. 有时候需要中断代理对象与目标对象之间的联系。对于使用new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直存在。
  2. proxy暴露了revocable()方法,这个方法支持撤销代理对象和目标对象之间的关联。撤销代理的操作是不可逆的。撤销代理之后再调用代理会抛出错误。
const target = {
  id: 'foo'
}
const handler = {
  get(){
    return 'intercepted'
  }
}
const { proxy, revoke } = Proxy.revocable(target, handler)
console.log(proxy.id); // foo
console.log(target.id); // foo

// 取消代理
revoke()
console.log(proxy.id); // TypeError 取消代理后再调用抛出错误

1.1.6 实用反射API

  1. 反射API与对象API
    1. 反射API并不限于捕获处理函数
    2. 大多数反射API方法在Object类型上有对应的方法。
    3. 通常,Object上的方法适用于通用程序,而反射方法适用于细粒度的对象控制和操作。
  2. 状态标记
    1. 很多反射方法返回称作“状态标记”的布尔值。表示意图执行的操作是否成功。
// 初始代码
const target = {
  id: 'foo'
}

try {
  Object.defineProperty(target,'name',{
    value: 'Nicholas'
  })
  console.log('success...');
} catch (error) {
  console.log('fail....');
}

// 如果给对象添加属性失败,则会抛出错误执行catch中的打印

// 在定义属性时如果发生问题,Reflect.defineProperty()会返回false,而不是抛出错误

const o = {}

if(Reflect.defineProperty(o,'name','Nicholas')){
  console.log('success...');
}else {
  console.log('fail...');
}

// 下列反射方法都会提供状态标记
// Reflect.defineProperty()
// Reflect.preventExtensions()
// Reflect.setPrototypeOf()
// Reflect.set()
// Reflect.deleteProperty()

1.1.7 代理另一个代理

  1. 代理可以拦截反射API的操作,而这意味着完全可以创建一个代理,通过它去代理另一个代理。这样就可以在一个目标对象之上构建多层拦截网
const target = {
  id: "foo",
};

const firstProxy = new Proxy(target, {
  get() {
    console.log("first proxy");
    return Reflect.get(...arguments);
  },
});

const secondProxy = new Proxy(firstProxy, {
  get() {
    console.log("second proxy");
    return Reflect.get(...arguments);
  },
});

console.log(secondProxy.id);
// second proxy
// first proxy
// foo

1.2 代理捕获器与反射方法

  1. 代理可以捕获13种不同的基本操作。这些操作有各自不同的反射API方法、参数、关联ECMAScript操作和不变式。
  2. 对于在代理对象上执行的任何一种操作,只会有一个捕获处理程序被调用。

1.2.1 get()

  1. get()捕获器会在获取属性值的操作中被调用。对应反射API的方法为Reflect.get()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  get(target,prototype,proxy){
    console.log('get()');
    return Reflect.get(...arguments)
  }
})
proxy.foo
// get()
  1. set() 捕获器会在设置属性值的操作中被调用。对应反射API的方法为Reflect.set()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  set(target,property,receiver){
    console.log('set()');
    return Reflect.set(...arguments)
  }
})

proxy.id = 'bar'
// set()
  1. has() 捕获器会在in操作符中被调用,对应反射API的方法为Reflect.has()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  has(target,property,receiver){
    console.log('has()');
    return Reflect.has(...arguments)
  }
})

'foo' in proxy
// has()
  1. defineProperty()捕获器会在Object.defineProperty()中被调用。对应的反射API方法为Reflect.defineProperty()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  defineProperty(target,property,descriptor){
    console.log('defineProperty...');
    return Reflect.defineProperty(...arguments)
  }
})
Object.defineProperty(proxy,'name',{value: 'Nicholas'})

proxy.name
// defineProperty...
  1. getOwnPropertyDescriptor()捕获器会在Object.getOwnPropertyDescriptor()中被调用。对应反射API的方法为Reflect.getOwnPropertyDescriptor()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  getOwnPropertyDescriptor(target,property){
    console.log('getOwnPropertyDescriptor...');
    return Reflect.getOwnPropertyDescriptor(...arguments)
  }
})

Object.getOwnPropertyDescriptor(proxy,'name')
proxy.name
// getOwnPropertyDescriptor...
  1. deleteProperty()捕获器会在delete操作符中被调用,对应的反射API方法为Reflect.deleteProperty()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  deleteProperty(target,property){
    console.log('deleteProperty...');
    return Reflect.deleteProperty(...arguments)
  }
})

delete proxy.name
// deleteProperty...
  1. ownKeys()捕获器会在Object.keys()及类似的方法中被调用。对应的反射API方法为Reflect.ownKeys()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  ownKeys(target,property){
    console.log('ownKeys...');
    return Reflect.ownKeys(...arguments)
  }
})

Object.keys(proxy)
// ownKeys...
  1. getPrototypeOf()捕获器会在Object.getPrototypeOf()中被调用。对应的反射API方法为Reflect.getPrototypeOf()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  getPrototypeOf(target){
    console.log('getPrototypeOf()...');
    return Reflect.getPrototypeOf(...arguments)
  }
})

Object.getPrototypeOf(proxy)
// getPrototypeOf()...
  1. setPrototypeOf()捕获器会在Object.setPrototypeOf()中被调用,对应的反射API方法为Reflect.getPrototypeOf()
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  setPrototypeOf(target,property){
    console.log('setPrototypeOf()...');
    return Reflect.setPrototypeOf(...arguments)
  }
})

Object.setPrototypeOf(proxy,Object)
// setPrototypeOf()...
  1. isExtensible()捕获器会在Object.isExtensible()中被调用。对应的反射API方法为Reflect.isExtensible()
    1. 使用这个方法可以确定对象是否为可篡改,如果可篡改,则返回true,相反返回false。
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  isExtensible(target){
    console.log('isExtensible...');
    return Reflect.isExtensible(...arguments)
  }
})

Object.isExtensible(proxy)
// isExtensible...
  1. preventExtensions()捕获器会在Object.preventExtensions()中被调用。对应反射API方法为Reflect.preventExtensions()
    1. 使用了Object.preventExtensions()方法,就不能向对象中新添加属性和方法了,但是可以修改对象原有的属性和方法。
const target = {
  id: "foo",
};

const proxy = new Proxy(target,{
  preventExtensions(target){
    console.log('preventExtensions...');
    return Reflect.preventExtensions(...arguments)
  }
})

Object.preventExtensions(proxy)
// preventExtensions...
  1. apply() 捕获器会在调用函数中被调用。对应的反射API为Reflect.apply()
  2. construct()捕获器会在new操作符中被调用。对应的反射API为Reflect.construct()

1.3 代理模式

1.3.1 跟踪属性访问

  1. 通过捕获get、set、has等操作。可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过。
const user = {
  name: 'Jack'
}
const proxy = new Proxy(user,{
  get(target,property,proxy){
    console.log('get()...');
    return Reflect.get(...arguments)
  },
  set(target,propety,proxy){
    console.log('set()...');
    return Reflect.set(...arguments)
  },
  has(target,property){
    console.log('has()...');
    return Reflect.has(...arguments)
  }
})

proxy.name  // get()...
proxy.name = 'Nicholas' // set()...

1.3.2 隐藏属性

  1. 代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举
const hiddenProperties = ['foo','bar']
const targetObject = {
  foo: 1,
  bar: 2,
  baz: 3
}

const proxy = new Proxy(targetObject,{
  get(target,property){
    // 隐藏目标对象上的属性
    if(hiddenProperties.includes(property)){
      return undefined
    }else {
      return Reflect.get(...arguments)
    }
  },
  has(target,property){
    if(hiddenProperties.includes(property)){
      return false
    }else{
      return Reflect.has(...arguments)
    }
  }
})

console.log(proxy.foo); // undefined
console.log(targetObject.foo); // 1
console.log('foo' in proxy); // false
console.log('bar' in targetObject); // true

1.3.3 属性验证

  1. 因为所有赋值操作都会触发Set()捕获器,所以可以根据所赋值的值决定是允许还是拒绝赋值

const target = {
  onlyNumber: 1
}

const proxy = new Proxy(target,{
  // set的参数 目标对象 代理属性 设置的值 代理对象
  set(target,property,value){
    if(typeof value !== 'number'){
      return false
    }else {
      return Reflect.set(...arguments)
    }
  }
})
proxy.onlyNumber = 2
console.log(target.onlyNumber); // 2
proxy.onlyNumber = '2'
console.log(proxy.onlyNumber); // 2

1.3.4 函数与构造函数验证

  1. 跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种类型的值。
function median(...nums) {
  return nums.sort()[Math.floor(nums.length / 2)];
}

const proxy = new Proxy(median, {
  apply(target, thisArg, argumentsList) {
    for (const arg of argumentsList) {
      if (typeof arg !== "number") {
        throw "Non-number argument";
      }
    }
    return Reflect.apply(...arguments);
  },
});
console.log(proxy(1, 4, 5));
// console.log(proxy(1,'4',5));

// 类似地,可以要求实例化时必须给构造函数传参
class User {
  constructor(id) {
    this.id = id;
  }
}

const proxy1 = new Proxy(User, {
  construct(target, argumentsList, newTarget) {
    if (argumentsList[0] == undefined) {
      throw "User connot be id";
    } else {
      return Reflect.construct(...arguments);
    }
  },
});

new proxy1(1); // 
new proxy1(); // 报错

1.3.5 数据绑定与可观察对象

  1. 通过代理可以把运行时中原本不相关的部分联系到一起,这样就可以实现各种模式,从而让不同的代码互操作。
  2. 例如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中
const userList = []
class User {
  constructor(name){
    this.name_ = name
  }
}

const proxy = new Proxy(User,{
  construct(target,argumentList,newTarget){
    const newUser = Reflect.construct(...arguments)
    userList.push(newUser)
    return newUser
  }
})
new proxy('Nicholas')
new proxy('Bob')
new proxy('Jack')

console.log(userList);

// [
//   User { name_: 'Nicholas' },
//   User { name_: 'Bob' },
//   User { name_: 'Jack' }
// ]