本章节复习的是JS中的元编程,涉及的更多的是ES6的新特性。

1. 概述

元编程,其实我是这么理解的:让代码自动写代码,可以更改源码底层的功能
元,是指程序本身。
有理解不到位,还请指点,具体详细的介绍,可以查看维基百科 元编程
从ES6开始,JavaScrip添加了对ProxyReflect对象的支持,允许我们连接并定义基本语言操作的自定义行为(如属性查找,赋值,枚举和函数调用等),从而实现JavaScrip的元级别编程。

  • Reflect: 用于替换直接调用Object的方法,并不是一个函数对象,也没有constructor方法,所以不能用new操作符。
  • Proxy: 用于自定义对象的行为,如修改setget方法,可以说是ES5中Object.defineProperty()方法的ES6升级版。
  • 两者联系: API完全一致,但Reflect一般在Proxy需要处理默认行为的时候使用。

参考资料

名称 地址
Reflect MDN Reflect
Proxy MDN Proxy
元编程 MDN 元编程

本文主要从Proxy介绍,还会有几个案例,实际看下怎么使用。

2. Proxy介绍

proxy 用于修改某些操作的默认行为,可以理解为一种拦截外界对目标对象访问的一种机制,从而对外界的访问进行过滤和修改,即代理某些操作,也称“代理器”。

2.1 基础使用

基本语法:

  1. let p = new Proxy(target, handler);

proxy实例化需要传入两个参数,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

  1. let p = new Proxy({}, {
  2. get: function (target, handler){
  3. return 'leo';
  4. }
  5. })
  6. p.name; // leo
  7. p.age; // leo
  8. p.abcd; // leo

上述a实例中,在第二个参数中定义了get方法,来拦截外界访问,并且get方法接收两个参数,分别是目标对象所要访问的属性,所以不管外部访问对象中任何属性都会执行get方法返回leo

注意

  • 只能使用Proxy实例的对象才能使用这些操作。
  • 如果handler没有设置拦截,则直接返回原对象。
  1. let target = {};
  2. let handler = {};
  3. let p = new Proxy(target, handler);
  4. p.a = 'leo';
  5. target.a; // 'leo'

同个拦截器函数,设置多个拦截操作

  1. let p = new Proxy(function(a, b){
  2. return a + b;
  3. },{
  4. get:function(){
  5. return 'get方法';
  6. },
  7. apply:function(){
  8. return 'apply方法';
  9. }
  10. })

这里还有一个简单的案例:

  1. let handler = {
  2. get : function (target, name){
  3. return name in target ? target[name] : 16;
  4. }
  5. }
  6. let p = new Proxy ({}, handler);
  7. p.a = 1;
  8. console.log(p.a , p.b);
  9. // 1 16

这里因为 p.a = 1 定义了p中的a属性,值为1,而没有定义b属性,所以p.a会得到1,而p.b会得到undefined从而使用name in target ? target[name] : 16;返回的默认值16

Proxy支持的13种拦截操作
13种拦截操作的详细介绍:打开阮一峰老师的链接

  • get(target, propKey, receiver)
    拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
  • set(target, propKey, value, receiver)
    拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
  • has(target, propKey)
    拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey)
    拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target)
    拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey)
    拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc)
    拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target)
    拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target)
    拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target)
    拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto)
    拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args)
    拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  • construct(target, args)
    拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。

2.2 取消Proxy实例

使用Proxy.revocable方法取消Proxy实例。

  1. let a = {};
  2. let b = {
  3. get: function(target, name) {
  4. return "[[" + name + "]]";
  5. }
  6. };
  7. let revoke = Proxy.revocable(a, b);
  8. let proxy = revoke.proxy;
  9. proxy.age; // "[[age]]"
  10. revoke.revoke();
  11. proxy.age; // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
  12. proxy.age = 10; // Uncaught TypeError: Cannot perform 'set' on a proxy that has been revoked
  13. delete proxy.age; // Uncaught TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked
  14. typeof proxy; // "object"

2.3 实现 Web服务的客户端

  1. const service = createWebService('http://le.com/data');
  2. service.employees().than(json =>{
  3. const employees = JSON.parse(json);
  4. })
  5. function createWebService(url){
  6. return new Proxy({}, {
  7. get(target, propKey, receiver{
  8. return () => httpGet(url+'/'+propKey);
  9. })
  10. })
  11. }

3. Proxy实践

3.1 数据拦截验证

通过Proxy代理对象的setget方法来进行拦截数据,像Vue就是用数据拦截来实现数据绑定。

  1. let handler = {
  2. // 拦截并处理get方法
  3. // 可以理解为设置get方法返回的默认值
  4. get : function (target, key){
  5. return key in target ? target[key] : 30;
  6. },
  7. // 拦截并处理set方法
  8. // 可以理解为设置set方法的默认行为
  9. set : function (target, key, value){
  10. if(key === "age"){
  11. if (!Number.isInteger(value)){
  12. throw new TypeError("age不是一个整数!");
  13. }
  14. if (value > 200){
  15. throw new TypeError("age不能大于200!");
  16. }
  17. }
  18. // 保存默认行为
  19. target[key] = value;
  20. }
  21. }
  22. let p = new Proxy({}, handler);
  23. p.a = 10; // p.a => 10
  24. p.b = undefined; // p.b => undefined
  25. p.c; // 默认值 30
  26. p.age = 100; // p.age => 100
  27. p.age = 300; // Uncaught TypeError: age不能大于200!
  28. p.age = "leo"; // Uncaught TypeError: age不是一个整数!

3.2 函数节流

通过拦截handler.apply()方法的调用,实现函数只能在1秒之后才能再次被调用,经常可以用在防止重复事件的触发。

  1. let p = (fun, time) => {
  2. // 获取最后点击时间
  3. let last = Date.now() - time;
  4. return new Proxy (fun, {
  5. apply(target, context, args){
  6. if(Date.now() - last >= time){
  7. fun.bind(target)(args);
  8. // 重复设置当前时间
  9. last = Date.now();
  10. }
  11. }
  12. })
  13. }
  14. let p1 = () => {
  15. console.log("点击触发");
  16. }
  17. let time = 1000; // 设置时间
  18. let proxyObj = p(p1, time);
  19. // 监听滚动事件
  20. document.addEventListener('scroll', proxyObj);

3.3 实现单例模式

通过拦截construct方法,让不同实例指向相同的constructer,实现单例模式。

  1. let p = function(fun){
  2. let instance;
  3. let handler = {
  4. // 拦截construct方法
  5. construct: function(targer, args){
  6. if(!instance){
  7. instance = new fun();
  8. }
  9. return instance;
  10. }
  11. }
  12. return new Proxy(fun, handler);
  13. }
  14. // 创建一个construct案例
  15. function Cons (){
  16. this.value = 0;
  17. }
  18. // 创建实例
  19. let p1 = new Cons();
  20. let p2 = new Cons();
  21. // 操作
  22. p1.value = 100;
  23. // p1.value => 100 , p2.value => 0
  24. // 因为不是相同实例
  25. // 通过Proxy实现单例
  26. let singleton = p(Cons);
  27. let p3 = new singleton();
  28. let p4 = new singleton();
  29. p3.value = 130;
  30. // p1.value => 130 , p2.value => 130
  31. // 现在是相同实例

参考资料

1.MDN 元编程
2. ES6中的元编程-Proxy & Reflect
本部分内容到这结束

Author 王平安
E-mail pingan8787@qq.com
博 客 www.pingan8787.com
微 信 pingan8787
每日文章推荐 https://github.com/pingan8787/Leo_Reading/issues
ES小册 js.pingan8787.com