如何创建有序对象?

问题

在不把对象改成数组的情况下,给对象添加新的属性,怎么保证这个属性在遍历的时候是最后一个?

解答

以前 JavaScript 中对象的键名是无序的,后来在 ES2015 之后规定了键名的顺序。

键名数组分为三个部分:

  1. 可作为数组索引的键名(如 0, 1, 2),升序排列。
  2. 字符串索引,按创建顺序排列。
  3. Symbol 索引,按创建顺序排列。

如果只使用第二类字符串索引,那么默认的顺序就满足需求了。而如果要加上第一类索引,那就需要自己维护键名的数组了,在新增删除键名时对数组进行操作。

这个场景 Proxy 就再合适不过了。下面的例子是按键名创建顺序排序的对象,如有其他排列需求只需对 ownKeys 做文章即可。

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const obj = proxy({ b: 1, d: 1 })
obj.c = 1
obj.a = 1
delete obj.d
Object.defineProperties(obj, {
y: {
value: 1,
enumerable: false,
configurable: false,
},
x: {
value: 1,
enumerable: true,
configurable: false,
},
})
obj[Symbol(2)] = 1
obj[Symbol(1)] = 1

console.log(Object.keys(obj))
console.log(Object.getOwnPropertyNames(obj))
console.log(Object.getOwnPropertySymbols(obj))

// b, c, a, x
// b, c, a, y, x
// Symbol(2), Symbol(1)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function proxy(target) {
const keys = Reflect.ownKeys(target)

/* 添加 key */
function pushKey(key) {
const index = keys.indexOf(key)
if (!~index) keys.push(key)
}

/* 删除 key */
function deleteKey(key) {
const index = keys.indexOf(key)
if (~index) keys.splice(index, 1)
}

return new Proxy(target, {
defineProperty(target, key, descriptor) {
const result = Reflect.defineProperty(target, key, descriptor)

/* 定义属性成功 则添加 key*/
if (result) pushKey(key)

return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)

/* 删除属性成功 则删除 key */
if (result) deleteKey(key)

return result
},
ownKeys() {
return [].concat(keys)
},
})
}
  • defineProperty: 在目标上无论新增还是修改键名/键值,都会触发。
  • deleteProperty: 在目标上移除键名时,会触发。
  • ownKeys: 获取键名列表时,返回 keys

要点说明

defineProperty / set

  • handler.set 无法拦截 Object.definePropertyObject.defineProperties
  • handler.set 会拦截以代理目标为原型的对象的 set 操作。

故选择了 hanlder.defineProperty

ownKeys

该拦截器可以拦截以下操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • Reflect.ownKeys()

所以我们的 keys 应为上述四者的集合,

Object.keys()Object.OwnPropertyNames() 的子集,

Reflect.ownKeys() 等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

故在初始化时,应用 Reflect.ownKeys() 创建初始的 keys

另外该拦截器还存在约束,使用前请务必了解一下: handler.ownKeys 的约束 | MDN