JS 中的数据代理

所谓数据代理(也叫数据劫持),指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。比较典型的是 Object.defineProperty() 和 ES2015 中新增的 Proxy 对象。另外还有已经被废弃的 Object.observe(),废弃的原因正是 Proxy 的出现,因此这里我们就不继续讨论这个已经被浏览器删除的方法了。

数据劫持最著名的应用当属双向绑定,这也是一个已经被讨论烂了的面试必考题。例如 Vue 2.x 使用的是 Object.defineProperty()(Vue 在 3.x 版本之后改用 Proxy 进行实现)。此外 immer.js 为了保证数据的 immutable 属性,使用了 Proxy 来阻断常规的修改操作,也是数据劫持的一种应用。

看看这两种方法的优劣。

Object.defineProperty

Vue 的双向绑定已经升级为前端面试的必考题,原理我就不再重复了,网上一大片。简单来说就是利用 Object.defineProperty(),并且把内部解耦为 Observer, Dep, 并使用 Watcher 相连。

Object.defineProperty() 的问题主要有三个:

不能监听数组的变化

看如下代码:

let arr = [1,2,3]
let obj = {}Object.defineProperty(obj, 'arr', {get () {console.log('get arr')return arr},set (newVal) {console.log('set', newVal)arr = newVal}
})obj.arr.push(4) // 只会打印 get arr, 不会打印 set
obj.arr = [1,2,3,4] // 这个能正常 set

数组的以下几个方法不会触发 set

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

Vue 把这些方法定义为变异方法 (mutation method),指的是会修改原来数组的方法。与之对应则是非变异方法 (non-mutating method),例如 filter, concat, slice 等,它们都不会修改原始数组,而会返回一个新的数组。Vue 官网有相关文档讲述这个问题。

