ES6躬行记(24)——代理和反射

  代理和反射是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])

 

转载于:https://www.cnblogs.com/strick/p/10429469.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值