零、写在前面
之前写了一篇 《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. 转换要求
- 转换到类时
createTime
转为 Date类型;- 前端使用
gender
作为性别字段,且需要根据1/0
显示男女; - 前端没有
bio
字段,需要过滤掉。
- 转换到 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 格式,且给 UserEntity
的 sex 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
和属性本身的歧义
我们对 UserEntity
的 gender
做了名为 sex
的 get/set
方法,前端在开发过程中可能去直接错误的去操作这个属性,导致和我们定义的 gender
冲突。
解决方案: 给
sex
标记@deprecated
来警示不要直接使用。
2. 无法通过 get/set
处理同名但不同数据类型数据转换
因为 JavaScript
的属性和 get/set
方法无法同名,所以无法通过这种方式完成相同属性名但不同类型的转换。
解决方案:通过子类重写并先调用父类的
fromJson
toJson
后,手动进行一次转换。
五、总结
如上代码,我们利用了 get/set
以及 Object
原型链上的一些方法来完成了转换。
虽然稍显麻烦,但也算解决了不在外部写一些方法来转换。各有所长,实现的方式有很多种,但思路才是很好玩的东西。
当然,我们还是计划在未来拥抱装饰器来解决这些问题。
JavaScript的装饰器提案已经快发布了,后续我们将继续摸索用装饰器在JavaScript中实现类似功能。(TypeScript的装饰器我们敢用,是因为编译后的JS代码没有包含装饰器的部分,在浏览器上不会有问题。)
六、That's all
欢迎持续关注专栏,也欢迎关注我们的前端开源项目 AirPower4T
本文完结,大半夜又水了一篇,美滋滋。