JavaScript 中如何代理 Set(集合) 和 Map(映射)

news/2025/3/29 8:00:33/
ECMAScript6 中 Set 和 Map 的代理方法

上一节:《JavaScript 中如何代理数组 | 下一节:《JavaScript 中的反射(Reflect)原理与应用

今日正在编写中,未完待续…

jcLee95
邮箱 :291148484@163.com
CSDN 主页https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343
本文地址https://blog.csdn.net/qq_28550263/article/details/128439767

目 录

1. 概述

  • 1.1 与代理普通对象的区别
  • 1.2 Set (集合)的原型属性和方法
  • 1.3 Map (映射)的原型属性和方法

2. 代理的实现思路

  • 2.1 从一个错误说起
  • 2.2 代理一般原型方法
  • 2.3 响应式代理原理与实现
    • 2.3.1 副作用函数与响应式数据
    • 2.3.2 实现响应式的基本思路
    • 2.3.3 为 Set/Map 代理建立响应联系

3. forEach方法的处理

4. 迭代器方法的处理


1. 概述

1.1 与代理普通对象的区别

Set 和 Map 与普通对象的一个区别是他们具有普通对象没有的属性和方法。代理 Set 和 代理 Map 的思路基本一致,只不过比代理普通对象要麻烦了许多。关键在于你需要完成 Set 和 Map 各具体原型方法的代理实现。因此,在实现对 Set 和 Map 的代理之前,我们需要大略过一下 Set 和 Map 的原型属性与方法。

1.2 Set (集合)的原型属性和方法

原型方法/属性描述
Set.prototype.add()如果 Set 对象中没有具有相同值的元素,则 add() 方法将插入一个具有指定值的新元素到 Set 对象中。
Set.prototype.clear()该方法移除 Set 对象中所有元素。
Set.prototype.delete()该方法从 Set 对象中删除指定的值(如果该值在 Set 中)。
Set.prototype.entries()该方法返回一个新的迭代器对象,这个对象包含的元素是类似 [value, value] 形式的数组,value 是集合对象中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序。
Set.prototype.forEach()该方法对 Set 对象中的每个值按插入顺序执行一次提供的函数。
Set.prototype.has()该方法返回一个布尔值来指示对应的值是否存在于 Set 对象中。
Set.prototype.keys()该方法是 values() 方法的别名。
Set.prototype.values()该方法返回一个新的迭代器对象,该对象按插入顺序包含 Set 对象中每个元素的值。
Set.prototype[@@iterator]()@@iterator 属性的初始值和 values 属性的初始值是同一个函数。
Set.prototype.size该属性将会返回 Set 对象中(唯一的)元素的个数。
get Set[@@species]该访问器属性返回Set的构造函数。

1.3 Map (映射)的原型属性和方法

温馨提示:Map 在编程语言中不叫 地图,而是 映射。

原型方法/属性描述
Map.prototype.clear()该方法会移除 Map 对象中的所有元素。
Map.prototype.delete()该法用于移除 Map 对象中指定的元素。
Map.prototype.entries()该方法返回一个新的迭代器对象,其中包含 Map 对象中按插入顺序排列的每个元素的 [key, value] 对。
Map.prototype.forEach()该方法按照插入顺序依次对 Map 中每个键/值对执行一次给定的函数。
Map.prototype.get()该方法从 Map 对象返回指定的元素。
Map.prototype.has()该方法返回一个布尔值,指示具有指定键的元素是否存在。
Map.prototype.keys()该返回一个引用的迭代器对象。它包含按照顺序插入 Map 对象中每个元素的 key 值。
Map.prototype.set()该方法为 Map 对象添加或更新一个指定了键(key)和值(value)的(新)键值对。
Map.prototype.values()该方法返回一个新的迭代器对象。它包含按顺序插入 Map 对象中每个元素的 value 值。
Map.prototype[@@iterator]()@@iterator 属性的初始值与 entries 属性的初始值是同一个函数对象。
Map.prototype.size该属性返回 Map 对象的成员数量。
get Map[@@species]该访问器属性会返回一个 Map 构造函数。

2. 代理的实现思路

2.1 从一个错误说起

我们想代理一个 Set 对象实例,于是这样做了:

const s = new Set([1, 2]);
const s_proxy = new Proxy(s, {})

接着我们尝试通过代理对象 s_proxy 获取 s 的 size:

s_proxy.size

