ES6之Proxy

1. Proxy

1.1. 概述

ES6(ECMAScript 2015)引入了Proxy对象,属于一种“元编程”(meta programming),即对编程语言进行编程。

这是一种可以用来定义基本操作(如获取或设置属性)的自定义行为的对象。

Proxy 可以让你在访问一个对象之前对其进行拦截,从而实现对对象操作的控制,比如验证、计算、日志记录等。

这对于开发框架、库或者进行某些高级的JavaScript编程非常有用。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问
进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

var obj = new Proxy({}, {
    get: function (target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
        console.log(`setting ${key}!`);
        return Reflect.set(target, key, value, receiver);
    }
});

上面代码对一个空对象架设了一层拦截,重定义了属性的读取( get )和设置( set )行为。

1.2. Proxy 构造函数

  • 目标对象(target):你想要对其操作进行拦截的对象。
  • 处理器对象(handler):定义了一系列拦截行为的对象,比如如何处理属性的获取(get)、设置(set)、枚举(enumerate)等。

创建一个Proxy实例的基本语法如下:

const proxy = new Proxy(target, handler);
  • target:被代理的目标对象。
  • handler:一个对象,其属性是被拦截操作的类型(如get, set等),属性值是对应的处理函数。

Proxy 对象的所有用法,都是上面这种形式,不同的只是 handler 参数的写法。

其中, new Proxy() 表示生成一个 Proxy 实例, target 参数表示所要拦截的目标对象, handler 参数也是一个对象,用来定制拦截行为。

下面是另一个拦截读取属性行为的例子。

