ES6-代理与反射

246 阅读9分钟

代理与反射

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地 说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

代理基础

最简单的代理是空代理,即除了一个抽象的目标对象,什么也不做,在代理对象上执行所有简单操作都会无障碍的传播到目标对象上。通过proxy构造函数创建对象,该构造函数接收两个参数:目标对象和处理程序对象。

const target = {
    id: 'target'
}

const handler = {}
const proxy = new Proxy(target, handler)

proxy.id = 'foo'
console.log(target.id, proxy.id) //foo

感觉代理对象就是目标对象的一个浅拷贝。

使用代理的主要目的就是可以定义捕获器,而捕获器就是在处理程序中定义的基本操作拦截器。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上使用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

const target = {
    id: 'target'
}

const handler = {
  get(){
    return 'no target'
  }
}
const proxy = new Proxy(target, handler)

proxy.id = 'foo'
console.log(target.id, proxy.id) //foo no target

当通过代理对象执行 get()操作时,就会触发定义的 get()捕获器。当然,get()不是 ECMAScript 对象可以调用的方法。这个操作在 JavaScript 代码中可以通过多种形式触发并被 get()捕获 器拦截到。proxy[property]、proxy.property 或 Object.create(proxy)[property]等操作都 会触发基本的 get()操作以获取属性。因此所有这些操作只要发生在代理对象上,就会触发 get()捕获 器。注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正 常的行为。

这里的get()被称之为捕获器,有三个参数: 目标对象、要查询的属性和代理对象 。

const target = {
    id: 'target'
}

const handler = {
  get(trapTarget, property, receiver) {
    return trapTarget[property];
  }
};
const proxy = new Proxy(target, handler)

console.log(target.id, proxy.id) //target target

调用全局 Reflect 对象上(封装了原始行为)的同名方法也可以重建对象原始操作。

const target = {
    id: 'target'
}

const handler = {
  get() {
    return Reflect.get(...arguments);
  }
  //get: Reflect.get 
}; 
const proxy = new Proxy(target, handler)

// proxy.id = 'foo'
console.log(target.id, proxy.id) //target target

如果想定义一个可以捕获所有方法,然后将每个方法转发给对应反射 API 的空代理,那么甚至不需要定义处理程序对象 :

const target = {
    id: 'target'
}
const proxy = new Proxy(target, Reflect); 

console.log(target.id, proxy.id) //target target

下面这个例子,在访问某个属性时,进行一些修改:

const target = {
  name: 'zhangxiaohua',
  age:'76'
}

const handler = {
  get(trapTarget, property, receiver) {
    if(property === 'name'){
      return 'i love ' + trapTarget[property];
    }
  }
};
const proxy = new Proxy(target, handler); 

console.log(target.name, proxy.name) //zhangxiaohua i love zhangxiaohua

