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. 迭代器方法的处理

编写中,尚未完成…

目录
相关文章
|
11月前
|
Go
go语言中遍历映射(map)
go语言中遍历映射(map)
237 8
|
3月前
|
存储 缓存 JavaScript
Set和Map有什么区别?
Set和Map有什么区别?
258 1
|
4月前
|
存储 JavaScript 前端开发
for...of循环在遍历Set和Map时的注意事项有哪些?
for...of循环在遍历Set和Map时的注意事项有哪些?
263 121
|
7月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
176 2
|
4月前
|
存储 C++ 容器
unordered_set、unordered_multiset、unordered_map、unordered_multimap的介绍及使用
unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。在unordered_set中,元素的值同时也是唯一地标识它的key。在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。它的迭代器至少是前向迭代器。前向迭代器的特性。
184 0
|
4月前
|
编译器 C++ 容器
用一棵红黑树同时封装出map和set
再完成上面的代码后,我们的底层代码已经完成了,这时候已经是一个底层STL的红黑树了,已经已符合库里面的要求了,这时候我们是需要给他穿上对应的“衣服”,比如穿上set的“衣服”,那么这个穿上set的“衣服”,那么他就符合库里面set的要求了,同样map一样,这时候我们就需要实现set与map了。因此,上层容器map需要向底层红黑树提供一个仿函数,用于获取T当中的键值Key,这样一来,当底层红黑树当中需要比较两个结点的键值时,就可以通过这个仿函数来获取T当中的键值了。我们就可以使用仿函数了。
47 0
|
4月前
|
存储 编译器 容器
set、map、multiset、multimap的介绍及使用以及区别,注意事项
set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。set默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。
201 0
|
8月前
|
编译器 容器
哈希表模拟封装unordered_map和unordered_set
哈希表模拟封装unordered_map和unordered_set
|
8月前
|
编译器 测试技术 计算机视觉
红黑树模拟封装map和set
红黑树模拟封装map和set

热门文章

最新文章