侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

JavaScript中的ES6 Proxy的具体使用

前端  /  管理员 发布于 5年前   310

场景

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

相比于文绉绉的定义内容,想必我们更希望了解它的使用场景,使其在真正的生产环境发挥强大的作用,而不仅仅是作为一个新的特性 -- 然后,实际中完全没有用到!

  • 为函数添加特定的功能
  • 代理对象的访问
  • 作为胶水桥接不同结构的对象
  • 监视对象的变化
  • 还有更多。。。

如果你还没有了解过 Proxy 特性,可以先去MDN Proxy 上查看基本概念及使用。

为函数添加特定的功能

下面是一个为异步函数自动添加超时功能的高阶函数,我们来看一下它有什么问题

/** * 为异步函数添加自动超时功能 * @param timeout 超时时间 * @param action 异步函数 * @returns 包装后的异步函数 */function asyncTimeout(timeout, action) { return function(...args) { return Promise.race([  Reflect.apply(action, this, args),  wait(timeout).then(Promise.reject), ]) }}

一般而言,上面的代码足以胜任,但问题就在这里,不一般的情况 -- 函数上面包含自定义属性呢?
众所周知,JavaScript 中的函数是一等公民,即函数可以被传递,被返回,以及,被添加属性!

例如下面这个简单的函数 get,其上有着 _name 这个属性

const get = async i => iget._name = 'get'

一旦使用上面的 asyncTimeout 函数包裹之后,问题便会出现,返回的函数中 _name 属性不见了。这是当然的,毕竟实际上返回的是一个匿名函数。那么,如何才能让返回的函数能够拥有传入函数参数上的所有自定义属性呢?

一种方式是复制参数函数上的所有属性,但这点实现起来其实并不容易,真的不容易,不信你可以看看 Lodash 的 clone 函数。那么,有没有一种更简单的方式呢?答案就是 Proxy,它可以代理对象的指定操作,除此之外,其他的一切都指向原对象。

下面是 Proxy 实现的 asyncTimeout 函数

/** * 为异步函数添加自动超时功能 * @param timeout 超时时间 * @param action 异步函数 * @returns 包装后的异步函数 */function asyncTimeout(timeout, action) { return new Proxy(action, { apply(_, _this, args) {  return Promise.race([  Reflect.apply(_, _this, args),  wait(timeout).then(Promise.reject),  ]) }, })}

测试一下,是可以正常调用与访问其上的属性的

;(async () => { console.log(await get(1)) console.log(get._name)})()

好了,这便是吾辈最常用的一种方式了 -- 封装高阶函数,为函数添加某些功能。

代理对象的访问

下面是一段代码,用以在页面上展示从后台获取的数据,如果字段没有值则默认展示 ''

模拟一个获取列表的异步请求

