代理和反射是ES6新增的两个特性,两者之间是协调合作的关系,它们的具体功能将在接下来的章节中分别讲解。

一、代理

  ES6引入代理(Proxy)地目的是拦截对象的内置操作,注入自定义的逻辑,改变对象的默认行为。也就是说,将某些JavaScript内部的操作暴露了出来,给予开发人员更多的权限。这其实是一种元编程(metaprogramming)的能力,即把代码看成数据,对代码进行编程,改变代码的行为。

  在ES6中,代理是一种特殊的对象,如果要使用,需要像下面这样先生成一个Proxy实例。

new Proxy(target, handler);

  构造函数Proxy()有两个参数,其中target是要用代理封装的目标对象,handler也是一个对象,它的方法被称为陷阱(trap),用于指定拦截后的行为。下面是一个代理的简单示例。

var obj = {},
  handler = {
    set(target, property, value, receiver) {
      target[property] = "hello " + value;
    }
  },
  p = new Proxy(obj, handler);
p.name = "strick";
console.log(p.name);        //"hello strick"

  在上面的代码中,p是一个Proxy实例,它的目标对象是obj,使用了属性相关的陷阱:set()方法。当它写入obj的name属性时,会对其进行拦截,在属性值之前加上“hello ”前缀。除了上例使用的set()方法,ES6还给出了另外12种可用的陷阱,在后面的章节中会对它们做简单的介绍。

1)陷阱

  表12罗列了目前所有可用的陷阱,第二列表示当前陷阱可拦截的行为,注意,只挑选了其中的几个用于展示。

表12  十三种陷阱

陷阱 拦截 返回值
 get()  读取属性  任意值
 set()  写入属性  布尔值
 has()  in运算符  布尔值
 deleteProperty()  delete运算符  布尔值
 getOwnPropertyDescriptor()  Object.getOwnPropertyDescriptor()  属性描述符对象
 defineProperty()  Object.defineProperty()  布尔值
 preventExtensions()  Object.preventExtensions()  布尔值
 isExtensible()  Object.isExtensible()  布尔值
 getPrototypeOf()

 Object.getPrototypeOf()              __proto__

 Object.prototype.isPrototypeOf()  instanceof

 对象 
 setPrototypeOf()  Object.setPrototypeOf()   布尔值
 apply()

 Function.prototype.apply()           函数调用

 Function.prototype.call()

 任意值
 construct()  new运算符作用于构造函数  对象
 ownKeys()

 Object.getOwnPropertyNames()    Object.keys()

 Object.getOwnPropertySymbols()  for-in循环

 数组

  目前支持的拦截就上面几种,像typeof运算符、全等比较等操作还不被ES6支持。接下来会挑选其中的两次个陷阱,讲解它们的简单应用。

  在JavaScript中,当读取对象上不存在的属性时,不会报错而是返回undefined,这其实在某些情况下会发生歧义,现在利用陷阱中的get()方法就能改变默认行为,如下所示。

var obj = {
    name: "strick"    
  },
  handler = {
    get(target, property, receiver) {
      if(property in target)
        return target[property];
      throw "未定义的错误";  
    }
  },
  p = new Proxy(obj, handler);
p.name;          //"strick"
p.age;            //未定义的错误

  在get()方法中有3个参数,target是目标对象(即obj),property是读取的属性的名称(即“name”和“age”),receiver是当前的Proxy实例(即p)。在读取属性时,会用in运算符判断当前属性是否存在,如果存在就返回相应的属性值,否则就会抛出错误,这样就能避免歧义的出现。   

  在众多陷阱中,只有apply()和construct()的目标对象得是函数。以apply()方法为例,它有3个参数,target是目标函数,thisArg是this的指向,argumentsList是函数的参数序列,它的具体使用如下所示。

function getName(name) {
  return name;
}
var obj = {
    prefix: "hello "    
  },
  handler = {
    apply(target, thisArg, argumentsList) {
      if(thisArg && thisArg.prefix)
        return target(thisArg.prefix + argumentsList[0]);
      return target(...argumentsList);
    }
  },
  p = new Proxy(getName, handler);
p("strick");                //"strick"
p.call(obj, "strick");        //"hello strick"

  p是一个Proxy实例,p("strick")是一次普通的函数调用,此时虽然拦截了,但是仍然会把参数原样传过去;而p.call(obj, "strick")是间接的函数调用,此时会给第一个参数添加前缀,从而改变函数最终的返回值。

2)撤销代理

  Proxy.revocable()方法能够创建一个可撤销的代理,它能接收两个参数,其含义与构造函数Proxy()中的相同,但返回值是一个对象,包含两个属性,如下所列。

