背景
我们页面的某一块可以将一个object结构展示在页面上(table结构),供用户查看、编辑。大致如下:
| name | type | required | description | operations(add,edit,delete等按钮) |
|---|---|---|---|---|
| obj | object | true | level1的节点 | add,edit,delete |
| sub1 | string | true | level2的节点 | edit,delete |
| sub2 | string | true | level2的节点 | edit,delete |
原本在obj层级后面点击add按钮,会在sub2的下面新增一行供用户填写。但是,最近用户有一个比较“奇葩”的需求,需要在子节点支持添加有序的兄弟节点(即sub1和sub2的operations也要支持add操作),sub1后面点击add按钮之后,会在sub1和sub2之间插入一行供用户填写,并且用户保存之后就是按照这个顺序。【注意是对象结构,不是数组结构,如果是数组结构也不需要考虑这么多了】
分析
我们知道js对象是一个散列表(hash表),它是无序的。并且前台目前没有LinkedHashMap等有序结构。本着尽量少修改代码的原则,这个问题我们分析一定是要修改前台的,那怎么做才能只修改前台呢?通过分析和实际测试,如果保存的时候发送给后台的顺序结构和看起来一样,那么后台无需修改。
在前台需要解决两个“有序问题”:一是页面上看起来有序,二是保存的时候发送给后台的顺序要和页面上看起来一样。
解决
上述的第一个问题比较好解决,原本我们的sub1和sub2都是另外新增的属性addTime来sort的,现在换成index:我们在对象数组中的每个对象加上index标识当前的插入顺序(代码中需要维护这个index逻辑,比如在index0和index1中间插入,则插入的index为1,原先的index1需要变成index2
1、传给后台的数组对象由后台根据index来进行排序)。
第二个问题是在保存的时候,按照index的顺序重新构建对象,比如保存的时候,sub1的index是0,sub2的index是2,它们之间插入了一个sub_ins,index是1,重新构建一个对象:按照sub1,sub_ins,sub2的顺序。为什么这么做,下面会解释。
对象输出顺序的规则
JSON.stringify()
Object.keys()
Object.entries
Object.values
for...in循环
Object.getOwnPropertyNames
Reflect.ownKeys【除此以外,上面的其他API均会将Symbol类型的属性过滤掉】
这些方法都不能保证我们看见的顺序。
1、声明变量keys值为一个空列表(List类型)
2、把每个Number类型的属性,按数值大小升序排序,并依次添加到keys中
3、把每个String类型的属性,按创建时间升序排序,并依次添加到keys中
4、把每个Symbol类型的属性,按创建时间升序排序,并依次添加到keys中
5、将keys返回(return keys)
注意:因为for...in可遍历原型链上的可枚举属性,测试下:
var s = Symbol()
var obj = {
1: 1,
2: 2,
c: 'c',
a: 'a',
b: 'b',
3: 3
}
obj.__proto__ = {
5: 5,
4: 4,
d: 'd',
c: 'c'
}
for (const key in obj) {
console.log(key, obj[key])
}
输出:
1 1
2 2
3 3
c c
a a
b b
4 4
5 5
d d
可以看到,for...in是先遍历自身属性,然后遍历原型链上的属性,并且对自身和原型链属性各自应用上述规则。需要注意的是自身和父元素的同名属性,原型链上的同名属性会被忽略。
问题
一是维护起来确实比较麻烦,二是如果用户填写的不是类似'sub1'这种,而是'1'、'2'这种数字,顺序规则会不太一样,当然,如果不允许直接填写数字就不会有这样的问题。
参考
Sorting object property by values
5分钟彻底理解Object.keys
js能够保证object属性的输出顺序吗?
SJ9011: Chrome Opera 中 for-in 语句遍历出对象属性的顺序与定义的不同
for...in... 和 Object.keys(), JSON.stringify() 顺序