这时错误产生了:
在这里插入图片描述
上面的报错意思是说:在不兼容的接收器 #<Set >上调用了方法 get Set.prototype.size
这表明,我们直接想要不定义任何东西使用Proxy来代理 Set,首先在 size 属性上就没有成功。为什么的?

这是因为 Set.prototype.size 是一个 get 访问器属性(它的 set 存储器属性未定义)。当调一个Set用该属性时:

因此我们需要在船舰代理对象时增加 getter 拦截方法,并使访问器属性 size 的 getter 函数执行时, this 指向被代理的 Set 对象实例,而不是 代理对象自己:

const s = new Set([1, 2]);
const s_proxy = new Proxy(s, {get(target, key, receiver) {// 对于 size 属性if(key === 'size') {// 返回 target['size'], target 表示被代理的目标对象return Reflect.get(target, key, target)}// 其它属性else{// 仍返回 receiver[key]// receiver表示 Proxy 或者继承 Proxy 的对象return Reflect.get(target, key, receiver)}}
})

【注】:
Reflect.get 方法就如同属性访问器语法(target[propertyKey]) 从对象中读取属性,只不过 Reflect.get 方法 是通过一个函数执行来操作的:

Reflect.get(target, propertyKey[, receiver])
  • target: 需要取值的目标对象
  • propertyKey: 需要获取的值的键值
  • receiver: 如果target对象中指定了getter,receiver则为getter调用时的this值
    返回属性的值。

现在,我们的代理对象上访问 size 就不会报错了:
在这里插入图片描述

2.2 代理一般原型方法

很快你就意识到,在代理对象 s_proxy 上同样无法使用 add()、clear()、delete() 等等 Set 的原型方法。

很显然,这个问题和 访问器属性 size 不那么一样:
由于 size 是一个Set实例上的访问器属性,我们要使用 s_proxy.size 只需要通过修改 receiver 来改变访问器 getter 函数的 this 指向。
但是 add() 这些函数不像调用 s_proxy.size 会自动执行 getter,不论如何修改 receiver ,访问 s_proxy.add 对应的 add() 方法并没有执行(而访问 s_proxy.set 对应的 get(...)方法会执行 ),因此这些方法执行时的 this 仍然指向着 代理对象 s_proxy而不是被它所代理的 s
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
因此我们的目标还是在像一个办法,当执行这些方法是将方法与原始数据对象target绑定。

const s = new Set([1, 2]);
const s_proxy = new Proxy(s, {get(target, key, receiver) {if(key === 'size') {return Reflect.get(target, key, target)}else{return target[key].bind(target);}}
})

在这里插入图片描述

当调用的是方法时,target[key] 返回就是代理对象上名为 key 的属性,如果是方法则返回的是方法。比如,访问代理对象上的 add 方法时:

s_proxy.add(3)

target[key] 返回的是 ƒ add() { [native code] },也就是 add 函数。

因此,target[key].bind(target);也就是将这个 add() 函数绑定到 target(被代理对象)上。换句话说就是将 target[key] 返回的函数的 this 指向原始对象 target

【注】
Function.prototype.bind()方法
该方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
其语法格式为:

function.bind(thisArg[, arg1[, arg2[, ...]]])
  • thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。
  • arg1, arg2, …:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

可知,不仅是 add 方法,其它的方法如 delete、clear 等等,一旦访问的时候后悔这样绑定到原始对象上执行。

不仅是 Set,Map也可以通过类似的思路完成代理。

为了方便为 Set/Map 类型数据创建代理,我们可将将创建代理的逻辑封装成为一个函数:

function createProxy(obj) {return new Proxy(obj, {get(target, key, receiver) {if(key === 'size') {return Reflect.get(target, key, target)}else{return targe0t[key].bind(target);}}})
}

2.3 响应式代理原理与实现

2.3.1 副作用函数与响应式数据

关于响应式数据与副作用函数更详细的介绍请参考博文《响应式数据基本原理》

先介绍一个概念——副作用函数。
所谓副作用函数,指的是当其调用执行后将会直接或者间接地影响到其它函数执行地函数。也就是说,一个函数影响到其它函数地执行,称之为副作用。

副作用函数是我们为了实现响应式数据而提出来的。响应式的目标也就是,副作用函数所依赖的某些变量(称响应式数据)发生改变时,副作用函数自动地随着这些数据的改变而重新执行。这也就意味着,一旦数据改变,由数据驱动的副作用函数对应的效果都将发生相应的变化。

2.3.2 实现响应式的基本思路

举一个例子,由一个数据 data,和一个名为effect的函数:

const data = {p:'我是p标签的文本'}function effect(){const p = document.getElementsByTagName('p')[0];p.innerText = data.p;
}

如果说 data 是一个响应式数据,那么在这里我们希望一旦 data 的内容发生改变,则函数 effect (副作用函数)自动执行。

这个问题的关键在于——我们怎么知道数据 data 什么时候会发生改变。毕竟我们还不能定时去读取它的值做比较,这样不仅消耗的资源多,而且 data 仍然称不上响应式,因为定时读取必然有时间间距。

既然不能通过比较获知数据是否发生改变,那我们就只能从引起改变的 源头 进行入手。只要我们能够 拦截 对数据 data 的读取和设置操作,那么我们就可以在拦截中执行副作用函数、从而产生数据变化的“副效果”。

我们可以这样实现:

// 副作用函数容器
const container = new Set();function createProxy(data){return new Proxy(data, {// 读取属性(方法)时,记录相应副作用函数get(target, key) {container.add(effect) // 记录副作用return target[key];   // 返回调用的属性或者方法(属性值可能为方法)},set(target, key, value) {target[key] = value;// 执行所有存储的副作用函数,产生副作用container.forEach((func)=>{func();})return true;}})
}// 数据
const data = {p:'我是p标签的文本'}
// 数据 data 对应的副作用函数
function effect(){const p = document.getElementsByTagName('p')[0];p.innerText = data.p;
}const dataProxy = createProxy(data);

2.3.3 为 Set/Map 代理建立响应联系

编写中,尚未完成…

3. forEach方法的处理

4. 迭代器方法的处理

编写中,尚未完成…


http://www.ppmy.cn/news/7307.html

相关文章

常用的工具网站(网址 + 效果图)

一&#xff0c;阿里图标库 https://www.iconfont.cn/?spma313x.7781069.1998910419.d4d0a486a 二&#xff0c;AI人工智能图片放大 https://bigjpg.com/zh 三&#xff0c;一个有情怀的免费PPT模板下载网站&#xff01; https://www.ypppt.com/ 四&#xff0c;照片抠图…

c语言操作符(上)

前言 &#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f; c语言初阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>: 讲解c语言中有关操作符的知识. 金句分享: 最慢的步伐…

【技巧】vs2019调试

文章目录一.计算机Bug的由来二.调试1.调试的定义2.调试的基本步骤2.release和debug的区别3.调试的快捷键4.其他功能的快捷键1、窗口快捷键2、项目功能快捷键**3、查找相关快捷键4、代码快捷键5、编辑快捷键5.经典例题6.写代码的好习惯举例&#xff1a;模拟实现strcpy7.const的作…

数据库,计算机网络、操作系统刷题笔记20

数据库&#xff0c;计算机网络、操作系统刷题笔记20 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle…

python实现FORCAST线性回归预测值函数

FORCAST线性回归预测值函数是部分软件里面的函数&#xff0c;不同软件函数名称有差异。 以下内容说明节选自某软件 FORCAST该函数的说明如下&#xff1a; FORCAST(X,N)&#xff1a;为X的N周期线性回归预测值。 注&#xff1a; 1、N包含当前k线。 2、N为有效值&#xff0c;但当前…

【endnote学习】为什么引用文献时期刊名没有显示为缩写名形式

为什么引用文献时期刊名没有显示为缩写名形式问题描述问题解决问题描述 在引用文献时&#xff0c;发现有个别文献引用信息中期刊名没有显示为缩写形式。比如(选择显示格式为AIChE): 引用信息里&#xff0c;期刊名“Physical review B”没有自动显示为缩写名。 出现这种情况有…

2022年iFLYTEKA.I.开发者大赛疫情微博情绪识别挑战赛

自然语言技术 零基础入门NLP - 新闻文本分类 基于word2vec的word相似度 疫情微博情绪识别挑战赛自然语言技术背景一、赛事任务二、使用步骤1.README2.数据下载3.模型训练及保存4.模型预测5.比赛结果背景 疫情发生对人们生活生产的方方面面产生了重要影响&#xff0c;并引发了…

黑客比程序员高在哪里?

黑客其实和一般的程序员一样&#xff0c;但是他们的关注点不一样。黑客关注的是如何破坏&#xff0c;通过这些有创造性的破坏来获取利益&#xff0c;展现自己的能力。而程序员关注的是如何创造&#xff0c;通过创造来获取利益&#xff0c;展现自己的能力。 就如同一个硬币的两…