概述
使用:
- proxy
- toJSON
- Symbol.iterator
- class
實現自定義可遍歷對象
Map 對象
平常開發時經常需要做數據結構的轉換映射, 例如 時間區間數據, 后臺返回的是兩個字段的對象 { startTime, endTime } , UI組件需要數組類型[ startTime, endTime ]。 在結構轉換中,對象字段遍歷的頻率是比較高的。
const obj = { name: 'cc', age: 24 }
const keys = Object.keys(obj)
// ['name', 'age']
const values = Object.values(obj)
// ['cc', 24]
const entries = Object.entries(obj)
// [['name', 'cc'], ['age', 24]]
delete obj.name
雖然ES6 提供不少對象遍歷的方法, 但始終沒有數組的方式用的順手。
那有沒有兼具對象字段取值和數組遍歷方法的方式呢? 現有ES6 標準中Map應該是最接近的。
const m = new Map([
['name', 'cc'],
['age', 24]
])
const keys = [...m.keys()]
// ['name', 'age']
const value = [...m.values()]
// ['cc', 24]
const entries = [...m.entries()]
// [['name', 'cc'], ['age', 24]]
const maps = [...m]
// [['name', 'cc'],['age', 24]]
maps.delete('name')
除了map直接通過解構轉為數組外,其他方式都需要調用對應的方法獲取迭代器,再轉為數組形式。使用上也沒有明顯的優勢,另外map 的設置與取值沒有字面量對象來的方便.
const m = new Map()
m.set('name', 'cc')
m.set('age', 24)
console.log(m.get('name')
自定義解構體
既然現有的數據結構不能滿足需求,那就只能自己造一個了。
目標
- 可直接調用或轉為數組, 而不是迭代器
- 轉為JSON數據結構時無多余字段
- 提供字段刪除方法 delete
第一版 - 綁定函數
第一想法是直接為普通對象添加數組獲取方法, keys
values
map
....
function withArr(b){
b.keys = () => Object.keys(b)
b.values = () => Object.values(b)
b.map = (cb) => Object.entries(b).map(cb)
return b
}
const b1 = withArr({
name: 'cc',
age: 24
})
console.log(
b1.keys()
)
console.log(
b1.values()
)
console.log(
b1.map(([key, value]) => `${key}: ${value}`)
)
/*
[ 'name', 'age', 'keys', 'values', 'map' ]
[
'cc',
24,
[Function (anonymous)],
[Function (anonymous)],
[Function (anonymous)]
]
[
'name: cc',
'age: 24',
'keys: () => Object.keys(b)',
'values: () => Object.values(b)',
'map: (cb) => Object.entries(b).map(cb)'
]
*/
雖然實現簡單,但返回的數據包含添加的遍歷方法,顯然不是想要的結果。
第二版 - 自定義類
class Struct{
constructor(init={}){
const _this = this
Object.entries(init).forEach(([key, value]) => {
_this[key] = value
})
return _this
}
keys(){
return Object.keys(this)
}
values(){
return Object.values(this)
}
entries(){
return Object.entries(this)
}
delete(key){
delete this[key]
}
length(){
return this.keys().length
}
}
const m = new Struct({
name: 'cc',
age: 24
})
console.log(
m.keys(),
m.values(),
m.length()
)
m.delete('age')
m.job = 'TT'
console.log(m)
console.log(JSON.stringify(m))
/*
[ 'name', 'age' ] [ 'cc', 24 ] 2
Struct { name: 'cc', job: 'TT' }
{"name":"cc","job":"TT"}
*/
簡單實現,滿足基本功能需求
第三版 - 迭代器
{
...
[Symbol.iterator](){
let _index = 0
const _this = this
const _keys = this.keys()
return {
next(){
const key = _keys[_index]
const done = _index >= _keys.length
_index++
return done ? { done } : { done, value: [key, _this[key]] }
},
return(){
return { done: true }
}
}
}
}
for([key, value] of m){
console.log(key, value)
}
/*
name cc
age 24
*/
添加迭代器, 支持 for 循環
第四版 - Proxy
function isObj(b){
return Object.prototype.toString.call(b) === '[object Object]'
}
class Struct {
static create(d){
return new Struct(d)
}
constructor(d = {}, props={deep: false}){
// 內部緩存字段列表
this._keys = []
this._isStruct = true
const _this = this
// 代理對象,實現惰性創建Struct對象
const _o = new Proxy(this, {
get(target, propKey, receiver){
const end = Reflect.get(target, propKey, receiver)
if(props.deep && isObj(end) && !end._isStruct){
return this[propKey] = Struct.create(end)
}
return end
},
set(target, propKey, value, receiver){
if(!_this._keys.includes(propKey)){
_this._keys.push(propKey)
}
return Reflect.set(target, propKey, value, receiver)
}
})
if(Array.isArray(d)){
d.forEach(([key, value]) => _o[key] = value)
}
if(isObj(d)){
Object.entries(d).forEach(([key, value]) => _o[key] = value)
}
return _o
}
has(key){
return this._keys.includes(key)
}
delete(key){
const index = this._keys.findIndex(_key => _key === key)
if(index !== -1){
const key = this._keys.splice(index, 1)[0]
Reflect.deleteProperty(this, key)
}
}
length(){
return this.keys.length
}
keys(){
return [...this._keys]
}
values(){
return this._keys.map(key => this[key])
}
toJSON(){
const _this = this
return this._keys.reduce((o, key) => ({...o, [key]: _this[key]}), {})
}
[Symbol.iterator](){
let index = 0
const _this = this
return {
next(){
const key = _this._keys[index]
const done = index >= _this._keys.length
index++
return done ? { done } : { done, value: [key, _this[key]] }
},
return(){
return { done: true }
}
}
}
}
const obj = new Struct({
name: 'c',
age: 24,
child: {
name: 'd',
age: 2
}
}, {deep: true})
console.log(obj.keys())
console.log(obj.child.keys())
console.log(JSON.stringify(obj))
實際使用的時,數據結構一般是多層嵌套的,我們可能需要操作的是一個或多個對象結構。 這里通過proxy 代理攔截判斷值類型,惰性轉換為Struct
類型。 這里使用_keys
緩存字段順序,_isStruct
防止重復包裝.
這一版的不足在加入了不必要的噪聲_keys
_isStruct
轉為json會出現不必要的字段,所以通過自定義toJSON
屏蔽噪聲。
但是Object.keys() 等方法依然將查詢出相關字段,這里和MDN的介紹有所出入, 按照MDN的說法, keys
等方法的結果應該與 for...in
一致, 但實際情況是for...in
使用到了迭代器, 而keys
方法并沒有。 如果只是使用的來說,有沒有Object 的遍歷方法沒那么重要,畢竟Struct
已經實現了相關方法。
最終版
function isObj(b){
return Object.prototype.toString.call(b) === '[object Object]'
}
class Struct {
/**
* 搜集keys緩存
* 這里將 _keyMap 作為獨立靜態屬性的目的
* 1. 防止Object.keys()時返回多余的字段
* 2. WeakMap 內屬性在對象未引用后將自動回收
*/
static _keyMap = new WeakMap()
static create(d, props){
return new Struct(d, props)
}
/**
* 數組映射對象生成器
* @param { [][key, value] } d 初始對象
* @param { Object } props 配置屬性
* @returns Struct
*/
static createByArray(d, props){
return Struct.create(Object.fromEntries(d), props)
}
constructor(d = {}, props={deep: false, setting: undefined, getting: undefined}){
const _o = new Proxy(this, {
get(target, propKey, receiver){
const end = Reflect.get(target, propKey, receiver)
// 惰性求值,將對象轉為Struct
if(props.deep && isObj(end) && !(end instanceof Struct)){
return this[propKey] = Struct.create(end, props)
}
return props.getting ? props.getting(end, target, propKey, receiver) : end
},
set(target, propKey, value, receiver){
const _keys = Struct._keyMap.get(_o)
// 收集字段
if(!_keys.includes(propKey)){
_keys.push(propKey)
}
return props.setting ? props.setting(target, propKey, value, receiver) : Reflect.set(target, propKey, value, receiver)
}
})
Struct._keyMap.set(_o, [])
if(isObj(d)){
Object.entries(d).forEach(([key, value]) => _o[key] = value)
}
return _o
}
has(key){
return this._keys.includes(key)
}
delete(key){
const index = this._keys.findIndex(_key => _key === key)
if(index !== -1){
const key = this._keys.splice(index, 1)[0]
Reflect.deleteProperty(this, key)
}
}
length(){
return this.keys().length
}
keys(){
return [...Struct._keyMap.get(this)]
}
values(){
return this.keys().map(key => this[key])
}
// toJSON(){
// const _this = this
// const _keys = this.keys()
// return _keys.reduce((o, key) => ({...o, [key]: _this[key]}), {})
// }
[Symbol.iterator](){
let _index = 0
const _this = this
const _keys = this.keys()
return {
next(){
const key = _keys[index]
const done = _index >= _keys.length
_index++
return done ? { done } : { done, value: [key, _this[key]] }
},
return(){
return { done: true }
}
}
}
}
const obj = Struct.create({
name: 'cc',
age: 24,
child: {
name: 'dd',
age: 2
}
}, {deep: true})
console.log(obj.keys())
console.log(obj.values())
console.log(obj.child.keys())
console.log(JSON.stringify(obj))
console.log(obj.pack)
console.log(obj.pack.keys())
/*
[ 'name', 'age', 'pack', 'child' ]
[ 'cc', 24, [ '1', '2', '3', '4' ], Struct { name: 'dd', age: 2 } ]
[ 'name', 'age' ]
{"name":"cc","age":24,"pack":["1","2","3","4"],"child":{"name":"dd","age":2}}
[ '1', '2', '3', '4' ]
Object [Array Iterator] {}
*/
最終版與第四版的區別:
- 修改遞歸對象判斷條件,剔除判斷字段
_isStruct
- 抽離
_keys
字段緩存隊列, 清理了內部噪聲字段_keys
_isStruct
, 自定義的toJSON
方法也就沒必要了 - 將數組創建模式改為獨立的方法,避免
誤傷
非構建數組
使用
- 創建
const obj = new Struct({
name: 'c'
})
const obj2 = new Struct.create({
name: 'd'
})
const obj3 = new Struct.createByArray([
['name', 'oo']
])
- 遍歷
obj.keys().map(...)
obj.values().map(...)
obj.entries().map(...)
for(let [key, value] of obj){
...
}
- 自定義處理
const obj = Struct.create({
name: 'cc',
age: 24,
job: 'IT'
}, {
deep: true,
getting({
end, target, propKey, receiver
}){
if(isUndefined(end) && isNumber(parseFloat(propKey))){
propKey = receiver.keys()[propKey]
}
return Reflect.get(target, propKey, receiver)
}
})
console.log(obj[0])
// 'cc'
API
- keys() 字段列表
- values() 值列表
- entries() key-value 列表
- has(key) 是否含有某屬性
- delete(key) 刪除屬性
- length() 屬性數量
props
- deep 是否使用惰性遞歸
- setting 自定義setting鉤子
- getting 自定義getting鉤子
總結
這里的Struct
算作是一種ES6 語法的組合嘗試, 通過組合控制對象的執行行為。
對比Go
內的一些上層數據結構也是使用類似的方式,通過組合底層結構和接口構建而來。
簡單體會對于面向對象的不同理解,之前使用面向對象時的目的是構建一個實際事物的數據映射。
其實也可以純粹的將對象總結為數據結構, 通過類類的方式創建數據解構, 使用函數式構建數據結構之間的關系.
參考
其他
數組可是有keys
values
entries
方法
const arr = [1,2,3]
console.log(arr.keys())
console.log(arr.values())
console.log(arr.entries())
/*
Object [Array Iterator] {}
Object [Array Iterator] {}
Object [Array Iterator] {}
*/
// 返回迭代器