用JavaScript我们也能愉快的完成数据转换

1,009 阅读5分钟

banner.jpg

零、写在前面

之前写了一篇 《TypeScript装饰器之我们是这么处理项目数据转换的》,有很多朋友私信和评论说,如果没有 TypeScript装饰器,纯 JavaScript 有没有什么好的数据转换的方案呢?

很遗憾,还真有,虽然没有 TypeScript 那么优雅,但是也足够好用。

这里用到了 Getter/Setter,以及 Object 原型链相关的知识。

一、假设需求

1. 后端返回的数据

这里我们先假设从后端来了个 JSON 长这样:

{
  id: 1,
  nickname: 'Hamm',
  age: 18,
  sex: 1,
  createTime: 1707021296000,
  bio: '一些废话,前端不需要的字段'
}

其中,id createTime 是固定返回的公共属性。

2. 前端类型声明

  • 基类
class BaseEntity {
  id;
  createTime;
}
  • 用户类
class User extends BaseEntity {
  nickname;
  age;
  sex;
}

3. 转换要求

  1. 转换到类时
  • createTime 转为 Date类型;
  • 前端使用 gender 作为性别字段,且需要根据 1/0 显示男女;
  • 前端没有 bio 字段,需要过滤掉。
  1. 转换到 JSON 时
  • createTime 转回后端需要的时间戳
  • gender 还原回后段需要的 sex,并且转换为 1/0

二、实现思路

正如我们之前有篇关于 几个有关Getter/Setter的小故事 文章中提到,我们可以用 get/set 来拦截数据达到转换数据的目的:

1. 基于基类实现 fromJson 静态方法

class BaseEntity {
  id;
  createTime;

  static fromJson(json) {
    const ins = new this();
    const filteredJson = Object.keys(json).reduce((item, key) => {
      if (ins.hasOwnProperty(key)) {
        item[key] = json[key];
      }
      if (ins.__proto__.hasOwnProperty(key)) {
        item[key] = json[key];
      }
      return item;
    }, {});
    const entity = Object.assign(ins, filteredJson);
    if (entity.createTime) {
      entity.createTime = new Date(entity.createTime);
    }
    return entity;
  }
}

class UserEntity extends BaseEntity {
  nickname;
  age;
  gender;
  get sex() {
    return this.gender;
  }
  set sex(value) {
    if (value === undefined || value === null) {
      this.gender = undefined
    }else{
      this.gender = value === 1 ? '男' : '女';
    }
  }
}

const json = {
  id: 1,
  nickname: 'Hamm',
  age: 18,
  sex: 1,
  createTime: 1707021296000,
  bio: '一些废话,前端不需要的字段'
}
console.log("json", json)
const user = UserEntity.fromJson(json)
console.log("entity", user)

其中,我们通过读取 get/set 以及本身的属性,来确定哪些是直接赋值,哪些是走set方法,哪些不存在的需要忽略。

2.调试输出,美滋滋:

json {
  id: 1,
  nickname: 'Hamm',
  age: 18,
  sex: 1,
  createTime: 1707021296000,
  bio: '一些废话,前端不需要的字段'
}
entity UserEntity {
  id: 1,
  createTime: 2024-02-04T04:34:56.000Z,
  nickname: 'Hamm',
  age: 18,
  gender: '男'
}

3. 实现动态方法的 toJson

接下来,我们需要将 BaseEntity 添加一个 toJson 方法,用于将实体转换为 JSON 格式,且给 UserEntitysex getter 做一下数据转换:

class BaseEntity {
  id;
  createTime;

  static fromJson(json) {
    const ins = new this();
    const filteredJson = Object.keys(json).reduce((item, key) => {
      if (ins.hasOwnProperty(key)) {
        item[key] = json[key];
      }
      if (ins.__proto__.hasOwnProperty(key)) {
        item[key] = json[key];
      }
      return item;
    }, {});
    const entity = Object.assign(ins, filteredJson);
    if (entity.createTime) {
      entity.createTime = new Date(entity.createTime);
    }
    return entity;
  }

  toJson() {
    const proto = Object.getPrototypeOf(this);
    const ownProperties = Object.getOwnPropertyNames(this);
    const getters = Object.getOwnPropertyNames(proto).filter(key => {
      const descriptor = Object.getOwnPropertyDescriptor(proto, key);
      return descriptor && typeof descriptor.get === 'function';
    });
    const json = {}
    getters.forEach(key => {
      if (this.__proto__.hasOwnProperty(key)) {
        json[key] = this[key]
        this[key] = undefined
      }
    })
    ownProperties.forEach(key => {
      if (this.hasOwnProperty(key)) {
        json[key] = this[key]
        this[key] = undefined
      }
    })
    if (json.createTime && typeof json.createTime === 'object') {
      json.createTime = json.createTime.valueOf()
    } else {
      json.createTime = undefined
    }
    return JSON.parse(JSON.stringify(json))
  }
}

