ES6对于今天来说,已经不算是一个很新的概念。从2015年第一版ES6发版之后,每一年都有新的版本产生,新版本是该年正式版本的语言标准。因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等。
还有一点就是虽然我们现在也会说ES7、ES8这种,但实际上这种说法是不严谨的,因为ES官方自2016年开始,就不再以ES+版本号这种命名了。相反,这些版本按照年份来命名,例如ECMAScript 2016和ECMAScript 2017。这种命名方式更为准确和清晰,能够直接反映出版本的发布年份。
其中Proxy和Reflect已经是ES2015第一版中出现的了,可见已经算是年代久远了。现在才想到看它实属惭愧。这其实也跟我们日常业务开发用到不多有关,但是正所谓脑子里没有,平常怎么能想到使用呢,因此我们还是有必要直面它的。废话不多说了,开始看一下它们到底是个啥。
Proxy是什么
MDN官方对于proxy的解释:
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
阮一峰ES6入门教程中对proxy的解释:
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
如果看完概念还不明白的话,那么我们举个场景例子:
有一个对象object,其中有一个age参数,我们希望age的范围始终要大于18。那么我们则需要在每次更新age的时候都去增加一个判断。先不说每次更新都这样判断是否麻烦,但凡有一个地方没加判断,age就有可能设置成小于18的数了。
因此我们就可以给object增加一个代理,让它在我们每次更新object中age的时候,对其进行一个判断,这样我们就只需要关注业务逻辑的更新,而不需要再去增加一些边界条件的判断,使得代码逻辑更清晰更灵活,相信这也是proxy设计的初衷。
知道了概念之后,我们还需要明白,它能对哪些操作进行拦截呢,又是怎么拦截的呢?
Proxy能对哪些操作拦截
我们日常接触较多的操作
1、对一个对象属性的修改。例如 object.age = 18 或 object['age'] = 18
2、获取一个对象的属性。例如object.age 或 object['age']
3、删除一个对象的属性。例如 delete object.age 或 delete object['age']
4、查看一个对象中是否存在某个属性。例如 'age' in object
5、获取对象自身所有属性。例如 Object.keys(obj) 或Object.getOwnPropertyNames(obj)
6、对一个函数进行调用。例如 fn()
7、new一个构造函数。例如 let obj = new Object()
还有一些日常接触不多的操作
8、给对象定义或修改某个属性的描述。如下所示:
let obj = {}
Object.defineProperty(obj, 'age', { value: 18, writable: true, enumerable: true, configurable: true
}); // 或者defineProperties。
Object.defineProperties(obj, { 'property1': { value: true, writable: true }, 'property2': { value: 'Hello', writable: false } // ... 可以定义更多属性
});
题外话:
defineProperty和defineProperties这二者区别就是前者一次只能对一个属性操作,后者一次则能操作多个属性。
一个属性的数据描述符有四个
value(属性值)、
writable(是否可写)、
enumerable(是否可枚举)、
configurable(是否可配置)。
value没写或者说未明确指定的话,为undefined。其余三个未明确指定的话默认为false
属性描述符除了上述数据描述符之外,还有存取描述符get(): 访问属性时会调用此函数、
set():修改该属性时会调用此函数、
enumerable(是否可枚举)、
configurable(是否可配置)
数据描述符和存取描述符是互斥的,不能同时在一个属性上设置两者,否则会抛出异常
9、得到某个对象中某个属性的具体描述。如下所示:
const object = { age: 18 };
Object.getOwnPropertyDescriptor(object, 'age');
// {value: 1, writable: true, enumerable: true, configurable: true}
10、设置一个对象不可扩展。如下所示:
let myObject = {};
Object.preventExtensions(myObject); // 将myObject设置过不可扩展
myObject.name = '张三'; // 无效
console.log(myObject) // {}
11、判断一个对象是否可扩展。如下所示:
const myObject = {};
Object.isExtensible(myObject) // true
12、设置一个目标对象的原型。如下所示:
const myObj = {};
Object.setPrototypeOf(myObj, Array.prototype);
13、获取一个对象的原型。如下所示:
let p = {};
Object.getPrototypeOf(p)
// 或者
p.__proto__;
// 或者
p instanceof Object;
以上所述的13种操作,都可以通过Proxy进行拦截。
Proxy怎么拦截呢
我们知道了proxy能够对哪些操作进行拦截后,然后再来看一下如何通过proxy对这些操作进行拦截。
ES6原生提供了一个Proxy构造函数,用来生成Proxy实例。
let proxy = new Proxy(target, handler);
所有拦截方式都是通过上面这种形式来操作的。Proxy构造函数接收两个参数,target表示要拦截的目标对象,handler参数也是一个对象,通过在handler中定义不同方法来进行不同拦截。
假设我们需要对一个获取属性的操作进行拦截。则可以在handle对象中定义一个get方法,get方法有三个参数,依次为目标对象、属性名、proxy实例本身(可选)。如下所示:
let obj = { age: 20 };
let handle = {get(target, propKey, receiver) {return 18;// return target[propKey];}
}
let proxyObj = new Proxy(obj, handle);
console.log(proxyObj.name); // 18
console.log(proxyObj.sex); // 18
console.log(proxyObj.age); // 18//----------------------------------------------------------------
// 我们可以将handle对象,直接写到Proxy第二个参数里,更简洁一些
let proxyObj = new Proxy(obj, {get(target, propKey, receiver) {return 18;// return target[propKey];}
});
上述代码,我们通过set方法拦截到了获取属性的操作,并且返回18,这时候我们不管获取什么属性,都会给我们返回18。注意,要想代理起作用,我们必须操作Proxy实例,也就是上述例子中的proxyObj,而不是针对目标对象。
如果我们需要返回实际属性值的话,只需要return target[propKey]即可。
其余操作也都有相对应的方法,同理,只需要在proxy的第二个参数里添加相应方法,即可对对象或函数的某种操作进行拦截,这里不再演示。
Proxy所有拦截操作的方法名
1、set(target, propKey, value, receiver):拦截目标对象的修改。方法有四个参数,依次为所拦截目标对象、待设置的属性名、待设置的属性值、一般为proxy实例。
2、get(target, propKey, receiver):拦截目标对象属性的读取。方法有三个参数,依次为所拦截目标对象、待获取的属性名、proxy实例。
3、deleteProperty(target, propKey):拦截目标对象属性的删除。方法有两个参数,依次为所拦截目标对象、待删除的属性名。
4、has(target, propKey):拦截检查目标对象中是否存在某个属性的操作。方法有两个参数,依次为拦截目标,待检查的属性名。
5、ownKeys(target):拦截获取目标对象所有属性的操作。方法有一个参数,就是拦截目标。
6、apply(target, object, args):拦截对目标函数的调用。方法有三个参数,依次为所拦截目标函数,目标的上下文对象(this)、被调用函数的参数数组。
7、construct(target, args, newTarget):拦截对目标构造函数的new操作。方法有三个参数,依次为目标构造函数、构造函数的参数数组、一般为proxy实例。
8、defineProperty(target, propKey, propDesc):拦截对目标对象属性描述的操作。方法有三个参数,依次为目标对象,目标对象的属性名、待定义或修改的属性的描述符。
9、getOwnPropertyDescriptor(target, propKey):拦截获取目标对象属性描述符的操作。方法有两个参数,依次为目标对象、目标对象属性名的描述。
10、preventExtensions(target):拦截设置对象不可扩展的操作。方法有一个参数,就是目标对象。
11、isExtensible(target):拦截判断对象是否可扩展的操作。方法就一个参数,就是目标对象。
12、setPrototypeOf(target, proto):拦截设置目标对象原型的操作。方法有两个参数,依次为目标对象,待设置的新原型或null。
13、getPrototypeOf(target):拦截获取目标对象原型的操作。方法有一个参数,就是目标对象。
Reflect是什么
Reflect是一个对象,其中目前有13个方法。包含Object对象中对对象操作的一些方法,以及将一些对象操作的命令变为函数方法(例如 'age' in obj 等同于 Reflect中的has(obj, 'age')方法、还有delete obj[age] 等同于 Relect中的deleteProperty(obj, 'age')方法)
之所以将Object中的一些方法和操作单独放到一个新的Reflect对象中,有以下几个目的:
1、一些对对象的操作明显是语言内部的方法,放在Object上可能会有点怪怪的,放在Reflect上会更清晰,无歧义。
2、优化某些操作,让方法更健壮,结果更合理。
3、将部分操作变为函数行为,例如上文所讲的in、或delete命令。
4、未来如果有对象操作的新方法,会只部署在Reflect对象上。
5、此外相信大家能够发现,Reflect上有13种方法,Rroxy也有13种方法。没错,它们是一一对应的,包括参数(参考上文proxy所有拦截操作的方法名)。因此用Reflect来作为Proxy拦截的默认操作简直太爽太配套。如下所示。
let obj = {};
var proxyObj = new Proxy(obj, {set: function (target, propKey, value, receiver) {console.log(`setting ${propKey}!`);// 当我们不希望影响该操作的结果时,我们只需要使用Reflect中相对于的方法进行默认操作即可return Reflect.set(target, propKey, value, receiver);},get(target, name) {console.log('get', target, name);// return target[name]; // badreturn Reflect.get(target, name); // good},deleteProperty(target, name) {console.log('delete' + name);return Reflect.deleteProperty(target, name);},has(target, name) {console.log('has' + name);return Reflect.has(target, name);}//...});
最后也建议大家在日常开发过程中用到以上13种操作之一的话,最好使用Reflect中的方法,更好一些。
参考链接:
ES6 入门教程
Proxy - JavaScript | MDN