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 相关文章推荐
window.onload 加载完毕的问题及解决方案(下)
Jul 09 Javascript
jquery中EasyUI使用技巧小结
Feb 10 Javascript
JavaScript操作XML/HTML比较常用的对象属性集锦
Oct 30 Javascript
Jquery1.9.1源码分析系列(十五)动画处理之外篇
Dec 04 Javascript
JS组件系列之Bootstrap table表格组件神器【终结篇】
May 10 Javascript
使用BootStrap实现用户登录界面UI
Aug 10 Javascript
浅谈React 属性和状态的一些总结
Nov 21 Javascript
webpack手动配置React开发环境的步骤
Jul 02 Javascript
详解原生JS回到顶部
Mar 25 Javascript
JavaScript运动原理基础知识详解
Apr 02 Javascript
JavaScript实现像雪花一样的Hexaflake分形
Jul 07 Javascript
vue 解决addRoutes多次添加路由重复的操作
Aug 04 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 Warning: Module 'modulename' already loaded in问题解决办法
2015/03/16 PHP
PHP实现QQ空间自动回复说说的方法
2015/12/02 PHP
IE6、IE7中获取Button元素的值的bug说明
2011/08/28 Javascript
node.js中的console.trace方法使用说明
2014/12/09 Javascript
javascript实现textarea中tab键的缩排处理方法
2015/06/26 Javascript
轻松学习jQuery插件EasyUI EasyUI表单验证
2015/12/01 Javascript
jQuery实现的鼠标滑过弹出放大图片特效
2016/01/08 Javascript
jQuery插件扩展测试实例
2016/06/21 Javascript
详解angularjs结合pagination插件实现分页功能
2017/02/10 Javascript
JavaScript从原型到原型链深入理解
2019/06/03 Javascript
js blob类型url的视频下载问题的解决
2019/11/29 Javascript
js事件机制----捕获与冒泡机制实例分析
2020/05/22 Javascript
解决antd 下拉框 input [defaultValue] 的值的问题
2020/10/31 Javascript
js实现有趣的倒计时效果
2021/01/19 Javascript
[09:33]2015国际邀请赛第四日TOP10
2015/08/08 DOTA
python实现的希尔排序算法实例
2015/07/01 Python
单利模式及python实现方式详解
2018/03/20 Python
Python二叉搜索树与双向链表转换算法示例
2019/03/02 Python
python字典嵌套字典的情况下找到某个key的value详解
2019/07/10 Python
Python3 使用selenium插件爬取苏宁商家联系电话
2019/12/23 Python
Python 将json序列化后的字符串转换成字典(推荐)
2020/01/06 Python
python如何查看网页代码
2020/06/07 Python
Keras 在fit_generator训练方式中加入图像random_crop操作
2020/07/03 Python
CSS去掉A标签(链接)虚线框的方法
2014/04/01 HTML / CSS
详解HTML5 Canvas绘制不规则图形时的非零环绕原则
2016/03/21 HTML / CSS
泰国综合购物网站:Lazada泰国
2018/04/09 全球购物
学院领导推荐信
2013/10/30 职场文书
新手上路标语
2014/06/20 职场文书
物理学专业求职信
2014/07/04 职场文书
企业员工爱岗敬业演讲稿
2014/08/26 职场文书
四风问题个人对照检查剖析材料
2014/09/27 职场文书
网球场地租赁协议范本
2014/10/07 职场文书
2015年宣传工作总结
2015/04/08 职场文书
2015秋季小学开学寄语
2015/05/27 职场文书
《自然之道》读后感3篇
2019/12/17 职场文书
Python time库的时间时钟处理
2021/05/02 Python