美文网首页
自定义可遍历对象 - Struct

自定义可遍历对象 - Struct

作者: copyLeft | 来源:发表于2021-11-22 17:05 被阅读0次

    概述

    使用:

    1. proxy
    2. toJSON
    3. Symbol.iterator
    4. 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] {}
    */
    

    最终版与第四版的区别:

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

    使用

    1. 创建
    const obj = new Struct({
       name: 'c'
    })
    const obj2 = new Struct.create({
       name: 'd'
    })
    const obj3 = new Struct.createByArray([
       ['name', 'oo']
    ])
    
    1. 遍历
    obj.keys().map(...)
    obj.values().map(...)
    obj.entries().map(...)
    for(let [key, value] of obj){
      ...
    }
    
    1. 自定义处理
    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] {}
    */
    
    
    // 返回迭代器
    

    相关文章

      网友评论

          本文标题:自定义可遍历对象 - Struct

          本文链接:https://www.haomeiwen.com/subject/nvpbtrtx.html