吾辈博客的原文地址: https://blog.rxliuli.com/p/c3...
就算只是扮演,也会成为真实的自个人一部分。对人类的精神来讲,真实和虚假其实并无明显的界限。入戏太深不是一件好事,但对于你来讲并不成立,由于戏中的你才是真正符合你的身份的你。现在的你是真实的,就算一开始你只是在模仿着这种形象,如今的你也已经成为了这种形象。不管如何,你也不可能再回到过去了。
Proxy
代理,在 JavaScript 彷佛很陌生,却又在生活中无处不在。或许有人在学习 ES6 的时候有所涉猎,但却并未真正了解它的使用场景,平时在写业务代码时也不会用到这个特性。git
相比于文绉绉的定义内容,想必咱们更但愿了解它的使用场景,使其在真正的生产环境发挥强大的做用,而不只仅是做为一个新的特性 -- 而后,实际中彻底没有用到!github
若是你尚未了解过 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 中的函数是一等公民,即函数能够被传递,被返回,以及,被添加属性!app
例以下面这个简单的函数 get
,其上有着 _name
这个属性异步
const get = async i => i get._name = 'get'
一旦使用上面的 asyncTimeout
函数包裹以后,问题便会出现,返回的函数中 _name
属性不见了。这是固然的,毕竟实际上返回的是一个匿名函数。那么,如何才能让返回的函数可以拥有传入函数参数上的全部自定义属性呢?
一种方式是复制参数函数上的全部属性,但这点实现起来其实并不容易,真的不容易,不信你能够看看 Lodash 的 clone 函数。那么,有没有一种更简单的方式呢?答案就是 Proxy
,它能够代理对象的指定操做,除此以外,其余的一切都指向原对象。
下面是 Proxy
实现的 asyncTimeout
函数async
/** * 为异步函数添加自动超时功能 * @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
函数有什么问题?
这里是答案的分割线
吾辈先解释一下这两个问题
list[0].a
会发生什么呢?是的,依旧会是 undefined
,由于 Reflect.ownKeys
也不能找到没有定义的属性(真*undefined
),所以致使访问未定义的属性仍然会是 undefined
而非指望的默认值。warp
返回的对象已是全新的了,和原对象没有什么联系。因此,当你修改时固然不会影响到原对象。''
-- 这并不意味着咱们愿意在其余操做时须要 ''
,不然咱们还要再转换一遍。(例如发送编辑后的数据到后台)这个时候 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
的使用场景,下面稍微来讲一下它的缺点
不能直接代理一些须要 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