重学ES6——代理和反射

413 阅读5分钟

前言

首先在展开代理(Proxy)和反射(Reflect)两个 API 之前,先引入一个概念 ———— 元编程(Metaprogramming)。

元编程是一种编程技术,其中计算机程序能够将其他程序视为其数据。这意味着一个程序可以被设计为读取、生成、分析或转换其他程序,甚至在运行时修改自身。 ———— 【维基百科 Metaprogramming

简而概之,就是说对于编程的再编程。一个机器 A(即程序)生产产品,一个机器 B(即程序)能够生产或者优化机器 A,那么这个机器 B 就是元编程。

元编程分类

  • 对程序进行结构访问(只读权限),如 Object.*等 API(Object.keys)访问
  • 对程序进行结构更改,如通过属性访问后,使用赋值运算符或 delete API 等修改
  • 重新定义默认的语言行为,如本文要展开的 Proxy API

JS 中无处不在的元编程

因此,Proxy 就是为开发者提供了一种程序拦截并拓展额外能力的操作,即修改了原先的语义。
而 Reflect 则是为开发者在处理程序时提供更好的原始行为调用方法,无需开发者手动重建。

proxy.png

对象的访问和控制

先来看看 Proxy 还未面世之前,JS 都有哪些对象控制方式

getter & setter

这两个方法是用于对象属性的访问和控制

  • 通过对象字面量定义
const person = {
  name: 'dilomen',
  get getName() {
    console.log('getter');
    return this.name;
  },
  set setName(value) {
    console.log('setter');
    this.name = value;
  },
};
  • 通过 ES6 的 class 定义
class Person {
  constructor() {
    this.name = 'dilomen';
  }
  get getName() {
    console.log('getter');
    return this.name;
  }
  set setName(value) {
    console.log('setter');
    this.name = value;
  }
}

const person = new Person();
person.name = 'Alice';
console.log(person.getName);
  • 通过 Object.defineProperty 定义

通过 Object.defineProperty 创建的 get/set 方法,和当前函数的私有变量出于同一个作用域,所以 get/set 方法分别创建了含有私有变量的闭包,因此可以访问,但是也要注意内存泄露问题。

function Person() {
  let _name = 'dilomen';

  Object.defineProperty(this, 'name', {
    get() {
      console.log(' defineProperty getter');
      return _name;
    },

    set(value) {
      console.log('defineProperty setter');
      _name = value;
    },
  });
}

const person = new Person();
person.name = 'Alice';
console.log(person);

Proxy

主角登场~

如果说 getter/setter 是对单个属性进行控制,那么 Proxy 就是对整个对象的所有交互的控制!包括已有的属性,和未来新增的属性等等。

/**
 * target 要控制的对象
 * handler 捕获器对象
 */
const proxy = new Proxy(target, handler);

proxy_handler.png

图片来源 javascript.info/proxy

handler 具体的对象属性如上,具体使用可以查阅 MDN,这边就不在展开了

developer.mozilla.org/zh-CN/docs/…

可撤销代理

当我们希望在处理某个逻辑后,需要对对象的代理,那么就必须使用以下方式构建 proxy

const person = {
  name: 'dilomen',
};
const handler = {
  get() {
    return 'Alice';
  },
};
// 使用Proxy.revocable可以构建一个可撤销代理
const { proxy, revoke } = Proxy.revocable(person, handler);
// 通过暴露出的revoke方法可以取消掉代理,并且该操作不可逆,即取消后,代理就永远不存在了
revoke();
console.log(proxy.name); // TypeError

原型链

原型链是 JS 实现继承的一种方式,这边主要是了解这个知识就可 ———— 当访问一个对象的自身属性不存在时,那么就会一层层往原型链上寻找

  • 那么如果 proxy 代理是原型链中的一个节点会如何呢?
let target = {};
let thing = Object.create(
  new Proxy(target, {
    get(trapTarget, key, receiver) {
      throw new Error('属性不存在');
    },
  })
);

thing.name = 'thing';

console.log(thing.name); // "thing"

thing.data; // Error: 属性不存在