var proxy = new Proxy({}, {
    get: function(target, property) {
        return 35;
    }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

上面代码中,作为构造函数, Proxy 接受两个参数。

  • 第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有 Proxy 的介入,操作原来要访问的就是这个对象;
  • 第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。

比如,上面代码中,配置对象有一个 get 方法,用来拦截对目标对象属性的访问请求。

get 方法的两个参数分别是目标对象和所要访问的属性。

可以看到,由于拦截函数总是返回 35 ,所以访问任何属性都得到 35 。

注意,要使得 Proxy 起作用,必须针对 Proxy 实例(上例是 proxy 对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

如果 handler 没有设置任何拦截,那就等同于直接通向原对象。

var target = {};
var handler = {};

var proxy = new Proxy(target, handler);

proxy.a = 'b';
target.a // "b"

上面代码中, handler 是一个空对象,没有任何拦截效果,访问 proxy 就等同于访问 target 。

1.3. 处理器方法(Handler Methods)

以下是几个常用的处理器方法:

1.3.1. get(target, propKey, receiver):

拦截对象属性的读取,比如 proxy.foo 和 proxy[‘foo’] 。

get 方法用于拦截某个属性的读取操作,可以接受三个参数,依次为:

  • 目标对象、
  • 属性名
  • proxy 实例本身(即 this 关键字指向的那个对象)

其中最后一个参数可选。

let proto = new Proxy({}, {
    get(target, propertyKey, receiver) {
        console.log('GET ' + propertyKey);
        return target[propertyKey];
    }
});

let obj = Object.create(proto);
obj.foo // "GET foo"

上面代码中,拦截操作定义在 Prototype 对象上面,所以如果读取 obj 对象继承的属性时,拦截会生效。

1.3.2. set(target, propKey, value, receiver):

拦截对象属性的设置,比如 proxy.foo = v 或 proxy[‘foo’] = v ,返回一个布尔值。

set 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为:

  • 目标对象、
  • 属性名、
  • 属性值
  • Proxy 实例本身

其中最后一个参数可选。

假定 Person 对象有一个 age 属性,该属性应该是一个不大于200的整数,那么可以使用 Proxy 保证 age 的属性值符合要求。

let validator = {
    set: function(obj, prop, value) {
        if (prop === 'age') {
            if (!Number.isInteger(value)) {
                throw new TypeError('The age is not aninteger');
            }
            if (value > 200) {
                throw new RangeError('The age seems invalid');
            }
        }
        // 对于age以外的属性,直接保存
        obj[prop] = value;
    }
};

let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

上面代码中,由于设置了存值函数 set ,任何不符合要求的 age 属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用 set 方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。

1.3.3. has(target, propKey):

拦截 propKey in proxy 的操作,返回一个布尔值。

has 方法用来拦截 HasProperty 操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是 in 运算符。

下面的例子使用 has 方法隐藏某些属性,不被 in 运算符发现。

var handler = {
    has (target, key) {
        if (key[0] === '_') {
            return false;
        }
        return key in target;
    }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);

'_prop' in proxy // false

上面代码中,如果原对象的属性名的第一个字符是下划线, proxy.has 就会返回 false ,从而不会被 in 运算符发现

1.3.4. deleteProperty(target, propKey):

拦截 delete proxy[propKey] 的操作,返回一个布尔值。

deleteProperty 方法用于拦截 delete 操作,如果这个方法抛出错误或者返回 false ,当前属性就无法被 delete 命令删除。

var handler = {
    deleteProperty (target, key) {
        invariant(key, 'delete');
        return true;
    }
};

function invariant (key, action) {
    if (key[0] === '_') {
        throw new Error(`Invalid attempt to ${action} private "${key}" property`);
    }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
1.3.5. ownKeys(target):

拦截 Object.getOwnPropertyNames(proxy) 、 Object.getOwnPropertySymbols(proxy) 、Object.keys(proxy) ,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。

1.3.6. getOwnPropertyDescriptor(target, propKey):

拦截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回属性的描述对象。

1.3.7. defineProperty(target, propKey, propDesc):

拦截 Object.defineProperty(proxy, propKey,propDesc) 、Object.defineProperties(proxy,propDescs) ,返回一个布尔值。

1.3.8. preventExtensions(target):

拦截 Object.preventExtensions(proxy) ,返回一个布尔值。

1.3.9. getPrototypeOf(target):

拦截 Object.getPrototypeOf(proxy) ,返回一个对象。

1.3.10. isExtensible(target):

拦截 Object.isExtensible(proxy) ,返回一个布尔值。

1.3.11. setPrototypeOf(target, proto):

拦截 Object.setPrototypeOf(proxy, proto) ,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

1.3.12. apply(target, object, args):

拦截 Proxy 实例作为函数调用的操作,比如 proxy(…args) 、 proxy.call(object,…args) 、 proxy.apply(…) 。

1.3.13. construct(target, args):

拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(…args) 。

更多详细内容,请微信搜索“前端爱好者戳我 查看

2. 应用场景

  • 数据绑定和响应式编程:例如Vue 3的Reactivity系统就利用Proxy来监测数据变化,实现自动更新视图。
  • 权限控制和验证:在访问特定属性或方法前进行权限检查。
  • 日志记录和调试:记录对象的访问和修改历史。
  • 虚拟化对象结构:为现有对象提供一个虚拟的、可能具有不同行为的视图。

Proxy是一个强大的工具,但需要注意的是,它可能会使代码变得难以理解和维护,因此应当谨慎使用。

ES6的Proxy提供了强大的元编程能力,能够让我们在访问或修改对象时插入自定义逻辑。以下是一些典型的使用场景及示例:

2.1. 数据校验

场景说明:在复杂应用中,尤其是在表单处理和状态管理时,经常需要对输入数据进行校验。使用Proxy可以在数据赋值时进行验证。

示例

const validator = {
  set(target, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new Error('Age must be an integer');
      }
      if (value < 0 || value > 120) {
        throw new Error('Age must be between 0 and 120');
      }
    }
    target[prop] = value;
    return true; // 表示设置成功
  }
};

const person = new Proxy({}, validator);
person.age = 25; // 正常赋值
console.log(person.age); // 输出: 25

try {
  person.age = 'young'; // 尝试赋予非法值
} catch (e) {
  console.error(e.message); // 输出错误信息
}

2.2. 缓存

场景说明:对于一些计算密集型或频繁访问的数据,可以通过Proxy实现只计算一次并缓存结果的机制。

示例

const cacheHandler = {
  get(target, prop, receiver) {
    const value = Reflect.get(...arguments);
    if (typeof value === 'function') {
      return (...args) => {
        if (!cacheHandler.cache.has(prop)) {
          cacheHandler.cache.set(prop, value(...args));
        }
        return cacheHandler.cache.get(prop);
      };
    }
    return value;
  },
  cache: new Map()
};

function expensiveCalculation(a, b) {
  console.log('Calculating...');
  return a * a + b * b;
}

const cachedCalculation = new Proxy(expensiveCalculation, cacheHandler);

console.log(cachedCalculation(2, 3)); // 输出: Calculating... 然后输出: 13
console.log(cachedCalculation(2, 3)); // 输出: 13,不再重新计算

2.3. 虚拟化DOM

场景说明:虽然这不是Proxy最直接的应用,但在某些库或框架中,它被用来虚拟化DOM操作,使得框架能更高效地管理UI状态。

示例概念

想象一个简化版的场景,我们不直接操作DOM,而是通过Proxy拦截对DOM元素属性的访问和修改,以便于跟踪变化并优化渲染。

// 简化的虚拟DOM概念展示,实际应用中会更复杂
const domProxy = (element) => {
  return new Proxy(element, {
    set(target, prop, value) {
      Reflect.set(target, prop, value);
      // 这里可以添加逻辑来决定是否重新渲染或更新DOM
      console.log(`Updating ${prop} to ${value}`);
      return true;
    }
  });
};

const div = document.createElement('div');
const proxiedDiv = domProxy(div);
proxiedDiv.textContent = 'Hello, World!'; // 触发更新逻辑

2.4. 远程对象代理

场景说明:在分布式系统中,可以使用Proxy来模拟远程对象的行为,使得客户端可以像操作本地对象一样操作远程服务。

示例概念

class RemoteObjectProxy {
  constructor(url) {
    this.url = url;
  }

  get(target, prop) {
    return fetch(`${this.url}/${prop}`).then(res => res.json());
  }

  // ...其他必要的代理逻辑,如set等
}

const proxy = new RemoteObjectProxy('http://api.example.com/data');
proxy.someProperty.then(data => console.log(data)); // 异步获取数据

这些示例展示了Proxy在不同场景下的灵活性和强大功能,从数据验证到性能优化,再到复杂的架构设计,Proxy都是一个值得掌握的现代JavaScript特性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端布道人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值