Skip to content

你是怎么理解ES6中Proxy的?使用场景有哪些?

Posted on:2024年7月20日 at 09:39

一、介绍

定义: 用于定义基本操作的自定义行为

本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作

一段代码来理解

#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=1024; I++)) do
    echo "echo $I" >>program
done
chmod +x program

这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行echo,如果我们手动来写1024行代码,效率显然低效

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

二、用法

Proxy为 构造函数,用来生成 Proxy 实例

var proxy = new Proxy(target, handler);

参数

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))

handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

handler解析

关于handler拦截属性,有如下:

Reflect

若需要在Proxy内部调用对象的默认行为,建议使用Reflect,其是ES6中操作对象而提供的新 API

基本特点:

下面我们介绍proxy几种用法:

get()

get接受三个参数,依次为目标对象、属性名和 proxy 实例本身,最后一个参数可选

var person = {
  name: "张三",
};

var proxy = new Proxy(person, {
  get: function (target, propKey) {
    return Reflect.get(target, propKey);
  },
});

proxy.name; // "张三"

get能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引

function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    },
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray("a", "b", "c");
arr[-1]; // c

注意:如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则会报错

const target = Object.defineProperties(
  {},
  {
    foo: {
      value: 123,
      writable: false,
      configurable: false,
    },
  },
);

const handler = {
  get(target, propKey) {
    return "abc";
  },
};

const proxy = new Proxy(target, handler);

proxy.foo;
// TypeError: Invariant check failed

set()

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 an integer");
      }
      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方法将不起作用

const obj = {};
Object.defineProperty(obj, "foo", {
  value: "bar",
  writable: false,
});

const handler = {
  set: function (obj, prop, value, receiver) {
    obj[prop] = "baz";
  },
};

const proxy = new Proxy(obj, handler);
proxy.foo = "baz";
proxy.foo; // "bar"

注意,严格模式下,set代理如果没有返回true,就会报错

"use strict";
const handler = {
  set: function (obj, prop, value, receiver) {
    obj[prop] = receiver;
    // 无论有没有下面这一行,都会报错
    return false;
  },
};
const proxy = new Proxy({}, handler);
proxy.foo = "bar";
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'

deleteProperty()

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

var handler = {
  deleteProperty(target, key) {
    invariant(key, "delete");
    Reflect.deleteProperty(target, key);
    return true;
  },
};
function invariant(key, action) {
  if (key[0] === "_") {
    throw new Error(`无法删除私有属性`);
  }
}

var target = { _prop: "foo" };
var proxy = new Proxy(target, handler);
delete proxy._prop;
// Error: 无法删除私有属性

注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错

取消代理

Proxy.revocable(target, handler);

三、使用场景

Proxy其功能非常类似于设计模式中的代理模式,常用功能如下:

使用 Proxy 保障数据类型的准确性

let numericDataStore = { count: 0, amount: 1234, total: 14 };
numericDataStore = new Proxy(numericDataStore, {
  set(target, key, value, proxy) {
    if (typeof value !== "number") {
      throw Error("属性只能是number类型");
    }
    return Reflect.set(target, key, value, proxy);
  },
});

numericDataStore.count = "foo";
// Error: 属性只能是number类型

numericDataStore.count = 333;
// 赋值成功

声明了一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey

let api = {
  _apiKey: "123abc456def",
  getUsers: function () {},
  getUser: function (userId) {},
  setUser: function (userId, config) {},
};
const RESTRICTED = ["_apiKey"];
api = new Proxy(api, {
  get(target, key, proxy) {
    if (RESTRICTED.indexOf(key) > -1) {
      throw Error(`${key} 不可访问.`);
    }
    return Reflect.get(target, key, proxy);
  },
  set(target, key, value, proxy) {
    if (RESTRICTED.indexOf(key) > -1) {
      throw Error(`${key} 不可修改`);
    }
    return Reflect.get(target, key, value, proxy);
  },
});

console.log(api._apiKey);
api._apiKey = "987654321";
// 上述都抛出错误

还能通过使用Proxy实现观察者模式

观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行

observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数

const queuedObservers = new Set();

const observe = (fn) => queuedObservers.add(fn);
const observable = (obj) => new Proxy(obj, { set });

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach((observer) => observer());
  return result;
}

观察者函数都放进Set集合,当修改obj的值,在会set函数中拦截,自动执行Set所有的观察者

原文转自:https://fe.ecool.fun/topic/70db8e81-b858-4f3a-8bf9-d2bd1eb142e2