likes
comments
collection
share

proxy代理及其7种应用场景!!这下弄懂了

作者站长头像
站长
· 阅读数 8

什么是 proxy?

proxy 的基本定义

Proxy 是 JavaScript 中的元编程特性,它允许开发者拦截并自定义对象的基本操作,从而实现对语言层面的编程。你可以将 Proxy 看作是一种"代理器",用来代理对象的访问,以便可以拦截和改变这些访问。通过创建代理对象,你可以在对象上设置拦截器,以捕获对对象属性、方法等的操作,然后执行自定义的逻辑。

proxy 的参数解析

proxy 这个词本身就是代理的意思,在这里可以理解它为一个代理器

new Proxy(target, handle)

  • target 参数表示所要拦截的目标对象,
  • handler 参数也是一个配置对象,用来定制拦截行为。如果 handler 没有设置任何拦截,那就等同于直接通向原对象。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy的常见拦截操作

1. get方法

get(target, propKey, receiver):实现拦截操作,用于拦截某个属性的读取操作。

可以接受三个参数,依次为

  • target 参数表示目标对象。
  • propKey 参数表示属性名。
  • receiver 参数表示 proxy 实例本身(严格地说,是操作行为所针对的对象,也就是所谓的接收器),其中最后一个参数可选。

2. set方法

get(target, property, value, receiver):用来拦截某个属性的赋值操作。一般用于对需要赋值的数进行过滤,加工,设置权限等等。

可以接受四个参数,依次为:

  • target 参数表示目标对象。
  • property 参数表示要设置的属性名称。
  • value参数表示要赋予属性的值。
  • receiver参数表示最初被调用的对象,通常是代理对象。

3. has方法

has() 方法用来拦截 HasProperty 操作,即判断对象是否具有某个属性时,这个方法会生效典型操作就是 in运算符 ,一般用于隐藏某些属性,不被 in 运算符发现。

const handler = {
  has(target, key) {
    if (key[0] === "_") {
      return false;
    }
    return key in target;
  },
};

let target = { _prop: "foo", prop: "foo" };
let proxy = new Proxy(target, handler);
if ('_prop' in proxy) {
  console.log(111);
} else {
  console.log(222);
} // 222

4. deleteProperty方法

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

const handler = {
  deleteProperty(target, key) {
    invariant(key, "delete");
    delete target[key];
    return true;
  },
};

const invariant = (key, action) => {
  if (key[0] === "_") {
    throw new Error(`can't ${action} to private '${key}' property`);
  }
};

let target = { _prop: "hah", name: "zs" };
let proxy = new Proxy(target, handler);
delete proxy._prop; // can't delete to private '_prop' property

5. apply方法

apply(target, object, args):用于拦截Proxy实例作为函数调用操作的操作符,例如 proxy(...args)、proxy.call(object, ...args) 和 proxy.apply(...)。这个拦截器在代理函数被调用时执行。

可以接受三个参数,依次为:

  • target :被代理的目标函数。
  • object:被调用时绑定的 this 值。
  • arg:一个类数组对象,包含函数调用时传入的实参列表。
let target = function () {
  console.log("目标函数被调用");
  return "res"
};

target = new Proxy(target, {
  apply: function (target, thisArg, argumentsList) {
    console.log("参数列表:", argumentsList);
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log("调用结果:", result);
    return result;
  },
});

target(1, 2, 3);

6. ownKeys方法

ownKeys(target)拦截自身属性的读取操作(例如object.getownPropertyNames(proxy)、 object.getownPropertysymbols(proxy)、object.keys(proxy)、for...in 循环等方法),并返回目标对象所有自身属性的属性名数组。

let target = {
  _foo: 'foo',
  _bar: 'bar',
  name: 'saucxs'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key.charAt(0) != '_');
  }
};

 target = new Proxy(target, handler);
for (let key of Object.keys(target)) {
  console.log(target[key]);
}
// "saucxs"

proxy 的 7 种应用场景

1. 实现私有变量

可以通过拦截属性操作实现私有变量,即使用 getset 拦截器来拦截属性的读取和赋值操作实现私有化。

let userList = {
  _prop: {},
  _userListId: "123abc",
  getUsers: function () {},
  getUser: function (userId) {},
  setUser: function (userId, config) {},
};

const RESTRICTED = ["_prop", "_userListId"];
userList = new Proxy(userList, {
  get(target, key, proxy) {
    if (RESTRICTED.indexOf(key) > -1) {
      throw Error(
        `${key} is restricted. Can't get to private '${key}' property.`
      );
    }
    return Reflect.get(target, key, proxy);
  },
  set(target, key, value, proxy) {
    if (RESTRICTED.indexOf(key) > -1) {
      throw Error(
        `${key} is restricted. Can't set to private '${key}' property.`
      );
    }
    return Reflect.get(target, key, value, proxy);
  },
});

// 以下操作都会抛出错误
console.log(userList._userListId);
userList._userListId = "123abcad";