使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。根据 ECMAScript 规范,每个 捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式” (trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的 值时,会抛出 TypeError:

const target = {};
Object.defineProperty(target, 'foo', {
  configurable: false,
  writable: false,
  value: 'bar'
});
const handler = {
  get() {
    return 'qux';
  }
};
const proxy = new Proxy(target, handler);
console.log(target.foo)
console.log(proxy.foo);
//'get' on proxy: property 'foo' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual 

对于使用 new Proxy()创建的普通代理来 说,这种联系会在代理对象的生命周期内一直持续存在, Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的 操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的结果都一样。撤销代理之后 再调用代理会抛出 TypeError 。

const target = {
  foo: 'bar'
};
const handler = {
  get() {
    return 'intercepted';
  }
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError 

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

const target = {
  name:'zhangxiaohua',
  age:'76'
 };
const firstProxy = new Proxy(target, {
  get(trapTarget, property, receiver) {
    if(property === 'name'){
      return 'i love ' +  trapTarget[property]
    }
  }
});
const secondProxy = new Proxy(firstProxy, {
  get(trapTarget, property, receiver) {
    if(property === 'name'){
      return 'i much love ' + trapTarget[property]
    }
    
  }
}); 

console.log(secondProxy.name,firstProxy.name,target.name,);

代理捕获器与反射方法

代理可以捕获 13 种不同的基本操作。这些操作有各自不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式。 在代理对象上执行的任何一种操作,只会有一个捕获处理程序被调用,不会存在重复捕获的情况。 只要在代理上调用,所有捕获器都会拦截它们对应的反射 API 操作。

get()

get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()。

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

set()捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。

捕获器处理程序接受三个参数:目标程序、 的目标对象上的字符串键属性 、 代理对象或继承代理对象的对象

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

返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError。

has()

has()捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。

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

has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。

defineProperty()

defineProperty()捕获器会在 Object.defineProperty()中被调用。对应的反射 API 方法为 Reflect.defineProperty()。

defineProperty()必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。

getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

getOwnPropertyDescriptor()捕获器会在 Object.getOwnPropertyDescriptor()中被调 用。对应的反射 API 方法为 Reflect.getOwnPropertyDescriptor()。

getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回 undefined。

deleteProperty()

delete 操作符用于删除对象的某个属性;如果没有指向这个属性的引用,那它最终会被释放。

deleteProperty()捕获器会在 delete 操作符中被调用。对应的反射 API 方法为 Reflect. deleteProperty()。

ownKeys()

Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。

ownKeys()捕获器会在 Object.keys()及类似方法中被调用。对应的反射 API 方法为 Reflect. ownKeys()。

getPrototypeOf()

Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。

getPrototypeOf()捕获器会在 Object.getPrototypeOf()中被调用。对应的反射 API 方法为 Reflect.getPrototypeOf()。

setPrototypeOf()

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null

setPrototypeOf()捕获器会在 Object.setPrototypeOf()中被调用。对应的反射 API 方法为 Reflect.setPrototypeOf()。

isExtendsible()

Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)

isExtensible()捕获器会在 Object.isExtensible()中被调用。对应的反射 API 方法为 Reflect.isExtensible()。

preventExtensions()

Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。

preventExtensions()捕获器会在 Object.preventExtensions()中被调用。对应的反射 API 方法为 Reflect.preventExtensions()。

apply()

apply()捕获器会在调用函数时中被调用。对应的反射 API 方法为 Reflect.apply()。

捕获器处理程序函数接收三个参数:目标对象、调用函数的this参数,调用函数的参数列表

target 必须是一个函数对象。

construct ()

construct()捕获器会在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct()。

捕获器处理程序参数接受三个参数:target目标函数、thisArg调用函数时的this参数、argsmentlist调用函数 时的参数列表

target 必须可以用作构造函数。

常见用法

跟踪属性访问

通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获 器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:

const user = {
  name: 'Jake'
};
const proxy = new Proxy(user, {
  get(target, property, receiver) {
    console.log(`Getting ${property}`);
    console.log(arguments)
    return Reflect.get(...arguments);
  },
  set(target, property, value, receiver) {
    console.log(`Setting ${property}=${value}`);
    return Reflect.set(...arguments);
  }
});
影藏属性

代理的内部实现对外部代码是不可见的,可以实现目标对象上属性的影藏

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);
    }
  }
});
属性验证

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

const target = {
  onlyNumbersGoHere: 0
};
const proxy = new Proxy(target, {
  set(target, property, value) {
    if (typeof value !== 'number') {
      return false;
    } else {
      return Reflect.set(...arguments);
    }
  }
});
  1. 函数与构造参数验证

    可以使用apply捕获器,对函数和构造函数参数进行审查。

    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 provided';
          }
        }
        return Reflect.apply(...arguments);
      }
    });
    

    同时也可以要求实例化时必须给构造函数传参:

    class User {
      constructor(id) {
        this.id_ = id;
      }
    }
    const proxy = new Proxy(User, {
      construct(target, argumentsList, newTarget) {
        if (argumentsList[0] === undefined) {
          throw 'User cannot be instantiated without id';
        } else {
          return Reflect.construct(...arguments);
        }
      }
    });
    
  2. 数据绑定与可观察对象

    通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的 代码互操作。 比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中 :

    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('John');
    new proxy('Jacob');
    new proxy('Jingleheimerschmidt'); 
    
    console.log(userList); 
    // [
    //   User { name_: 'John' }, 
    //   User { name_: 'Jacob' },
    //   User { name_: 'Jingleheimerschmidt' }
    // ]
    

    这就利用捕获器construct(),每次创建User实例时就将新实例添加到userList列表中。

    还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:

    const userList = [];
    function emit(newValue) {
      console.log(newValue);
    }
    const proxy = new Proxy(userList, {
      set(target, property, value, receiver) {
        const result = Reflect.set(...arguments);
        if (result) {
          emit(Reflect.get(target, property, receiver));
        }
        return result;
      }
    });
    proxy.push('John');
    
    proxy.push('Jacob');
    // John
    // 1
    // Jacob
    // 2