如果调用了 list[0].a 会发生什么呢?是的,依旧会是 undefined,因为 Reflect.ownKeys 也不能找到没有定义的属性(真*undefined),因此导致访问未定义的属性仍然会是 undefined 而非期望的默认值。
没有指向原对象,后续的修改会造成麻烦
如果我们此时修改对象的一个属性,那么会影响到原本的属性么?不会,因为 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 拼接字符串。