Vue 的做法是把这些方法重写来实现数组的劫持。一个极简的实现如下:

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = {};aryMethods.forEach((method)=> {// 这里是原生 Array 的原型方法let original = Array.prototype[method];// 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上// 注意:是实例属性而非原型属性arrayAugmentations[method] = function () {console.log('我被改变啦!');// 调用对应的原生方法并返回结果return original.apply(this, arguments);};});let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 这样就能在调用 push, pop 这些方法时走进我们刚定义的方法,多了一句 console.log
list.__proto__ = arrayAugmentations;
list.push('d');  // 我被改变啦!// 这个 list2 是个普通的数组,所以调用 push 不会走到我们的方法里面。
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 不输出内容

必须遍历对象的每个属性

使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历,于是多了一层嵌套。如:

Object.keys(obj).forEach(key => {Object.defineProperty(obj, key, {// ...})
})

必须深层遍历嵌套的对象

所谓的嵌套对象,是指类似

let obj = {info: {name: 'eason'}
}

如果是这一类嵌套对象,那就必须逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。 Vue 的源码中就能找到这样的逻辑 (叫做 walk 方法)。 

 

Proxy

Proxy 在 ES2015 规范中被正式加入,它的支持度虽然不如 Object.defineProperty(),但其实也基本支持了 (除了 IE 和 Opera Mini 等少数浏览器,数据来自 caniuse),所以使用起来问题也不太大。

针对对象

在数据劫持这个问题上,Proxy 可以被认为是 Object.defineProperty() 的升级版。外界对某个对象的访问,都必须经过这层拦截。因此它是针对 整个对象,而不是 对象的某个属性,所以也就不需要对 keys 进行遍历。这解决了上述 Object.defineProperty() 的第二个问题。

let obj = {name: 'Eason',age: 30
}let handler = {get (target, key, receiver) {console.log('get', key)return Reflect.get(target, key, receiver)},set (target, key, value, receiver) {console.log('set', key, value)return Reflect.set(target, key, value, receiver)}
}
let proxy = new Proxy(obj, handler)proxy.name = 'Zoe' // set name Zoe
proxy.age = 18 // set age 18

如上代码,Proxy 是针对 obj 的。因此无论 obj 内部包含多少个 key ,都可以走进 set。(省了一个 Object.keys() 的遍历)

另外这个 Reflect.getReflect.set 可以理解为类继承里的 super,即调用原来的方法。详细的 Reflect 可以查看这里,或者查看js专栏,本文不做展开。

支持数组
 

let arr = [1,2,3]let proxy = new Proxy(arr, {get (target, key, receiver) {console.log('get', key)return Reflect.get(target, key, receiver)},set (target, key, value, receiver) {console.log('set', key, value)return Reflect.set(target, key, value, receiver)}
})proxy.push(4)
// 能够打印出很多内容
// get push     (寻找 proxy.push 方法)
// get length   (获取当前的 length)
// set 3 4      (设置 proxy[3] = 4)
// set length 4 (设置 proxy.length = 4)

Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的。

嵌套支持

本质上,Proxy 也是不支持嵌套的,这点和 Object.defineProperty() 是一样的。因此也需要通过逐层遍历来解决。Proxy 的写法是在 get 里面递归调用 Proxy 并返回,代码如下:

let obj = {info: {name: 'eason',blogs: ['webpack', 'babel', 'cache']}
}let handler = {get (target, key, receiver) {console.log('get', key)// 递归创建并返回if (typeof target[key] === 'object' && target[key] !== null) {return new Proxy(target[key], handler)}return Reflect.get(target, key, receiver)},set (target, key, value, receiver) {console.log('set', key, value)return Reflect.set(target, key, value, receiver)}
}
let proxy = new Proxy(obj, handler)// 以下两句都能够进入 set
proxy.info.name = 'Zoe'
proxy.info.blogs.push('proxy')

其他区别

除了上述两点之外, Proxy 还拥有以下优势:

  • Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富
  • Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。

这第二个优势源于它是新标准。但新标准同样也有劣势,那就是:

  • Proxy 的兼容性不如 Object.defineProperty() (caniuse 的数据表明,QQ 浏览器和百度浏览器并不支持 Proxy,这对国内移动开发来说估计无法接受,但两者都支持 Object.defineProperty())
  • 不能使用 polyfill 来处理兼容性

这些比较仅针对“数据劫持的实现”这个需求而言。 Object.defineProperty() 除了定义 getset 之外,还能实现其他功能,因此即便不考虑兼容性的情况下,并不是说一个可以完全淘汰另一个。

应用

只谈技术本身而不谈应用场景基本都是耍流氓。一个技术只有拥有了应用场景,才真正有价值。

如开头所说,数据劫持多出现在框架内部,例如 Vue, immer 之类的,不过这些好像和我们普通程序员相去甚远。除开这些,我列举几个可能的应用场景,大家在平时的工作中可能还能想到更多。

一道面试题

其实除了阅读 Vue 的数据绑定源码之外,我第二次了解这个技术是通过一道曾经在开发者群体中小火一阵的诡异题目:

什么样的 a 可以满足 (a === 1 && a === 2 && a === 3) === true 呢?(注意是 3 个 =,也就是严格相等)

既然是严格相等,类型转换什么的基本不考虑了。一个自然的想法就是每次访问 a 返回的值都不一样,那么肯定会想到数据劫持。(可能还有其他解法,但这里只讲数据劫持的方法)

 
let current = 0
Object.defineProperty(window, 'a', {get () {current++return current}
})
console.log(a === 1 && a === 2 && a === 3) // true

使用 Proxy 也可以,但因为 Proxy 的语法是返回一个新的对象,因此要做到 a === 1 可能比较困难,做到 obj.a === 1 还是 OK 的,反正原理是一样的,也不必纠结太多。

多继承

Javascript 通过原型链实现继承,正常情况一个对象(或者类)只能继承一个对象(或者类)。但通过这两个方法都可以实现一种黑科技,允许一个对象继承两个对象。下面的例子使用 Proxy 实现。

 
let foo = {foo () {console.log('foo')}
}let bar = {bar () {console.log('bar')}
}
// 正常状态下,对象只能继承一个对象,要么有 foo(),要么有 bar()
let sonOfFoo = Object.create(foo);
sonOfFoo.foo();     // foo
let sonOfBar = Object.create(bar);
sonOfBar.bar();     // bar// 黑科技开始
let sonOfFooBar = new Proxy({}, {get (target, key) {return target[key] || foo[key] || bar[key];}
})
// 我们创造了一个对象同时继承了两个对象,foo() 和 bar() 同时拥有
sonOfFooBar.foo();   // foo 有foo方法,继承自对象foo
sonOfFooBar.bar();   // bar 也有bar方法,继承自对象bar

当然实际有啥用处我暂时还没想到,且考虑到代码的可读性,多数可能只存在于炫技或者面试题中

隐藏私有变量

既然能够操纵 get,自然就可以实现某些属性可以访问,而某些不可以,这就是共有和私有属性的概念。实现起来也很简单:

function getObject(rawObj, privateKeys) {return new Proxy(rawObj, {get (target, key, receiver) {if (privateKeys.indexOf(key) !== -1) {throw new ReferenceError(`${key} 是私有属性,不能访问。`)}return target[key]}})
}let rawObj = {name: 'Zoe',age: 18,isFemale: true
}
let obj = getObject(rawObj, ['age'])console.log(obj.name) // Zoe
console.log(obj.age) // 报错

对象属性的设定时校验

如果对象的某些属性有类型要求,只能接受特定类型的值,通过 Proxy 我们可以在设置时即给出错误,而不是在使用时再统一递归遍历检查。这样无论在执行效率还是在使用友好度上都更好一些。

let person = {name: 'Eason',age: 30
}let handler = {set (target, key, value, receiver) {if (key === 'name' && typeof value !== 'string') {throw new Error('用户姓名必须是字符串类型')}if (key === 'age' && typeof value !== 'number') {throw new Error('用户年龄必须是数字类型')}return Reflect.set(target, key, value, receiver)}
}let personForUser = new Proxy(person, handler)personForUser.name = 'Zoe' // OK
personForUser.age = '18' // 报错

各类容错检查

我们常常会向后端发送请求,等待响应并处理响应的数据,且为了代码健壮性,通常会有很多判断,如:

// 发送请求代码省略,总之获取到了 response 对象了。
if (!response.data) {console.log('响应体没有信息')return
} else if (!response.data.message) {console.log('后端没有返回信息')return
} else if (!response.data.message.from || !response.data.message.text) {console.log('后端返回的信息不完整')return
} else {console.log(`你收到了来自 ${response.data.message.from} 的信息:${response.data.message.text}`)
}

代码的实质是为了获取 response.data.message.fromresponse.data.message.text,但需要逐层判断,否则 JS 就会报错。

我们可以考虑用 Proxy 来改造这段代码,让它稍微好看些。

// 故意设置一个错误的 data1,即 response.data = undefined
let response = {data1: {message: {from: 'Eason',text: 'Hello'}}
}// 也可以根据 key 的不同给出更友好的提示
let dealError = key => console.log('Error key', key)let isOK = obj => !obj['HAS_ERROR']let handler = {get (target, key, receiver) {// 基本类型直接返回if (target[key] !== undefined && typeof target[key] !== 'object') {return Reflect.get(target, key, receiver)}// 如果是 undefined,把访问的的 key 传递到错误处理函数 dealError 里面if (!target[key]) {if (!target['HAS_ERROR']) {dealError(key)}return new Proxy({HAS_ERROR: true}, handler)}// 正常的话递归创建 Proxyreturn new Proxy(target[key], handler)}
}let resp = new Proxy(response, handler)if (isOK(resp.data.message.text) && isOK(resp.data.message.from)) {console.log(`你收到了来自 ${response.data.message.from} 的信息:${response.data.message.text}`)
}

因为我们故意设置了 response.data = undefined,因此会进入 dealError 方法,参数 key 的值为 data

虽然从代码量来看比上面的 if 检查更长,但 isOK, handlernew Proxy 的定义都是可以复用的,可以移动到一个单独的文件,仅暴露几个方法即可。所以实际的代码只有 dealError 的定义和最后的一个 if 而已。

更多应用场景

  • 设置对象默认值  - 创建一个对象,它的某些属性自带默认值。

  • 优化的枚举类型  - 枚举类型的 key 出错时立刻报错而不是静默的返回 undefined,因代码编写错误导致的重写、删除等也可以被拦截。

  • 追踪对象和数组的变化  - 在数组和对象的某个元素/属性发生变化时抛出事件。这可能适用于撤销,重做,或者直接回到某个历史状态。

  • 给对象的属性访问增加缓存,提升速度  - 在对对象的某个属性进行设置时记录值,在访问时直接返回而不真的访问属性。增加 TTL 检查机制(Time To Live,存活时间)防止内存泄露。

  • 支持 in 关键词的数组 - 通过设置 has 方法,内部调用 array.includes。使用的时候则直接 console.log('key' in someArr)

  • 实现单例模式  - 通过设置 construct 方法,在执行 new 操作符总是返回同一个单例,从而实现单例模式。

  • Cookie 的类型转换  - document.cookie 是一个用 ; 分割的字符串。我们可以把它转化为对象,并通过 ProxysetdeleteProperty 重新定义设置和删除操作,用以对外暴露一个可操作的 Cookie 对象,方便使用。

参考文档

  • 面试官: 实现双向绑定Proxy比defineproperty优劣如何? 
  • ES6 特性 - 10 个代理用例 

 

 

 

 

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/690562.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

oauthlib,一个强大的 Python 身份校验库!

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站零基础入门的AI学习网站~。 目录 ​编辑 前言 什么是 OAuthLib? 安装 OAuthLib OAuthLib 的主要功能 OAuthLib 的用法 实现…

kafka安装配置(docker)

Kafka对于zookeeper是强依赖,保存kafka相关的节点数据,所以安装Kafka之前必须先安装zookeeper Docker安装zookeeper 下载镜像: docker pull zookeeper:3.4.14 创建容器 docker run -d --name zookeeper -p 2181:2181 zookeeper:3.4.14 D…

文案馆头像壁纸微信小程序源码【支持流量主】

文案馆头像壁纸微信小程序源码【支持流量主】 源码介绍:文案馆头像壁纸微信小程序源码是一款可以获取套图、头像、壁纸的小程序。小程序源码内置流量主功能 需求环境:微信小程序phpmysql 下载地址: https://www.changyouzuhao.cn/13453.ht…

【C语言】长篇详解,字符系列篇2-----受长度限制的字符串函数,字符串函数的使用和模拟实现【图文详解】

欢迎来CILMY23的博客喔,本期系列为【【C语言】长篇详解,字符系列篇2-----“混杂”的字符串函数,字符串函数的使用和模拟实现【图文详解】,图文讲解各种字符串函数,带大家更深刻理解C语言中各种字符串函数的应用&#x…

即时设计是什么?

在过去的两年里,由于疫情的推动以及科学技术的不断进步,国内外协同办公室发展迅速。在市场的推动下,市场上出现了越来越多的协同办公软件,使工作场所的工作更加高效。 在设计领域,具有协同功能的软件市场似乎仍处于空…

记一个大坑: 树莓派上docker运行motioneye找不到摄像头

当在树莓派上执行这段命令后,将创建montioneye容器 docker run --name"motioneye" \-p 8765:8765 \--hostname"motioneye" \-v /etc/localtime:/etc/localtime:ro \-v /etc/motioneye:/etc/motioneye \-v /var/lib/motioneye:/var/lib/motione…

七、C数组的介绍

1、前言 C语言中的数据类型包括基本数据类型和复合数据类型。前面介绍的整形、浮点型、字符型都是基本数据类型,而数组属于复合数据类型。 基本数据类型是编程中最基础的数据类型,用于存储简单的数据值。复合数据类型则是由基本数据类型组合而成的数据类…

C# CAD-Xdata数据添加与修改

运行环境Visual Studio 2022 c# cad2016 一、XData(扩展数据)特定代码值 XData(扩展数据)特定代码值 XData通过一系列DXF组码(DxfCode)存储不同类型的数据,包括但不限于ASCII字符串、已注册应…

162基于matlab的多尺度和谱峭度算法对振动信号进行降噪处理

基于matlab的多尺度和谱峭度算法对振动信号进行降噪处理,选择信号峭度最大的频段进行滤波,输出多尺度谱峭度及降噪结果。程序已调通,可直接运行。 162 matlab 信号处理 多尺度谱峭度 (xiaohongshu.com)

用Windows桌面应用程序制作一个扫雷游戏

游戏介绍: 这段代码是一个简易版的扫雷游戏的主程序部分。游戏分为几个主要部分: **主函数 (main)**:负责整个游戏流程的控制。首先,它初始化了一个枚举类型的变量 input 用于存储玩家的选择。然后,进入一个循环,在这个循环中,它会显示游戏菜单,接收玩家的输入,并根…

考研英语单词28

Day 28 obscure a.模糊的,不清楚的【vague a.模糊的,不清楚的】 blur “不乐” n.模糊(的东西) v.变模糊 rough a.粗糙的,艰难的 readily ad.轻易地,乐意地 management n.经营,管理…

实时文字to图:SDXL Turbo 和 LCM-LoRA

参考文章: SDXL Turbo: Real-time Prompting - Stable Diffusion Art 根据目前的实际使用情况 sdxl-turbo 速度更快sdxl 有时候出的人脸会变形

mysql 2-1

添加数据 方式二 更新数据 删除数据 小结 计算列 数据类型 可选属性 适用场景 如何选择 浮点类型 存在精度问题 定点数介绍 BIT类型 日期与时间类型 YEAR类型 DATA类型 TIME类型 DATATIME TIMESTAMP 文本字符串类型 适用场景 TEXT类型

【ArcGIS Pro二次开发】(80):标注_CIMLabelClass

CIMLabelClass(Cartographic Information Model Label Class)是ArcGIS Pro SDK中的一个类。 它主要用于定义标签的样式和属性,如字体、大小、颜色、对齐方式等,以及标签的排列和布局规则。 1、获取当前地图的标签引擎 // 获取当…

基于Springboot的校园求职招聘系统(有报告)。Javaee项目,springboot项目。

演示视频: 基于Springboot的校园求职招聘系统(有报告)。Javaee项目,springboot项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构…

物联网芯片ESP8266 介绍

ESP8266是一款由Espressif Systems所开发的低成本的Wi-Fi微控制器芯片,它具有内置的TCP/IP网络协议栈,可以提供任何微控制器访问到Wi-Fi网络的能力。 主要特点: 价格优势: 相对于其它Wi-Fi芯片,ESP8266的价格较低,使得它非常适合…

Git基本操作(超详细)

文章目录 创建Git本地仓库配置Git配置命令查看是否配置成功重置配置 工作区、暂存区、版本库添加文件--场景一概述实例操作 查看.git文件添加文件--场景二修改文件版本回退撤销修改情况⼀:对于工作区的代码,还没有 add情况⼆:已经 add &#…

老卫带你学---分布式系统(1)

概念 分布式系统就是一组协作计算机系统,通过网络通信来完成一系列连贯任务 其特点在于 parallelism并行性,cpu等计算资源可以并行计算toleration fault容错性,即使有一台设备出现问题,也不会影响整个系统的功能physical isola…

解释 C++ 中的多态性,以及如何实现运行时多态性?

解释 C 中的多态性,以及如何实现运行时多态性? 在C中,多态性是指对象在不同情况下表现出不同的行为的能力。这意味着通过相同的接口可以调用不同类型的对象,并且会根据对象的实际类型来执行相应的操作。C中的多态性通过虚函数来实…

大白话说说redux

redux的3个重要概念 store 就是用来存放应用的各种状态的action 就是用来描述应用发生了什么动作的,注意理解他是对动作的描述reducer 就是用来处理应用的动作,并且决定怎么去更新应用存放在store里面的状态。 redux的3个原则 应用的所有状态存储为re…