24、代理和反射
最后更新于:2022-04-02 06:07:46
代理和反射是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罗列了目前所有可用的陷阱,第二列表示当前陷阱可拦截的行为,注意,只挑选了其中的几个用于展示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/e3/57/e357af899d9664edec85ebb796cc5f92_1311x1317.png)
:-: 表12 十三种陷阱
目前支持的拦截就上面几种,像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])
~~~
*****
> 原文出处:
[博客园-ES6躬行记](https://www.cnblogs.com/strick/category/1372951.html)
[知乎专栏-ES6躬行记](https://zhuanlan.zhihu.com/pwes6)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎浏览。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
';