async function list() { // 此处仅为构造列表 class Person { constructor({ id, name, age, sex, address } = {}) {  this.id = id  this.name = name  this.age = age  this.sex = sex  this.address = address } } return [ new Person({ id: 1, name: '琉璃' }), new Person({ id: 2, age: 17 }), new Person({ id: 3, sex: false }), new Person({ id: 4, address: '幻想乡' }), ]}

尝试直接通过解构为属性赋予默认值,并在默认值实现这个功能

;(async () => { // 为所有为赋值属性都赋予默认值 '' const persons = (await list()).map( ({ id = '', name = '', age = '', sex = '', address = '' }) => ({  id,  name,  age,  sex,  address, }), ) console.log(persons)})()

下面让我们写得更通用一些

function warp(obj) { const result = obj for (const k of Reflect.ownKeys(obj)) { const v = Reflect.get(obj, k) result[k] = v === undefined ? '' : v } return obj};(async () => { // 为所有为赋值属性都赋予默认值 '' const persons = (await list()).map(warp) console.log(persons)})()

暂且先看一下这里的 warp 函数有什么问题?

这里是答案的分割线

  • 所有属性需要预定义,不能运行时决定
  • 没有指向原对象,后续的修改会造成麻烦

吾辈先解释一下这两个问题

  1. 所有属性需要预定义,不能运行时决定

如果调用了 list[0].a 会发生什么呢?是的,依旧会是 undefined,因为 Reflect.ownKeys 也不能找到没有定义的属性(真*undefined),因此导致访问未定义的属性仍然会是 undefined 而非期望的默认值。

  1. 没有指向原对象,后续的修改会造成麻烦

如果我们此时修改对象的一个属性,那么会影响到原本的属性么?不会,因为 warp 返回的对象已经是全新的了,和原对象没有什么联系。所以,当你修改时当然不会影响到原对象。

Pass: 我们当然可以直接修改原对象,但这很明显不太符合我们的期望:显示时展示默认值 '' -- 这并不意味着我们愿意在其他操作时需要 '',否则我们还要再转换一遍。(例如发送编辑后的数据到后台)

这个时候 Proxy 也可以派上用场,使用 Proxy 实现 warp 函数

function warp(obj) { const result = new Proxy(obj, { get(_, k) {  const v = Reflect.get(_, k)  if (v !== undefined) {  return v  }  return '' }, }) return result}

现在,上面的那两个问题都解决了!

注: 知名的 GitHub 库 immer 就使用了该特性实现了不可变状态树。

作为胶水桥接不同结构的对象

通过上面的例子我们可以知道,即便是未定义的属性,Proxy 也能进行代理。这意味着,我们可以通过 Proxy 抹平相似对象之间结构的差异,以相同的方式处理类似的对象。

Pass: 不同公司的项目中的同一个实体的结构不一定完全相同,但基本上类似,只是字段名不同罢了。所以使用 Proxy 实现胶水桥接不同结构的对象方便我们在不同公司使用我们的工具库!

嘛,开个玩笑,其实在同一个公司中不同的实体也会有类似的结构,也会需要相同的操作,最常见的应该是树结构数据。例如下面的菜单实体和系统权限实体就很相似,也需要相同的操作 -- 树 <=> 列表 相互转换。

思考一下如何在同一个函数中处理这两种树节点结构

/** * 系统菜单 */class SysMenu { /** * 构造函数 * @param {Number} id 菜单 id * @param {String} name 显示的名称 * @param {Number} parent 父级菜单 id */ constructor(id, name, parent) { this.id = id this.name = name this.parent = parent }}/** * 系统权限 */class SysPermission { /** * 构造函数 * @param {String} uid 系统唯一 uuid * @param {String} label 显示的菜单名 * @param {String} parentId 父级权限 uid */ constructor(uid, label, parentId) { this.uid = uid this.label = label this.parentId = parentId }}

下面让我们使用 Proxy 来抹平访问它们之间的差异

const sysMenuProxy = { parentId: 'parent' }const sysMenu = new Proxy(new SysMenu(1, 'rx', 0), { get(_, k) { if (Reflect.has(sysMenuProxy, k)) {  return Reflect.get(_, Reflect.get(sysMenuProxy, k)) } return Reflect.get(_, k) },})console.log(sysMenu.id, sysMenu.name, sysMenu.parentId) // 1 'rx' 0const sysPermissionProxy = { id: 'uid', name: 'label' }const sysPermission = new Proxy(new SysPermission(1, 'rx', 0), { get(_, k) { if (Reflect.has(sysPermissionProxy, k)) {  return Reflect.get(_, Reflect.get(sysPermissionProxy, k)) } return Reflect.get(_, k) },})console.log(sysPermission.id, sysPermission.name, sysPermission.parentId) // 1 'rx' 0

看起来似乎有点繁琐,让我们封装一下

/** * 桥接对象不存在的字段 * @param {Object} map 代理的字段映射 Map * @returns {Function} 转换一个对象为代理对象 */function bridge(map) { /** * 为对象添加代理的函数 * @param {Object} obj 任何对象 * @returns {Proxy} 代理后的对象 */ return function(obj) { return new Proxy(obj, {  get(target, k) {  // 如果遇到被代理的属性则返回真实的属性  if (Reflect.has(map, k)) {   return Reflect.get(target, Reflect.get(map, k))  }  return Reflect.get(target, k)  },  set(target, k, v) {  // 如果遇到被代理的属性则设置真实的属性  if (Reflect.has(map, k)) {   Reflect.set(target, Reflect.get(map, k), v)   return true  }  Reflect.set(target, k, v)  return true  }, }) }}

现在,我们可以用更简单的方式来做代理了。

const sysMenu = bridge({ parentId: 'parent',})(new SysMenu(1, 'rx', 0))console.log(sysMenu.id, sysMenu.name, sysMenu.parentId) // 1 'rx' 0const sysPermission = bridge({ id: 'uid', name: 'label',})(new SysPermission(1, 'rx', 0))console.log(sysPermission.id, sysPermission.name, sysPermission.parentId) // 1 'rx' 0

如果想看 JavaScirpt 如何处理树结构数据话,可以参考吾辈的JavaScript 处理树数据结构

监视对象的变化

接下来,我们想想,平时是否有需要监视对象的变化,然后进行某些处理呢?

例如监视用户复选框选中项列表的变化并更新对应的需要发送到后台的 id 拼接字符串。

// 模拟页面的复选框列表const hobbyMap = new Map() .set(1, '小说') .set(2, '动画') .set(3, '电影') .set(4, '游戏')const user = { id: 1, // 保存兴趣 id 的列表 hobbySet: new Set(), // 发送到后台的兴趣 id 拼接后的字符串,以都好进行分割 hobby: '',}function onClick(id) { user.hobbySet.has(id) ? user.hobbySet.delete(id) : user.hobbySet.add(id)}// 模拟两次点击onClick(1)onClick(2)console.log(user.hobby) // ''

下面使用 Proxy 来完成 hobbySet 属性改变后 hobby 自动更新的操作

/** * 深度监听指定对象属性的变化 * 注:指定对象不能是原始类型,即不可变类型,而且对象本身的引用不能改变,最好使用 const 进行声明 * @param object 需要监视的对象 * @param callback 当代理对象发生改变时的回调函数,回调函数有三个参数,分别是对象,修改的 key,修改的 v * @returns 返回源对象的一个代理 */function watchObject(object, callback) { const handler = { get(_, k) {  try {  // 注意: 这里很关键,它为对象的字段也添加了代理  return new Proxy(v, Reflect.get(_, k))  } catch (err) {  return Reflect.get(_, k)  } }, set(_, k, v) {  callback(_, k, v)  return Reflect.set(_, k, v) }, } return new Proxy(object, handler)}// 模拟页面的复选框列表const hobbyMap = new Map() .set(1, '小说') .set(2, '动画') .set(3, '电影') .set(4, '游戏')const user = { id: 1, // 保存兴趣 id 的列表 hobbySet: new Set(), // 发送到后台的兴趣 id 拼接后的字符串,以都好进行分割 hobby: '',}const proxy = watchObject(user, (_, k, v) => { if (k === 'hobbySet') { _.hobby = [..._.hobbySet].join(',') }})function onClick(id) { proxy.hobbySet = proxy.hobbySet.has(id) ? proxy.hobbySet.delete(id) : proxy.hobbySet.add(id)}// 模拟两次点击onClick(1)onClick(2)// 现在,user.hobby 的值将会自动更新console.log(user.hobby) // 1,2

当然,这里实现的 watchObject 函数还非常非常非常简陋,如果有需要可以进行更深度/强大的监听,可以尝试自行实现一下啦!

缺点

说完了这些 Proxy 的使用场景,下面稍微来说一下它的缺点

运行环境必须要 ES6 支持

这是一个不大不小的问题,现代的浏览器基本上都支持 ES6,但如果泥萌公司技术栈非常老旧的话(例如支持 IE6),还是安心吃土吧 #笑 #这种公司不离职等着老死

不能直接代理一些需要 this 的对象

这个问题就比较麻烦了,任何需要 this 的对象,代理之后的行为可能会发生变化。例如 Set 对象

const proxy = new Proxy(new Set([]), {})proxy.add(1) // Method Set.prototype.add called on incompatible receiver [object Object]

是不是很奇怪,解决方案是把所有的 get 操作属性值为 function 的函数都手动绑定 this

const proxy = new Proxy(new Set([]), { get(_, k) { const v = Reflect.get(_, k) // 遇到 Function 都手动绑定一下 this if (v instanceof Function) {  return v.bind(_) } return v },})proxy.add(1)

总结

Proxy 是个很强大的特性,能够让我们实现一些曾经难以实现的功能(所以这就是你不支持 ES5 的理由?#打),就连 Vue3+ 都开始使用 Proxy 实现了,你还有什么理由在乎上古时期的 IE 而不用呢?(v^_^)v

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

您可能感兴趣的文章:

  • ES6 Proxy实现Vue的变化检测问题
  • ES6知识点整理之Proxy的应用实例详解
  • 详解ES6中的代理模式――Proxy
  • 实例解析ES6 Proxy使用场景介绍
  • 浅谈es6语法 (Proxy和Reflect的对比)
  • ES6中Proxy代理用法实例浅析
  • ES6中Proxy与Reflect实现重载(overload)的方法
  • 详细探究ES6之Proxy代理


  • 上一条:
    JS中的一些常用的函数式编程术语
    下一条:
    JavaScript 处理树数据结构的方法示例
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 使用 Alpine.js 排序插件对元素进行排序(0个评论)
    • 在js中使用jszip + file-saver实现批量下载OSS文件功能示例(0个评论)
    • 在vue中实现父页面按钮显示子组件中的el-dialog效果(0个评论)
    • 使用mock-server实现模拟接口对接流程步骤(0个评论)
    • vue项目打包程序实现把项目打包成一个exe可执行程序(0个评论)
    • 近期文章
    • 智能合约Solidity学习CryptoZombie第三课:组建僵尸军队(高级Solidity理论)(0个评论)
    • 智能合约Solidity学习CryptoZombie第二课:让你的僵尸猎食(0个评论)
    • 智能合约Solidity学习CryptoZombie第一课:生成一只你的僵尸(0个评论)
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2017-06
    • 2017-07
    • 2017-08
    • 2017-09
    • 2017-10
    • 2017-11
    • 2018-03
    • 2018-04
    • 2018-05
    • 2018-06
    • 2018-09
    • 2018-11
    • 2018-12
    • 2019-02
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2021-04
    • 2021-05
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-11
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-09
    • 2023-10
    • 2023-11
    • 2023-12
    • 2024-01
    • 2024-02
    • 2024-03
    • 2024-04
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客