当代理被作为原型时,只有当默认操作执行到原型上时才会调用代理捕获器。可以从上面的代码看出,当获取 name 属性时,是 thing 自有属性,所以没有走到原型上,而 data 则原型链查找触发到了代理。

当代理被作为原型时,trapTarget 是原型对象,receiver 是实例对象。按照上面的代码,trapTarget 就是 target,receiver 就是 thing,所以可以访问代理的原始目标和要操作的对象。

  • 类(函数)继承原型代理,其原型不是一个代理
function NoSuchProperty() {}

NoSuchProperty.prototype = new Proxy(
  {},
  {
    get(trapTarget, key, receiver) {
      throw new Error('属性不存在');
    },
  }
);

class Person extends NoSuchProperty {
  constructor(name) {
    super();
    this.name = name;
  }
}

let person = new Person('dilomen');
const personProto = Object.getPrototypeOf(person);
console.log(personProto === NoSuchProperty.prototype); // Person原型不是一个代理,但是继承了NoSuchProperty的代理
console.log(Object.getPrototypeOf(personProto) === NoSuchProperty.prototype);

Reflect

Reflect 反射 API 是一个提供了各种拦截 JS 操作静态方法的内置对象

  • 一些和对象 API 对应的方法

    • Object API 更多的是通用程序的应用,而 Reflect API 更多用于细粒度的对象控制和操作
  • 状态标记:返回结果为操作是否执行成功。主要是以下方法:

    • Reflect.defineProperty()

    • Reflect.preventExtensions()

    • Reflect.setPrototypeOf()

    • Reflect.set()

    • Reflect.deleteProperty()

      const o = {};
      try {
          Object.defineProperty(o, 'foo', 'bar');
          console.log('success');
      } catch(e) {
          console.log('failure');
      }
      // 返回的是布尔值
      if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) {
         console.log('success');
      } else {
         console.log('failure');
      }
      
  • 替换操作符

    方法操作符
    Reflect.get()对象属性访问操作符
    Reflect.set()=赋值操作符
    Reflect.has()in 操作符或 with()
    Reflect.deleteProperty()delete 操作符
    Reflect.construct()new 操作符

Proxy + Reflect

Reflect 的静态对象和 handler 是完全对应的,所以可以直接使用以下方式,等于创建了一个所有捕获器都转发到反射 API 的空代理

new Proxy(target, Reflect);

也可以对单个或几个捕获器进行定义

new Proxy(target, {
  get(target, propertyKey) {
    report(propertyKey + '属性被调用了');
    return Reflect.get(target, propertyKey);
  },
});

使用场景

  • 不确定是哪个或哪些属性
  • 涉及的属性比较多
  • 存在新增当前不存在的属性

添加日志,跟踪属性

new Proxy(target, {
  get: (target, propertyKey) {
    report(propertyKey + '属性被访问了');
    return Reflect.get(target, propertyKey);
  },
});

多个属性针对性处理

const properties = ['xxx', 'yyy', 'zzz'];
new Proxy(target, {
  set: (target, propertyKey, value) {
    if (properties.includes(propertyKey)) {
      // do somethings
    }
    return Reflect.set(target, propertyKey, value);
  },
});

拓展语言能力

如让 js 支持负数组索引

new Proxy(target, {
  get: (target, index) {
    return target[index < 0 ? target.length + index : index]
  },
  set: (target, index, value) {
    // 同
  }
})

数据绑定&可观察对象

参考 Vue3 的实现

性能

虽然 Proxy 的功能很强大,但是性能也是一个不容忽视的问题

从下面的程序可看出,当对 proxy 有大量控制操作时,将会出现严重的性能问题

const obj = {
  num: 1,
};

function execute(obj) {
  for (let i = 0; i < 1000000; i++) {
    obj.num = i;
  }
}

const proxy = new Proxy(obj, {});

console.time('execute');
execute(obj); // execute: 2.2158203125 ms
console.timeEnd('execute');

console.time('proxy execute');
execute(proxy); // proxy execute: 323.283935546875 ms
console.timeEnd('proxy execute');

参考资料