借助 Proxy 实现一个 DefaultDict

3.4k words

这里的 DefaultDict 指的是类似于 Python 中的 defaultdict 的一种类。其基本特点就是当某个属性不存在于该对象中时,该对象会自动为这个属性创建一个默认值。这个默认值是由用户在创建 DefaultDict 时指定的。

举个例子,现在需要一个对象,如果某个属性不在这个对象时,在为这个属性赋值为 0.

1
2
3
4
5
6
7
8
9
10
11
12
const words = ['hello', 'hello', 'world', 'please', 'say', 'say', 'say']
const defaultDict = defaultDictFactory({}, () => 0)
for (const word of words) {
defaultDict[word]++
}
console.log(defaultDict)
/*
{ hello: 3,
world: 2,
please: 2,
say: 4}
*/

这个例子其实就是非常简单的一个统计单词数量的一个例子,如果不使用 defaultDict, 那么估计就会这么写:

1
2
3
4
5
const words = ['hello', 'hello', 'world', 'please', 'say', 'say', 'say']
const defaultDict = {}
for (const word of words) {
defaultDict[word] = defaultDict[word] == null ? 1 : defaultDict[word] + 1
}

你觉得那个更美观或实用一点呢? 这个其实见仁见智,至少前者确实带来了一些便利。

回到正题,这里开始讲怎么去实现它。

Proxy 对象

实现的方法很多,不一定必须要 Proxy 对象,但它最为 ES6 推出的一个类,有必要去尝试一下。简单的说,Proxy 可以改变对象的一些默认行为,包括增删改查。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = new Proxy({}, {
get: function (target, prop) {
console.log(target, prop)
return target[prop]
}
})

obj.foo = 1
console.log(obj['bar'])
/*
{ foo: 1 } 'bar'
undefined
*/

可见,Proxy 对对象属性的获取进行了一点修改。在这里 obj.foo = 1 不属于对 foo 属性的获取,而是对 foo 属性的赋值(set),所以在执行 obj.foo = 1 时,get: function (target, prop) { ... } 并没有被执行。

更多的可以参考 ECMAScript 6 入门: Proxy

实现

这里先定义个 handler,也就是对对象的属性获取进行拦截。那么这里需要思考,需要哪些参数呢?

首先一个,如何确认默认值,那么默认值的产生需要用户定义。所以我们需要一个 defaultFactory 函数用于生成默认值,这里使用了函数,为了有更多的可操作空间。

另外,如何判断一个属性在不在这个对象中呢?大部分用 'foo' in obj 判断,但极少时候用其它方式。所以这里就设置一个默认操作,如果用户没有指定,我们就用 in 操作符判断属性是否存在。

这么到这里可以基本实现了 defaultDict:

1
2
3
4
5
6
7
8
9
10
11
12
13
function defaultDictFactory (initials, defaultFactory, validator) {
if (validator == null) {
validator = (prop, object) => prop in object
}
return new Proxy(initials, {
get: function (target, prop) {
if (!validator(prop, target)) {
target[prop] = defaultFactory(target, prop)
}
return target[prop]
}
})
}

defaultDictFactory 作为一个工厂函数,专门生产 defaultDict。本来我想用 class 实现,不过遇到了瓶颈,所以改为工厂模式。
initials 为初始对象,因为用户或许会将一个非空对象转化为 defaultDict
defaultFactory 函数用于生产默认值。
validator 判断属性是否存在,可以有用户自定义判断属性是否存在的规则。

但为了安全起见,可以加一些对参数的检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function defaultDictFactory (initials, defaultFactory, validator) {
if (defaultFactory == null || typeof defaultFactory !== 'function') {
throw new TypeError(`defaultFactory must be a function`)
}
if (validator != null && typeof validator !== 'function') {
throw new TypeError(`validator must be a function`)
} else if (validator == null) {
validator = (prop, object) => prop in object
}
return new Proxy(initials, {
get: function (target, prop) {
if (!validator(prop, target)) {
target[prop] = defaultFactory(prop, target)
}
return target[prop]
}
})
}

这样子基本就完成了 defaultDictFactory 的定义。

Example

这里还是以统计单词为例,增加 1 个要求: 单词的默认值为单词的长度

那么只需要设置 defaultFactory:

1
2
3
4
5
6
7
8
9
const words = ['hello', 'hello', 'world', 'please', 'say', 'say', 'say']
const defaultDict = defaultDictFactory({}, (prop) => prop.length)
for (const word of words) {
defaultDict[word]++
}
console.log(Object.entries(defaultDict))
/*
[ [ 'hello', 7 ], [ 'world', 6 ], [ 'please', 7 ], [ 'say', 6 ] ]
*/

其它

建立 defaultDict 的最初想法一方面来自于 Python 的 defaultdict,因为这确实挺方便的。另一方面则来自于对平时刷题时经常遇到的 obj.foo = obj.foo == null ? 1 : obj.foo + 1 的这种写法觉得不美观的写法,所以试图改变一下。

参考

  1. http://es6.ruanyifeng.com/#docs/proxy
  2. https://gist.github.com/thomasboyt/5987633