代理和反射是ES6新增的两个特性,二者之间是协调合做的关系,它们的具体功能将在接下来的章节中分别讲解。编程
ES6引入代理(Proxy)地目的是拦截对象的内置操做,注入自定义的逻辑,改变对象的默认行为。也就是说,将某些JavaScript内部的操做暴露了出来,给予开发人员更多的权限。这实际上是一种元编程(metaprogramming)的能力,即把代码当作数据,对代码进行编程,改变代码的行为。数组
在ES6中,代理是一种特殊的对象,若是要使用,须要像下面这样先生成一个Proxy实例。app
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种可用的陷阱,在后面的章节中会对它们作简单的介绍。this
1)陷阱spa
表12罗列了目前全部可用的陷阱,第二列表示当前陷阱可拦截的行为,注意,只挑选了其中的几个用于展现。prototype
表12 十三种陷阱代理
陷阱 | 拦截 | 返回值 |
get() | 读取属性 | 任意值 |
set() | 写入属性 | 布尔值 |
has() | in运算符 | 布尔值 |
deleteProperty() | delete运算符 | 布尔值 |
getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() | 属性描述符对象 |
defineProperty() | Object.defineProperty() | 布尔值 |
preventExtensions() | Object.preventExtensions() | 布尔值 |
isExtensible() | Object.isExtensible() | 布尔值 |
getPrototypeOf() | Object.getPrototypeOf() __proto__code 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])