JavaScript中的ES6 Proxy的具体使用


Posted in Javascript onJune 16, 2019

场景

就算只是扮演,也会成为真实的自我的一部分。对人类的精神来说,真实和虚假其实并没有明显的界限。入戏太深不是一件好事,但对于你来说并不成立,因为戏中的你才是真正符合你的身份的你。如今的你是真实的,就算一开始你只是在模仿着这种形象,现在的你也已经成为了这种形象。无论如何,你也不可能再回到过去了。
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 => i
get._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' 0

const 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' 0

const 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

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

Javascript 相关文章推荐
js实现页面打印功能实例代码(附去页眉页脚功能代码)
Dec 15 Javascript
javascript Firefox与IE 替换节点的方法
Feb 24 Javascript
jQuery Pagination Ajax分页插件(分页切换时无刷新与延迟)中文翻译版
Jan 11 Javascript
Jquery中使用setInterval和setTimeout的方法
Apr 08 Javascript
JavaScript对象和字串之间的转换实例探讨
Apr 21 Javascript
单击浏览器右上角的X关闭窗口弹出提示的小例子
Jun 12 Javascript
js 得到文件后缀(通过正则实现)
Jul 08 Javascript
JS实现间歇滚动的运动效果实例
Dec 22 Javascript
BootStrop前端框架入门教程详解
Dec 25 Javascript
利用imgareaselect辅助后台实现图片上传裁剪
Mar 02 Javascript
VueJS事件处理器v-on的使用方法
Sep 27 Javascript
详解如何制作并发布一个vue的组件的npm包
Nov 10 Javascript
简谈创建React Component的几种方式
Jun 15 #Javascript
JS中的一些常用的函数式编程术语
Jun 15 #Javascript
JavaScript模块管理的简单实现方式详解
Jun 15 #Javascript
JavaScript工具库之Lodash详解
Jun 15 #Javascript
jQuery创建折叠式菜单
Jun 15 #jQuery
JavaScript的Proxy可以做哪些有意思的事儿
Jun 15 #Javascript
Async/Await替代Promise的6个理由
Jun 15 #Javascript
You might like
如何使用PHP中的字符串函数
2006/10/09 PHP
php 图像函数大举例(非原创)
2009/06/20 PHP
php simplexmlElement操作xml的命名空间实现代码
2011/01/04 PHP
深入解析PHP的Yii框架中的event事件机制
2016/03/17 PHP
PHP8.0新功能之Match表达式的使用
2020/07/19 PHP
jQuery 判断页面元素是否存在的代码
2009/08/14 Javascript
讨论javascript(一)工厂方式 js面象对象的定义方法
2009/12/15 Javascript
Javascript 键盘keyCode键码值表
2009/12/24 Javascript
jQuery移动和复制dom节点实用DOM操作案例
2012/12/17 Javascript
JavaScript代码复用模式详解
2014/11/07 Javascript
javascript制作网页图片上实现下雨效果
2015/02/26 Javascript
jQuery 回调函数(callback)的使用和基础
2015/02/26 Javascript
Node.js的Web模板引擎ejs的入门使用教程
2016/06/06 Javascript
Angular的$http与$location
2016/12/26 Javascript
React Router基础使用
2017/01/17 Javascript
关于AngularJs数据的本地存储详解
2017/01/20 Javascript
JavaScript中捕获与冒泡详解及实例
2017/02/03 Javascript
weui框架实现上传、预览和删除图片功能代码
2017/08/24 Javascript
微信小程序事件流原理解析
2019/11/27 Javascript
Vue 实现登录界面验证码功能
2020/01/03 Javascript
Javascript中window.name属性详解
2020/11/19 Javascript
[11:12]2018DOTA2国际邀请赛寻真——绿色长城OpTic
2018/08/10 DOTA
使用Python脚本来获取Cisco设备信息的示例
2015/05/04 Python
Python 加密的实例详解
2017/10/09 Python
Python 元组拆包示例(Tuple Unpacking)
2019/12/24 Python
适合Python初学者的一些编程技巧
2020/02/12 Python
浅谈keras中的keras.utils.to_categorical用法
2020/07/02 Python
如何在mac版pycharm选择python版本
2020/07/21 Python
教师节横幅标语
2014/10/08 职场文书
2015年班干部工作总结
2015/04/29 职场文书
保险公司反洗钱宣传活动总结
2015/05/08 职场文书
2019如何书写演讲稿?
2019/07/01 职场文书
redis配置文件中常用配置详解
2021/04/14 Redis
SQL实现LeetCode(177.第N高薪水)
2021/08/04 MySQL
Python 数据可视化之Seaborn详解
2021/11/02 Python
Go语言安装并操作redis的go-redis库
2022/04/14 Golang