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

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

ECMAScript6 中 Set 和 Map 的代理方法


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 {
        // ...
      }
    }
  })
}

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 的读取和设置操作,那么我们就可以在拦截中执行副作用函数、从而产生数据变化的“副效果”。

我们可以这样实现:

<p></p>
<script>
// 副作用函数容器
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 = {text:'改变数据则响应式变化:'}
// 数据 data 对应的副作用函数
function effect(){
  const p = document.getElementsByTagName('p')[0];
  p.innerText = data.text;
}
const dataProxy = createProxy(data);
effect();
// 改变数据,则副效果自动执行
// 从而驱动实现数据响应式
setInterval(
  ()=>{
    dataProxy.text = dataProxy.text+'*';
  },2000
)
</script>

这个案例中我们使用定时器每隔 2 秒将数据在原数据上增加一个 “*”。数据改变后,可以看到数据对应的效果也自动改变,效果如下:

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

编写中,尚未完成…

3. forEach方法的处理

4. 迭代器方法的处理

编写中,尚未完成…

目录
相关文章
|
25天前
|
存储 NoSQL 关系型数据库
Redis 集合(Set)
10月更文挑战第17天
36 5
|
26天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
36 6
|
26天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
32 2
|
11天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
11天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
11天前
|
Java 开发者
|
11天前
|
JavaScript 前端开发 Java
除了 JavaScript,还有哪些编程语言支持 Set 类型
【10月更文挑战第30天】这些编程语言中的 `Set` 类型虽然在语法和具体实现细节上有所不同,但都提供了类似的集合操作功能,方便开发者在不同的编程场景中处理集合相关的数据和逻辑。
|
26天前
|
Java 开发者
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素。通过哈希表和红黑树两种模式,Set能够高效地识别并拒绝重复元素的入侵,确保集合的纯净。无论是HashSet还是TreeSet,都能在不同的场景下发挥出色的表现,成为开发者手中的利器。
26 2
|
2月前
|
Go 定位技术 索引
Go 语言Map(集合) | 19
Go 语言Map(集合) | 19
|
2月前
|
存储 前端开发 API
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
该文章详细介绍了ES6中Set和Map数据结构的特性和使用方法,并探讨了它们在前端开发中的具体应用,包括如何利用这些数据结构来解决常见的编程问题。
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用