前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >自定义可遍历对象 - Struct

自定义可遍历对象 - Struct

作者头像
copy_left
发布2021-12-08 10:11:21
6000
发布2021-12-08 10:11:21
举报
文章被收录于专栏:方球方球

概述

使用:

  1. proxy
  2. toJSON
  3. Symbol.iterator
  4. class

实现自定义可遍历对象

Map 对象

平常开发时经常需要做数据结构的转换映射, 例如 时间区间数据, 后台返回的是两个字段的对象 { startTime, endTime } , UI组件需要数组类型[ startTime, endTime ]。 在结构转换中,对象字段遍历的频率是比较高的。

代码语言:javascript
复制
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应该是最接近的。

代码语言:javascript
复制
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 的设置与取值没有字面量对象来的方便.

代码语言:javascript
复制
const m = new Map()
m.set('name', 'cc')
m.set('age', 24)
console.log(m.get('name')

自定义解构体

既然现有的数据结构不能满足需求,那就只能自己造一个了。

目标

  • 可直接调用或转为数组, 而不是迭代器
  • 转为JSON数据结构时无多余字段
  • 提供字段删除方法 delete

第一版 - 绑定函数

第一想法是直接为普通对象添加数组获取方法, keys values map ....

代码语言:javascript
复制
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)'
]
*/

虽然实现简单,但返回的数据包含添加的遍历方法,显然不是想要的结果。

第二版 - 自定义类

代码语言:javascript
复制
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"}
*/

简单实现,满足基本功能需求

第三版 - 迭代器

代码语言:javascript
复制
{
  ...
  [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

代码语言:javascript
复制
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 已经实现了相关方法。

最终版

代码语言:javascript
复制
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] {}
*/

最终版与第四版的区别:

  1. 修改递归对象判断条件,剔除判断字段 _isStruct
  2. 抽离_keys 字段缓存队列, 清理了内部噪声字段_keys _isStruct , 自定义的 toJSON 方法也就没必要了
  3. 将数组创建模式改为独立的方法,避免误伤 非构建数组

使用

  1. 创建
代码语言:javascript
复制
const obj = new Struct({
   name: 'c'
})
const obj2 = new Struct.create({
   name: 'd'
})
const obj3 = new Struct.createByArray([
   ['name', 'oo']
])
  1. 遍历
代码语言:javascript
复制
obj.keys().map(...)
obj.values().map(...)
obj.entries().map(...)
for(let [key, value] of obj){
  ...
}
  1. 自定义处理
代码语言:javascript
复制
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 方法

代码语言:javascript
复制
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] {}
*/


// 返回迭代器
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021/11/22 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
    • Map 对象
      • 自定义解构体
        • 目标
        • 第一版 - 绑定函数
        • 第二版 - 自定义类
        • 第三版 - 迭代器
        • 第四版 - Proxy
        • 最终版
      • 使用
        • API
        • props
      • 总结
        • 参考
          • 其他
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档