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 相关文章推荐
JQuery下关于$.Ready()的分析
Dec 13 Javascript
Jquery ajax加载等待执行结束再继续执行下面代码操作
Nov 24 Javascript
浅析Bootstrap表格的使用
Jun 23 Javascript
js控制div层的叠加简单方法
Oct 15 Javascript
原生js实现可拖动的登录框效果
Jan 21 Javascript
js中小数向上取整数,向下取整数,四舍五入取整数的实现(必看篇)
Feb 13 Javascript
vue父子组件的数据传递示例
Mar 07 Javascript
JavaScript切换搜索引擎的导航网页搜索框实例代码
Jun 11 Javascript
详谈JS中数组的迭代方法和归并方法
Aug 11 Javascript
JavaScript编程设计模式之构造器模式实例分析
Oct 25 Javascript
浅析vue-router实现原理及两种模式
Feb 11 Javascript
Element el-button 按钮组件的使用详解
Feb 01 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调用JAVA的WebService简单实例
2014/03/11 PHP
解读PHP的Yii框架中请求与响应的处理流程
2016/03/17 PHP
ExtJS PropertyGrid中使用Combobox选择值问题
2010/06/13 Javascript
如何使用Javascript获取距今n天前的日期
2013/07/08 Javascript
Js使用WScript.Shell对象执行.bat文件和cmd命令
2014/12/18 Javascript
使用jQuery仿苹果官网焦点图特效
2014/12/23 Javascript
jQuery中trigger()方法用法实例
2015/01/19 Javascript
jquery点击改变class并toggle的实现代码
2016/05/15 Javascript
改变checkbox默认选中状态及取值的实现代码
2016/05/26 Javascript
vue 实现数字滚动增加效果的实例代码
2018/07/06 Javascript
js实现多个倒计时并行 js拼团倒计时
2019/02/25 Javascript
解决vue-cli 打包后自定义动画未执行的问题
2019/11/12 Javascript
JS表单验证插件之数据与逻辑分离操作实例分析【策略模式】
2020/05/01 Javascript
深入了解Vue3模板编译原理
2020/11/19 Vue.js
[01:24:16]2018DOTA2亚洲邀请赛 4.6 全明星赛
2018/04/10 DOTA
用Python计算三角函数之atan()方法的使用
2015/05/15 Python
Django验证码的生成与使用示例
2017/05/20 Python
Python获取航线信息并且制作成图的讲解
2019/01/03 Python
python协程之动态添加任务的方法
2019/02/19 Python
python3 实现调用串口功能
2019/12/26 Python
Pytorch技巧:DataLoader的collate_fn参数使用详解
2020/01/08 Python
Python定义一个Actor任务
2020/07/29 Python
python 如何区分return和yield
2020/09/22 Python
使用PyCharm官方中文语言包汉化PyCharm
2020/11/18 Python
微信浏览器取消缓存的方法
2015/03/28 HTML / CSS
html5 figure和figcaption的使用方法
2018/09/10 HTML / CSS
新东网科技Java笔试题
2012/07/13 面试题
留学推荐信怎么写
2014/01/25 职场文书
大学军训感想
2014/02/12 职场文书
初中作文评语大全
2014/04/23 职场文书
法制宣传口号
2014/06/16 职场文书
教师查摆问题自查报告
2014/10/11 职场文书
后进基层党组织整改方案
2014/10/25 职场文书
家长通知书家长意见
2014/12/30 职场文书
反邪教学习心得体会
2016/01/15 职场文书
想要创业,那么你做好准备了吗?
2019/07/01 职场文书