(1)proxy:新生成的Proxy实例。

(2)revoke:撤销函数,它没有参数,能把与它一起生成的Proxy实例撤销掉。

  下面是一个简单的示例,obj是目标对象,handler是陷阱对象,传递给Proxy.revocable()后,通过对象解构将返回值赋给了proxy和revoke两个变量。

var obj = {},
  handler = {};
let {proxy, revoke} = Proxy.revocable(obj, handler);
revoke();
delete proxy.name;        //类型错误
typeof proxy;             //"object"

  在调用revoke()函数后,就不能再对proxy进行拦截了。像上例使用delete运算符,就会抛出类型错误,但像typeof之类的不可拦截的运算符还是可以成功执行的。

3)原型

  代理可以成为其它对象的原型,就像下面这样。

var obj = {
    name: "strick"    
  },
  handler = {
    get(target, property, receiver) {
      if(property == "name")
        return "hello " + target[property];
      return true;
    }
  },
  p = new Proxy({}, handler);
Object.setPrototypeOf(obj, p);     //obj的原型指向Proxy实例
obj.name;                      //"strick"
obj.age;                       //true

  p是一个Proxy实例,它会拦截属性的读取操作,obj的原型指向了p,注意,p的目标对象不是obj。当obj读取name属性时,不会触发拦截,因为name是自有属性,所以不会去原型上查找,最终得到的结果是没有前缀的“strick”。之前的代理都是直接作用于相关对象(例如上面的obj),因此只要执行可拦截的动作就会被处理,但现在中间隔了个原型,有了更多的限制。而在读取age属性时,由于自有属性中没有它,因此就会去原型上查找,从而触发了拦截操作,返回了true。

二、反射

  反射(Reflect)向外界暴露了一些底层操作的默认行为,它是一个没有构造函数的内置对象,类似于Math对象,其所有方法都是静态的。代理中的每个陷阱都会对应一个同名的反射方法(例如Reflect.set()、Reflect.ownKeys()等),而每个反射方法又都会关联到对应代理所拦截的行为(例如in运算符、Object.defineProperty()等),这样就能保证某个操作的默认行为可随时被访问到。反射让对象的内置行为变得更加严谨、合理与便捷,具体表现如下所列。

  (1)参数的检验更为严格,Object的getPrototypeOf()、isExtensible()等方法会将非对象的参数自动转换成相应的对象(例如字符串转换成String对象,如下代码所示),而关联的反射方法却不会这么做,它会直接抛出类型错误。

Object.getPrototypeOf("strick") === String.prototype;     //true
Reflect.getPrototypeOf("strick");                         //类型错误

  (2)更合理的返回值,Object.setPrototypeOf()会返回它的第一个参数,而Reflect的同名方法会返回一个布尔值,后者能更直观的反馈设置是否成功,两个方法的对比如下所示。

var obj = {};
Object.setPrototypeOf(obj, String) === obj;                //true
Reflect.setPrototypeOf(obj, String);                       //true

  (3)用方法替代运算符,反射能以调用方法的形式完成new、in、delete等运算符的功能,在下面的示例中,先使用运算符,再给出对应的反射方法。

function func() { }
new func();
Reflect.construct(func, []);

var people = {
    name: "strick"
};
"name" in people;
Reflect.has(people, "name");

delete people["name"];
Reflect.deleteProperty(people, "name");

  (4)避免冗长的方法调用,以apply()方法为例,如下所示。

Function.prototype.apply.call(Math.ceil, null, [2.5]);        //3
Reflect.apply(Math.ceil, null, [2.5]);                      //3

  上面代码的第一条语句比较绕,需要将其分解成两部分:Function.prototype.apply()和call()。ES5规定apply()和call()两个方法在最后都要调用一个有特殊功能的内部函数,如下代码所示,func参数表示调用这两个方法的函数。

[[Call]](func, thisArg, argList)

  内部函数的功能就是在调用func()函数时,传递给它的参数序列是argList,其内部的this指向了thisArg。当执行第一条语句时,传递给[[Call]]函数的三个参数如下所示。

[[Call]](Function.prototype.apply, Math.ceil, [null, [2.5]])

  接下来会调用原型上的apply()方法,由于其this指向了Math.ceil(即当前调用apply()方法的是Math.ceil),因此[[Call]]函数的第一个参数就是Math.ceil,如下所示。

[[Call]](Math.ceil, null, [2.5])
//相当于
Math.ceil.apply(null, [2.5])

 

 posted on 2019-05-13 09:15  咖啡机(K.F.J)  阅读(468)  评论(0编辑  收藏  举报