JS带你深入领略Proxy的世界

1. Proxy 的基本结构

Proxy 的基本使用方式:

/**
 * target: 表示要代理的目标,可以是object, array, function类型
 * handler: 是一个对象,可以编写各种代理的方法
 */
const proxy = new Proxy(target, handler);

例如我们想要代理一个对象,可以通过设置 get 和 set 方法来代理获取和设置数据的操作:

const person = {
  name: 'wenzi',
  age: 20,
};
const personProxy = new Proxy(person, {
  get(target, key, receiver) {
    console.log(`get value by ${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`set ${key}, old value ${target[key]} to ${value}`);
    target[key] = value;
  },
});

Proxy 仅仅是一个代理,personProxy 上有 person 所有的属性和方法。我们通过personProxy获取和设置 name 时,就会有相应的 log 输出:

personProxy.name; // "wenzi"
// log: get value by name

personProxy.name = 'hello';
// log: set name, old value wenzi to hello

并且通过 personProxy 设置数据时,代理的原结构里的数据也会发生变化。我们打印下 person,可以发现字段 name 的值 也变成了hello:

console.log(person); // {name: "hello", age: 20}

Proxy 的第 2 个参数 handler 除了可以设置 get 和 set 方法外,还有更多丰富的方法:

1.get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy['foo']。

2.set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy['foo'] = v,返回一个布尔值。

3.has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。

4.deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。

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

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

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

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

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

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

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

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

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

如我们通过 delete 删除其中一个元素时,可以通过deleteProperty()方法来拦截这个操作。还是上面代理 person 的代码,我们添加一个 deleteProperty:

const person = {
  name: 'wenzi',
  age: 20,
};
const personProxy = new Proxy(person, {
  // 忽略get和set方法,与上面一样
  // ...
  deleteProperty(target, key, receiver) {
    console.log(`delete key ${key}`);
    delete target[key];
  },
});

当执行 delete 操作时:

delete personProxy['age'];
// log: delete key age

2. Proxy 与 Reflect

Proxy 与 Reflect 可以说形影不离了,Reflect 里所有的方法和使用方式与 Proxy 完全一样。

例如上面 Proxy 里的 get(), set()和 deleteProperty()方法我们都是直接操作原代理对象的,这里我们改成使用Reflect来操作:

const personProxy = new Proxy(person, {
  get(target, key, receiver) {
    console.log(`get value by ${key}`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`set ${key}, old value ${target[key]} to ${value}`);
    return Reflect.set(target, key, value, receiver);
  },
  deleteProperty(target, key, receiver) {
    console.log(`delete key ${key}`);
    return Reflect.deleteProperty(target, key, receiver);
  },
});

可以发现完美地实现这些功能。

3. 代理数组

我们在之前的文章 Vue 中对数组特殊的操作 中,讨论过 Vue 为什么没有使用Object.defineProperty来劫持数据,而是重写了 Array 原型链上的几个方法,通过这几个方法来实现 Vue 模板中数据的更新。

但若 Proxy 的话,就可以直接代理数组:

const arr = [1, 2, 3, 4];
const arrProxy = new Proxy(arr, {
  get(target, key, receiver) {
    console.log('arrProxy.get', target, key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log('arrProxy.set', target, key, value);
    return Reflect.set(target, key, value, receiver);
  },
  deleteProperty(target, key) {
    console.log('arrProxy.deleteProperty', target, key);
    return Reflect.deleteProperty(target, key);
  },
});

现在我们再来操作一下代理后的数组 arrProxy 看下:

arrProxy[2] = 22; // arrProxy.set (4) [1, 2, 3, 4] 2 22
arrProxy[3]; // arrProxy.get (4) [1, 2, 22, 4] 3
delete arrProxy[2]; // arrProxy.deleteProperty (4) [1, 2, 22, 4] 2
arrProxy.push(5); // push操作比较复杂,这里进行了多个get()和set()操作
arrProxy.length; // arrProxy.get (5) [1, 2, empty, 4, 5] length

可以看到无论获取、删除还是修改数据,都可以感知到。还有数组原型链上的一些方法,如:

1.push()

2.pop()

3.shift()

4.unshift()

5.splice()

6.sort()

7.reverse()

也都能通过 Proxy 中的代理方法劫持到。

concat()方法比较特殊的是,他是一个赋值操作,并不改变原数组,因此在调用 concat()方法操作数组时,如果没有赋值操作,那么这里只有 get()拦截到。

4. 代理函数

Proxy 中还有一个apply()方法,是表示自己作为函数调用时,被拦截的操作。

const getSum = (...args) => {
  if (!args.every((item) => typeof item === 'number')) {
    throw new TypeError('参数应当均为number类型');
  }
  return args.reduce((sum, item) => sum + item, 0);
};
const fnProxy = new Proxy(getSum, {
  /**
   * @params {Fuction} target 代理的对象
   * @params {any} ctx 执行的上下文
   * @params {any} args 参数
   */
  apply(target, ctx, args) {
    console.log('ctx', ctx);
    console.log(`execute fn ${getSum.name}, args: ${args}`);
    return Reflect.apply(target, ctx, args);
  },
});

执行 fnProxy:

// 10, ctx为undefined, log: execute fn getSum, args: 1,2,3,4
fnProxy(1, 2, 3, 4);

// ctx为undefined, Uncaught TypeError: 参数应当均为number类型
fnProxy(1, 2, 3, '4');

// 10, ctx为window, log: execute fn getSum, args: 1,2,3,4
fnProxy.apply(window, [1, 2, 3, 4]);

// 6, ctx为window, log: execute fn getSum, args: 1,2,3
fnProxy.call(window, 1, 2, 3);

// 6, ctx为person, log: execute fn getSum, args: 1,2,3
fnProxy.apply(person, [1, 2, 3]);

5. 一些简单的应用场景

我们知道 Vue3 里已经用 Proxy 重写了响应式系统,mobx 也已经用了 Proxy 模式。在可见的未来,会有更多的 Proxy 的应用场景,我们这里也稍微讲解几个。

5.1 统计函数被调用的上下文和次数

这里我们用 Proxy 来代理函数,然后函数被调用的上下文和次数。

const countExecute = (fn) => {
  let count = 0;

  return new Proxy(fn, {
    apply(target, ctx, args) {
      ++count;
      console.log('ctx上下文:', ctx);
      console.log(`${fn.name} 已被调用 ${count} 次`);
      return Reflect.apply(target, ctx, args);
    },
  });
};

现在我们来代理下刚才的getSum()方法:

const getSum = (...args) => {
  if (!args.every((item) => typeof item === 'number')) {
    throw new TypeError('参数应当均为number类型');
  }
  return args.reduce((sum, item) => sum + item, 0);
};

const useSum = countExecute(getSum);

useSum(1, 2, 3); // getSum 已被调用 1 次

useSum.apply(window, [2, 3, 4]); // getSum 已被调用 2 次

useSum.call(person, 3, 4, 5); // getSum 已被调用 3 次

5.2 实现一个防抖功能

基于上面统计函数调用次数的功能,也给我们实现一个函数的防抖功能添加了灵感。

const throttleByProxy = (fn, rate) => {
  let lastTime = 0;
  return new Proxy(fn, {
    apply(target, ctx, args) {
      const now = Date.now();
      if (now - lastTime > rate) {
        lastTime = now;
        return Reflect.apply(target, ctx, args);
      }
    },
  });
};

const logTimeStamp = () => console.log(Date.now());
window.addEventListener('scroll', throttleByProxy(logTimeStamp, 300));

logTimeStamp()至少需要 300ms 才能执行一次。

5.3 实现观察者模式

我们在这里实现一个最简单类 mobx 观察者模式。

const list = new Set();
const observe = (fn) => list.add(fn);
const observable = (obj) => {
  return new Proxy(obj, {
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      list.forEach((observer) => observer());
      return result;
    },
  });
};
const person = observable({ name: 'wenzi', age: 20 });
const App = () => {
  console.log(`App -> name: ${person.name}, age: ${person.age}`);
};
observe(App);

person就是使用 Proxy 创建出来的代理对象,每当 person 中的属性发生变化时,就会执行 App()函数。这样就实现了一个简单的响应式状态管理。

6. Proxy 与 Object.defineProperty 的对比

上面很多例子用Object.defineProperty也都是可以实现的。那么这两者都各有什么优缺点呢?

6.1 Object.defineProperty 的优劣

Object.defineProperty的兼容性可以说比 Proxy 要好很多,出特别低的 IE6,IE7 浏览器外,其他浏览器都有支持。

但 Object.defineProperty 支持的方法很多,并且主要是基于属性进行拦截的。因此在 Vue2 中只能重写 Array 原型链上的方法,来操作数组。

6.2 Proxy 的优劣

Proxy与上面的正好相反,Proxy 是基于对象来进行代理的,因此可代理更多的类型,例如 Object, Array, Function 等;而且代理的方法也多了很多。

劣势就是兼容性不太好,即使用 polyfill,也无法完美的实现。

7. 总结

Proxy 能实现的功能还有很多,后面我们也会继续进行探索,并且尽可能去了解下基于 Proxy 实现的类库,例如 mobx5 的源码和实现原理等。

以上就是JS带你深入领略Proxy的世界的详细内容,更多关于JS中的代理Proxy的资料请关注我们其它相关文章!

(0)

相关推荐

  • 如何通过Proxy实现JSBridge模块化封装

    最近公司在做一个项目,通过把我们自己的Webview植入第三方APP,然后我们的业务全部通过H5实现.至于为什么不直接用第三方APP WebView,主要是身处金融行业,需要做一些风控相关功能. 由于是Hybrid APP的性质,所以web与Native的通信是无法避免的:而为什么我要封装jsBridge,主要在于下面两点: 公司APP的JSBridge提供了数据的序列化和全局函数的注入,而我们这次由于包大小考虑,这一块需要H5自己来实现: 原生提供的接口协议太多,记住麻烦: 回调的写法不太人性

  • node.js使用 http-proxy 创建代理服务器操作示例

    本文实例讲述了node.js使用 http-proxy 创建代理服务器操作.分享给大家供大家参考,具体如下: 代理,也称网络代理,是一种特殊网络服务,允许一个终端通过代理服务与另一个终端进行非直接的连接,这样利于安全和防止被攻击. 代理服务器,就是代理网络用户去获取网络信息,就是信息的中转,负责转发. 代理又分 正向代理 和 反向代理: 正向代理:帮助局域网内的用户访问外面的服务. 反向代理:帮助外面的用户访问局域网内部的服务. 一.安装 http-proxy npm install http-

  • JavaScript中的ES6 Proxy的具体使用

    场景 就算只是扮演,也会成为真实的自我的一部分.对人类的精神来说,真实和虚假其实并没有明显的界限.入戏太深不是一件好事,但对于你来说并不成立,因为戏中的你才是真正符合你的身份的你.如今的你是真实的,就算一开始你只是在模仿着这种形象,现在的你也已经成为了这种形象.无论如何,你也不可能再回到过去了. Proxy 代理,在 JavaScript 似乎很陌生,却又在生活中无处不在.或许有人在学习 ES6 的时候有所涉猎,但却并未真正了解它的使用场景,平时在写业务代码时也不会用到这个特性. 相比于文绉绉的

  • JavaScript中的Proxy对象

    Js中Proxy对象 Proxy对象用于定义基本操作的自定义行为,例如属性查找.赋值.枚举.函数调用等. 语法 const proxy = new Proxy(target, handler); target: 要使用Proxy包装的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理. handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理proxy的行为. 描述 Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,

  • 详解nodejs通过代理(proxy)发送http请求(request)

    有可能有这样的需求,需要node作为web服务器通过另外一台http/https代理服务器发http或者https请求,废话不多说直接上代码大家都懂的: var http = require('http') var opt = { host:'这里放代理服务器的ip或者域名', port:'这里放代理服务器的端口号', method:'POST',//这里是发送的方法 path:' https://www.google.com', //这里是访问的路径 headers:{ //这里放期望发送出去

  • 使用nodejs中httpProxy代理时候出现404异常的解决方法

    在公司中使用nodejs构建代理服务器实现前后台分离,代码不能拿出来,然后出现httpProxy代理资源的时候老是出现404.明明被代理的接口是存在的.代码大概如下: var http = require('http'), httpProxy = require('http-proxy'); var proxy = httpProxy.createProxyServer({}); var server = http.createServer(function(req, res) { proxy.

  • 理解JavaScript中的Proxy 与 Reflection API

    一.创建 Proxy let target = {} let proxy = new Proxy(target, {}) proxy.name = "proxy" console.log(proxy.name) // proxy console.log(target.name) // proxy target.name = "target" console.log(proxy.name) // target console.log(target.name) // t

  • nuxt.js服务端渲染中axios和proxy代理的配置操作

    需要npm axios? 刚开始,我以为需要像普通的vue SPA开发那样,需要npm axios,这种方式的确可以生效.但在使用时并不方便.尤其是设置代理比较麻烦,而且在asyncData里与在普通methods里使用方式不一样. 后来在nuxt的github上发现了nuxt是默认集成了axios的,所以不需要npm axios,但是需要进行适当的配置. 以上是百度到的,发现老是报错,现在网上的教程完全是在扯淡,npm axios 是不需要安装了,但是 @nuxtjs/axios 要安装啊 第

  • JS带你深入领略Proxy的世界

    1. Proxy 的基本结构 Proxy 的基本使用方式: /** * target: 表示要代理的目标,可以是object, array, function类型 * handler: 是一个对象,可以编写各种代理的方法 */ const proxy = new Proxy(target, handler); 例如我们想要代理一个对象,可以通过设置 get 和 set 方法来代理获取和设置数据的操作: const person = { name: 'wenzi', age: 20, }; con

  • js带缩略图的图片轮播效果代码分享

    本文实例讲述了js带缩略图的图片轮播效果.分享给大家供大家参考.具体如下: 这是一款基于javaScript实现带缩略图的图片轮播特效代码,实现过程很简单. 运行效果图:-------------------查看效果 下载源码------------------- 小提示:浏览器中如果不能正常运行,可以尝试切换浏览模式. 在head区域引入CSS样式: <link href="css/jb51.net.css" rel="stylesheet" type=&q

  • js带前后翻页的图片切换效果代码分享

    本文实例讲述了javascript带前后翻页的图片切换效果.分享给大家供大家参考.具体如下: 这是一款基于javascript带前后翻页的图片切换效果代码,实现过程很简单. 运行效果图: -------------------查看效果------------------- 小提示:浏览器中如果不能正常运行,可以尝试切换浏览模式. 在head区域引入CSS样式: <link href="css/css.css" rel="stylesheet" type=&qu

  • js带点自动图片轮播幻灯片特效代码分享

    本文实例讲述了javascript带点自动图片轮播幻灯片特效.分享给大家供大家参考.具体如下: 这是一款基于javascript实现带点自动图片轮播幻灯片特效代码,实现过程很简单. 运行效果图:-------------------查看效果 下载源码------------------- 小提示:浏览器中如果不能正常运行,可以尝试切换浏览模式. (1)在head区域引入CSS样式: <link rel="stylesheet" href="css/zzsc.css&qu

  • 一文带你了解 C# DLR 的世界(DLR 探秘)

    在很久之前,我写了一片文章详解C# 匿名对象(匿名类型).var.动态类型 dynamic,可以借鉴.因为那时候是心中想当然的认为只有反射能够在运行时解析对象的成员信息并调用成员方法.后来也是因为其他的事一直都没有回过头来把这一节知识给补上,正所谓亡羊补牢,让我们现在来大致了解一下DLR吧. DLR 全称是 Dynamic Language Runtime(动态语言运行时).这很容易让我们想到同在C#中还有一个叫 CLR 的东西,它叫 Common Language Runtime.那这两者有什

  • Vue.js 带下拉选项的输入框(Textbox with Dropdown)组件

    带下拉选项的输入框 (Textbox with Dropdown) 是既允许用户从下拉列表中选择输入又允许用户自由键入输入值.这算是比较常见的一种 UI 元素,可以为用户提供候选项节省操作时间,也可以给可能存在的少数情况提供适配的可能. 本来想着这个组件比较常见应该已经有比较多现成的例子可以直接应用,但是搜索了一圈发现很多类似的组件都具备了太多的功能,例如搜索,多选等等 (简单说:太复杂了!).于是就想着还是自己动手写一个简单易用的,此处要感谢肥老板在我困惑时的鼎力相助. 这个 UI 元素将被用

  • js带闹铃功能的倒计时代码

    Js倒计时代码,带闹铃功能,自定义闹钟倒计时功能,点击开始按钮,即可开始倒数,代码不是太复杂,新手应该能看懂,代码分享给大家. 效果图: 源码: <html> <head> <title>Js倒计时,闹铃功能</title> <script language="javascript"> function $(id){ return document.getElementById(id); } function down(){

  • js带按钮的提示框可供选择示例代码

    今天在项目当中遇到一个问题(本人前台功底不深,高手勿喷): 可以供选择的弹出框: 利用jQuery没有想到好的提示效果. 参看js的API文档实现如下===confirm: 复制代码 代码如下: var r=confirm("该订单号已经存在,请重新输入或查询已存在订单详情!") if (di != null&& (r==true)) { userNameCnl.focus();//定位 userNameCnl.value = "";//清空数据 }

  • js实现带按钮的上下滚动效果

    本文实例讲述了js实现带按钮的上下滚动效果.分享给大家供大家参考.具体实现方法如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <

  • js实现带进度条提示的多视频上传功能

    本文实例为大家分享了js带进度条上传多视频的具体代码,供大家参考,具体内容如下 效果: 引用: <link rel="stylesheet" href="bootstrap.css" rel="external nofollow" > <script src="jquery.fileupload.js"></script> <script src="http://malsup

随机推荐