class UserEntity extends BaseEntity {
  nickname;
  age;
  gender;
  get sex() {
    if (this.gender === undefined || this.gender === null) {
      return undefined
    }
    return this.gender === '男' ? 1 : 0;
  }
  set sex(value) {
    if (value === undefined || value === null) {
      this.gender = undefined
    }else{
      this.gender = value === 1 ? '男' : '女';
    }
  }
}

const user = new UserEntity()
user.id = 1
user.nickname = "Hamm"
user.age = 18
user.gender = "男"
user.createTime = new Date()
console.log("entity", user)
console.log("json", user.toJson())

4. 继续调试输出,继续美滋滋

entity UserEntity {
  id: 1,
  createTime: 2024-10-23T19:37:07.521Z,
  nickname: 'Hamm',
  age: 18,
  gender: '男'
}
json { sex: 1, id: 1, createTime: 1729712227521, nickname: 'Hamm', age: 18 }

三、完整代码如下

class BaseEntity {
  id;
  createTime;

  static fromJson(json) {
    const ins = new this();
    const filteredJson = Object.keys(json).reduce((item, key) => {
      if (ins.hasOwnProperty(key)) {
        item[key] = json[key];
      }
      if (ins.__proto__.hasOwnProperty(key)) {
        item[key] = json[key];
      }
      return item;
    }, {});
    const entity = Object.assign(ins, filteredJson);
    if (entity.createTime) {
      entity.createTime = new Date(entity.createTime);
    }
    return entity;
  }

  toJson() {
    const proto = Object.getPrototypeOf(this);
    const ownProperties = Object.getOwnPropertyNames(this);
    const getters = Object.getOwnPropertyNames(proto).filter(key => {
      const descriptor = Object.getOwnPropertyDescriptor(proto, key);
      return descriptor && typeof descriptor.get === 'function';
    });
    const json = {}
    getters.forEach(key => {
      if (this.__proto__.hasOwnProperty(key)) {
        json[key] = this[key]
        this[key] = undefined
      }
    })
    ownProperties.forEach(key => {
      if (this.hasOwnProperty(key)) {
        json[key] = this[key]
        this[key] = undefined
      }
    })
    if (json.createTime && typeof json.createTime === 'object') {
      json.createTime = json.createTime.valueOf()
    } else {
      json.createTime = undefined
    }
    return JSON.parse(JSON.stringify(json))
  }
}

class UserEntity extends BaseEntity {
  nickname;
  age;
  gender;
  get sex() {
    if (this.gender === undefined || this.gender === null) {
      return undefined
    }
    return this.gender === '男' ? 1 : 0;
  }
  set sex(value) {
    if (value === undefined || value === null) {
      this.gender = undefined
    }else{
      this.gender = value === 1 ? '男' : '女';
    }
  }
}

const json = {
  id: 1,
  nickname: 'Hamm',
  age: 18,
  sex: 1,
  createTime: 1707021296000,
  bio: '一些废话,前端不需要的字段'
}
console.log("json",json)
const user = UserEntity.fromJson(json)
console.log("entity", user)
console.log("json", user.toJson())

四、存在问题

如上,虽然实现了需求,但还有几个问题需要处理。

1. 存在的 get/set 和属性本身的歧义

我们对 UserEntitygender 做了名为 sexget/set 方法,前端在开发过程中可能去直接错误的去操作这个属性,导致和我们定义的 gender 冲突。

解决方案: 给 sex 标记 @deprecated 来警示不要直接使用。

2. 无法通过 get/set 处理同名但不同数据类型数据转换

因为 JavaScript 的属性和 get/set 方法无法同名,所以无法通过这种方式完成相同属性名但不同类型的转换。

解决方案:通过子类重写并先调用父类的 fromJson toJson 后,手动进行一次转换。

五、总结

如上代码,我们利用了 get/set 以及 Object 原型链上的一些方法来完成了转换。

虽然稍显麻烦,但也算解决了不在外部写一些方法来转换。各有所长,实现的方式有很多种,但思路才是很好玩的东西。

当然,我们还是计划在未来拥抱装饰器来解决这些问题。

JavaScript的装饰器提案已经快发布了,后续我们将继续摸索用装饰器在JavaScript中实现类似功能。(TypeScript的装饰器我们敢用,是因为编译后的JS代码没有包含装饰器的部分,在浏览器上不会有问题。)

六、That's all

欢迎持续关注专栏,也欢迎关注我们的前端开源项目 AirPower4T

本文完结,大半夜又水了一篇,美滋滋。