在上面面的代码中,我们声明了两个私有变量:_prop, _userListId,便于 userList 这个对象内部的方法调用,但不希望从外部也能够访问以及修改userList._prop, userList._userListId,所以利用 proxy 拦截属性的操作实现变量的私有化。

2. 性能监测

对于那些调用频繁、运行缓慢或占用执行环境资源较多的属性或接口,如果开发者想要记录它们的使用情况或性能表现,此时可以借助proxy代理使用 apply 处理程序来拦截函数的调用实现性能检测。

// 创建一个普通的函数
function myFunction() {
  // 模拟一个耗时的操作
  for (let i = 0; i < 1000000000; i++) {
    // do something
  }
  console.log("函数调用完成");
}

// 创建一个代理对象来拦截对函数的调用
const proxyFunction = new Proxy(myFunction, {
  apply: function (target, thisArg, argumentsList) {
    // 记录函数调用开始时间
    const startTime = performance.now();
    // 调用原始函数
    const result = Reflect.apply(target, thisArg, argumentsList);
    // 计算执行时间
    const executionTime = performance.now() - startTime;
    // 打印执行时间
    console.log(`函数执行时间:${executionTime} 毫秒`);
    // 返回函数调用的结果
    return result;
  },
});
// 通过代理调用函数并检测性能
proxyFunction();

3. 链式调用

拦截对象的属性访问操作,并在调用属性方法后返回代理对象,以便可以继续在代理对象上进行属性访问或方法调用。

const Chainable = function(obj) {
  return new Proxy(obj, {
    get(target, prop) {
      if (prop in target) {
        if (typeof target[prop] === 'function') {
          return (...args) => {
            target[prop](...args);
            return Chainable(target);
          };
        }
        return Chainable(target[prop]);
      }
      throw new Error(`Property ${prop} not found`);
    }
  });
};

const api = {
  add(x) {
    this.value += x;
  },
  subtract(x) {
    this.value -= x;
  },
  getValue() {
    console.log(this.value);
  }
};

const chain = Chainable(api);

chain.add(5).subtract(3).getValue(); // Output: 2

在上面的示例中,我们定义了一个 Chainable 函数,它接受一个对象作为参数,并返回一个代理对象。代理对象的 get 操作用于拦截属性的读取,所以利用 get 操作:如果属性是函数,它会返回一个函数,该函数在调用后返回代理对象,从而实现链式调用的效果。

4. 只读模式

使用 set 拦截器,防止对象的属性被修改即可实现只读模式

let target = { readOnlyProp: 10 };
target = new Proxy(target, {
 set(target, prop, value) {
   throw new Error('Property is read-only');
 }
});

target.readOnlyProp = 20; // Error: Property is read-only

5. 数据校验

在属性赋值前使用 set 拦截器进行数据校验,确保只有符合规定的数据被赋值。

let userInfo = {
  id: 0,
  username: "abc",
  password: 14,
};

userInfo = new Proxy(userInfo, {
  set(target, key, value, proxy) {
    if (key === "username" && typeof value !== "string") {
      throw Error(`${key} in userInfo can only be string`);
    }
    return Reflect.set(target, key, value, proxy);
  },
});

// 抛出错误:username in userInfo can only be string
userInfo.username = 123;

// 赋值成功
userInfo.username = 333;

6. 数据双向绑定

调用 get 和 set 方法来截获对目标对象属性的读取和写入操作。通过在这些方法中添加自定义逻辑,可以实现数据的双向绑定。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <button>Click Me</button>
    <script>
        const btn = document.querySelector('button');

        let obj = { 'value': 233 };
        // 监听数据的变化同时更新视图
        const handler = {
            get: function (target, property, receiver) {
                return target[property];
            },
            set: function (target, property, value, receiver) {
                target[property] = value;
                btn.innerText = `value is ${target[property]}.`; // 值在改变的同时更新视图
                return true;
            }
            // 注意target属性操作使用[]
        };
         obj = new Proxy(obj, handler);

        btn.onclick = () => {
            obj.value = obj.value + 1; // 在真正操作时只要关系数据就行
        };
    </script>
</body>
</html>

7. 缓存机制

通过使用 Proxy 代理对象,我们能够在函数调用的各个阶段进行拦截和自定义操作,从而实现了缓存机制。利用代理函数的 apply 方法拦截了原始函数的调用,通过在这个方法中添加缓存逻辑,可以在每次调用函数前检查缓存并返回缓存数据,避免重复计算从而提高性能。

const cacheFun = (n) => {
  const cacheData = new Map();
  return (n) => {
    if (cacheData.has(n)) {
      console.log("data from cache");
      return cacheData.get(n);
    }
    const result = n * n;
    cacheData.set(n, result);
    return result;
  };
};

const proxyFunction = new Proxy(cacheFun(), {
  apply(target, thisArg, args) {
    console.log("Using cache...");
    return target(...args);
  },
});

console.log(proxyFunction(5)); // Using cache... 25
console.log(proxyFunction(5)); // Using cache... data from cache... 25 

~0v0~ 以上是proxy代理有关内容如有错,还请指正!