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]); //
Reflect.apply(Math.ceil, null, [2.5]); //
上面代码的第一条语句比较绕,需要将其分解成两部分: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躬行记(24)——代理和反射的更多相关文章
- ES6躬行记(1)——let和const
古语云:“纸上得来终觉浅,绝知此事要躬行”.的确,不管看了多少本书,如果自己不实践,那么就很难领会其中的精髓.自己研读过许多ES6相关的书籍和资料,平时工作中也会用到,但在用到时经常需要上搜索引擎中查 ...
- ES6躬行记 笔记
ES6躬行记(18)--迭代器 要实现以下接口## next() ,return,throw 可以用for-of保证迭代对象的正确性 例如 var str = "向
- ES6躬行记(21)——类的继承
ES6的继承依然是基于原型的继承,但语法更为简洁清晰.通过一个extends关键字,就能描述两个类之间的继承关系(如下代码所示),在此关键字之前的Man是子类(即派生类),而在其之后的People是父 ...
- ES6躬行记(13)——类型化数组
类型化数组(Typed Array)是一种处理二进制数据的特殊数组,它可像C语言那样直接操纵字节,不过得先用ArrayBuffer对象创建数组缓冲区(Array Buffer),再映射到指定格式的视图 ...
- ES6躬行记(3)——解构
解构(destructuring)是一种赋值语法,可从数组中提取元素或从对象中提取属性,将其值赋给对应的变量或另一个对象的属性.解构地目的是简化提取数据的过程,增强代码的可读性.有两种解构语法,分别是 ...
- ES6躬行记(9)——字符串
在介绍字符串之前,有必要先了解一点Unicode的基础知识,有助于理解ES6提供的新功能和新特性. 一.Unicode Unicode是一种字符集(即多个字符的集合),它的目标是涵盖世界上的所有字符, ...
- ES6躬行记(7)——代码模块化
在ES6之前,由于ECMAScript不具备模块化管理的能力,因此往往需要借助第三方类库(例如遵守AMD规范的RequireJS或遵循CMD规范的SeaJS等)才能实现模块加载.而自从ES6引入了模块 ...
- ES6躬行记(4)——模板字面量
模板字面量(Template Literal)是一种能够嵌入表达式的格式化字符串,有别于普通字符串,它使用反引号(`)包裹字符序列,而不是双引号或单引号.模板字面量包含特定形式的占位符(${expre ...
- ES6躬行记(15)——箭头函数和尾调用优化
一.箭头函数 箭头函数(Arrow Function)是ES6提供的一个很实用的新功能,与普通函数相比,不但在语法上更为简洁,而且在使用时也有更多注意点,下面列出了其中的三点: (1)由于不能作为构造 ...
随机推荐
- zookeeper学习0
参考文献: 5分钟让你了解 ZooKeeper 的功能和原理 Zookeeper专题——1.分布式事务(a概述) Zookeeper专题——2.分布式锁-基于Zookeeper的分布式锁
- RedirectAttributes
RedirectAttributes是Spring mvc 3.1版本之后出来的一个功能,专门用于重定向之后还能带参数跳转的 他有两种带参的方式: 第一种: attr.addAttribute(&q ...
- Fedora20 安装 MySQL
参考资料: http://www.cnblogs.com/focusj/archive/2011/05/09/2057573.html http://linux.chinaunix.net/techd ...
- CodeForces 429B【dp】
题意: 在一个n*m的矩阵中有两只虫子,一只从左上角向右下角移动,另外一只从左下角向右上角移动. 要求: 1.第一只虫子每次只能向左或者向下移动一格,另外一只只能向上或者向右移动一格. 2.两只虫子的 ...
- Spring Boot中验证码实现kaptcha
要生成验证码网上的方案比较多,基本是基于两大类:1为自定义生成,操作用Image类,2为kaptcha生成,有模糊算法. 当然也可以直接交由前端进行处理 1.基于kaptcha 首先不要怀疑的是报名是 ...
- Maven查看依赖树
1.命令行,但是只能查看一层的引用 mvn dependency:tree 如图所示: 2.使用Eclipse的Dependency Hierarchy查看,这个就比较深层次的查看,如图所示: 3.使 ...
- oracle数据库 SQL语句、内置函数大全
1.数值函数 函数 返回值 样例 显示 CEIL(n) 大于或等于数值n的最小整数 SELECT CEIL(10.6) FROM TABLE_NAME; 11 FLOOR(n) 小于等于数 ...
- Mark 创建路径(c#)-动态分段
http://bbs.esrichina-bj.cn/ESRI/viewthread.php?action=printable&tid=128564 public void CreateRou ...
- 微信小程序之 Classify(商品属性分类)
1.项目目录 2.逻辑层 broadcast.js // pages/broadcast/broadcast.js Page({ /** * 页面的初始数据 */ data: { firstIndex ...
- 【Mongodb教程 第四课 】MongoDB 创建集合
reateCollection() 方法 MongoDB db.createCollection(name, options) 是用来创建集合. 语法: 基本的 createCollection() ...