JavaScript的Proxy代理怎么用?

373 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

Proxy的基本介绍

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

用法:new Proxy(target, handler)

  • target:表示所要拦截的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:也是一个对象,用来定制拦截行为。(监听目标对象行为的监听器

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

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b":handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target

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


var handler = {
  get: function(target, name) {//拦截对象属性的读取
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },
  apply: function(target, thisBinding, args) {//拦截Proxy 实例作为函数调用的操作
    return args[0];
  },
  construct: function(target, args) {//拦截 Proxy 实例作为构造函数调用的操作
    return {value: args[1]};
  }
};
var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

打印结果:

image-20220105154737091

实际应用示例

get

属性读取操作的捕捉器。

get(target, property, receiver)

  • target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy,
  • property —— 目标属性名,
  • receiver —— 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。通常,这就是 proxy 对象本身(或者,如果我们从 proxy 继承,则是从该 proxy 继承的对象)。

实例一

当尝试获取不存在的数组项时,会得到 undefined,可以将常规数组包装到代理(proxy)中,以捕获读取操作,并在没有要读取的属性的时返回 0

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // 默认值
    }
  }
});

console.log( numbers[1] ); // 1
console.log( numbers[123] ); // 0(没有这个数组项)

实例二

假如有一组词典,上面有短语以及翻译:

let dictionary = {
  'Hello': '你好',
  'Bye': '再见'
};

console.log( dictionary['Hello'] ); // Hola
console.log( dictionary['Welcome'] ); // undefined

如果没有我们要读取的短语,那么从 dictionary 读取它将返回 undefined。但实际上,返回一个未翻译的短语通常比 undefined 要好。所以在这种情况下可以返回一个未翻译的短语来替代 undefined

为此,可以把 dictionary 包装进一个拦截读取操作的代理:

dictionary = new Proxy(dictionary, {
  get(target, phrase) { // 拦截读取属性操作
    if (phrase in target) { //如果词典中有该短语
      return target[phrase]; // 返回其翻译
    } else {
      // 否则返回未翻译的短语
      return phrase;
    }
  }
});
// 在词典中查找任意短语!
// 最坏的情况也只是它们没有被翻译。
console.log( dictionary['Hello'] ); // 你好
console.log( dictionary['Welcome']); // Welcome(没有被翻译)

实例三

实现访问 array[-1],即从尾端算起的负值索引访问数组元素。换句话说,array[-N]array[array.length - N] 相同。

let array = [1, 2, 3];
array[-1] = array[2]; // 3,最后一个元素

创建一个 proxy 来实现该行为。

let array = [1, 2, 3];
array = new Proxy(array, {
  get(target, prop, receiver) {
    if (prop < 0) {
      // 即使我们像 arr[1] 这样访问它
      // prop 是一个字符串,所以我们需要将其转换成数字
      prop = +prop + target.length;
    }
    return Reflect.get(target, prop, receiver);
  }
});

console.log(array[-1]); // 3
console.log(array[-2]); // 2

set

属性设置操作的捕捉器。

set(target, property, value, receiver)

  • target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy,
  • property —— 目标属性名称,
  • value —— 目标属性的值
  • receiver —— 与 get 捕捉器类似,仅与 setter 访问器属性相关。

实例一

可用于做数据验证,如Person对象有一个age属性,该属性应该是一个不大于 200 的整数。任何不符合要求的age属性赋值,都会抛出一个错误,可以这样写

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }
 
    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
    return true;
  }
};
 
let person = new Proxy({}, validator);

打印结果:

image-20220105155342657

has

in 操作符的捕捉器。

实例一

可以使用has捕捉器实现,利用in操作符来检查一个数字是否在 range 范围内。(因为has 捕捉器会拦截 in 调用。)

has(target, property)

  • target —— 是目标对象,被作为第一个参数传递给 new Proxy,
  • property —— 属性名称。
let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end;
  }
});

console.log(5 in range); // true
console.log(50 in range); // false

apply

函数调用操作的捕捉器。

实例一

apply(target, thisArg, args) 捕捉器能使代理以函数的方式被调用:

  • target 是目标对象(在 JavaScript 中,函数就是一个对象),
  • thisArg 是 this 的值。
  • args 是参数列表。

使用 Proxy 来替换包装函数

function delay(f, ms) {
  return new Proxy(f, {
    apply(target, thisArg, args) {
      setTimeout(() => target.apply(thisArg, args), ms);
    }
  });
}
function sayHi(user) {
  console.log(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
console.log(sayHi.length); // 1 (*) proxy 将“获取 length”的操作转发给目标对象
sayHi("John"); // Hello, John!(3 秒后)

更多属性参考表

表格一:

属性值监听器参数监听内容
get(target, prop, reciver)监听目标对象的属性读取
set(target, prop, value, reciver)监听目标对象的属性赋值
has(target, prop)监听 in 语句的使用
apply(target, thisArg, arguments)监听目标函数(作为目标对象)的调用行为
deleteProperty(target, prop)监听 delete 语句对目标对象的删除属性行为
construct(target, arguments, newTarget)监听目标构造函数(作为目标对象)利用 new 而生成实例的行为

表格二:

属性值监听器参数监听内容
isExtensible(target)监听 Objext.isExtensible() 的读取
preventExtensions(target)监听 Objext.preventExtensions() 的读取
getOwnPropertyDescriptor(target, prop)监听 Objext.getOwnPropertyDescriptor() 的调用
defineProperty(target, property, descriptor)监听 Object.defineProperty() 的调用
ownKeys(target)监听 Object.getOwnPropertyName() 的读取
getPrototypeOf(target)监听 Objext.getPrototypeOf() 的读取
setPrototypeOf(target, prototype)监听 Objext.setPrototypeOf() 的调用

参考资料:Proxy and Reflect


🎨【点赞】【关注】不迷路,更多前端干货等你解锁

往期推荐

👉 一起来看看JS的原型继承

👉 JS中的getter和setter你会用吗?

👉 深入理解ES6箭头对象

👉 JS的装饰器模式实例分析