JavaScript的強語言之路—另類的JSON序列化與反序列化

2020年01月17日 更新 此篇文章代碼Demo:github.com/stelalae/js…yarn start:debug看控制檯輸出效果。前端

JSON(JavaScript Object Notation)是一種輕量級,徹底獨立於語言的數據交換格式。目前被普遍應用在先後端的數據交互中。在JavaScript中的應用隨處可見,靈活性、擴展性、可讀性也是最強的!對應的JSON.parseJSON.stringify就能夠看作是對象的序列化和反序列化,將對象與字符串之間相互轉換。node

序列化與反序列化

互聯網的產生帶來了機器間通信的需求,而互聯通信的雙方須要採用約定的協議,序列化和反序列化屬於通信協議的一部分。通信協議每每採用分層模型,不一樣模型每層的功能定義以及顆粒度不一樣,例如:TCP/IP 協議是一個四層協議,而 OSI 模型倒是七層協議模型。在 OSI 七層協議模型中展示層(Presentation Layer)的主要功能是把應用層的對象轉換成一段連續的二進制串,或者反過來,把二進制串轉換成應用層的對象 -- 這兩個功能就是序列化和反序列化。通常而言,TCP/IP 協議的應用層對應與 OSI 七層協議模型的應用層,展現層和會話層,因此序列化協議屬於 TCP/IP 協議應用層的一部分。本文對序列化協議的講解主要基於 OSI 七層協議模型。react

  • 序列化: 將數據結構或對象轉換成二進制串的過程。
  • 反序列化:將在序列化過程當中所生成的二進制串轉換成數據結構或者對象的過程。

簡單來講,序列化是將對象轉換成字節流的過程,而反序列化的是將字節流恢復成對象的過程。二者的關係以下: ios

不一樣的計算機語言中,數據結構、對象以及二進制串的表示方式並不相同。如Java/JavaScript中使用的是對象(Object),來自類的實例化。而C是用struct去表示數據解構,或根據指針的偏移量在內存中讀取數據。C++則是Java方式或C方式都可,由於C++比C強化了class的概念。git

從計算機語言的發展歷史來看,序列化協議從通用性、健壯性、可讀性、擴展性、安全性、性能等方面,設計出下面幾種常見的協議:es6

  • 早期協議:COM、CORBA。
  • XML&SOAP:XML(可擴展標記語言,Extensible Markup Language)本質上是一種描述語言,具備自我描述的屬性。SOAP(Simple Object Access protocol) 是基於 XML 爲序列化和反序列化協議的結構化消息傳遞協議,在前期互聯網階段被普遍應用,對應的解決方案叫作Web Service。
  • JSON:源於JavaScript,遇上移動浪潮發展,現正在普遍使用在各類業務場景中。
  • Thrift:是Facebook開源的輕量級RPC服務框架,在大數據量、分佈式、跨語言、跨平臺的服務端使用較多。
  • Protobuf:來自Google,因改善了XML、JSON的詬病,空間開銷小、高解析性能、語言支持度高等亮點,許多公司將其做爲後端之間通訊、對象持久化等方面的首選方案。在終端上,IM業務上也使用較多。
  • Avro:屬於 Apache Hadoop的子項目,提供兩種序列化格式:JSON、Binary,即調試時用JSON,上線後用Binary。(我的以爲就是對JSON在傳輸時的改良版)

上述序列化協議的Benchmark請自行搜索與查詢。在選型上也要根據具體業務場景,我的建議:JSON適合①有前端參與②小型項目, Protobuf適合①高性能②T級別數據。github

JSON的parse與stringify

首先來看看MDN的介紹:JSON.stringify()JSON.parsejson

  • JSON.stringify(value[, replacer [, space]]):用來將一個 JavaScript 值(對象或者數組)轉換爲一個 JSON 字符串,若是指定了 replacer 是一個函數,則能夠選擇性地替換值,或者若是指定了 replacer 是一個數組,則可選擇性地僅包含數組指定的屬性。
  • JSON.parse(text[, reviver]):用來解析JSON字符串,構造由字符串描述的JavaScript值或對象。提供可選的 reviver 函數用以在返回以前對所獲得的對象執行變換(操做)。

你可能已注意到,JSON的stringify比parse多了一段描述axios

  • 轉換值若是有toJSON()方法,該方法定義什麼值將被序列化。
  • 非數組對象的屬性不能保證以特定的順序出如今序列化後的字符串中。
  • 布爾值、數字、字符串的包裝對象在序列化過程當中會自動轉換成對應的原始值。
  • undefined、任意的函數以及 symbol 值,在序列化過程當中會被忽略(出如今非數組對象的屬性值中時)或者被轉換成 null(出如今數組中時)。函數、undefined被單獨轉換時,會返回undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 對包含循環引用的對象(對象之間相互引用,造成無限循環)執行此方法,會拋出錯誤。
  • 全部以 symbol 爲屬性鍵的屬性都會被徹底忽略掉,即使 replacer 參數中強制指定包含了它們。
  • Date日期調用了toJSON()將其轉換爲了string字符串(同Date.toISOString()),所以會被當作字符串處理。
  • NaN和Infinity格式的數值及null都會被當作null。
  • 其餘類型的對象,包括Map/Set/weakMap/weakSet,僅會序列化可枚舉的屬性。

這段描述說明了,在JSON.stringify序列化時哪些數據會被保留、轉換、忽略。因此個人前端項目裏必定會在接口請求層裏對undefined和null進行屏蔽:後端

const body = JSON.stringify(params, (k, v) => {
    if (v !== null && v !== undefined) {
      return v;
    }
});
複製代碼

一樣在axios請求響應後,對response data裏的undefined和null進行屏蔽:

import axios from 'axios';
import { ResponseData } from '../defines';

// axios.defaults.timeout = 10000;

const parseJSON = (response: any) => {
  // 先對Object進行序列化,再有條件的反序列化
  const dataString = JSON.stringify(response);
  const dataObj = JSON.parse(dataString, (k: any, v: any) => {
    if (v === null) {
      return undefined;
    }
    return v;
  });
  return dataObj;
};

const parseResponse = (response: any) => {
  if (response.status >= 200 && response.status < 300) {
    return response.data;
  }
  return {};
};

export const request = async (options: any): Promise<ResponseData> => {
  try {
    const resp = await axios(options);
    const data = await parseResponse(resp);
    return parseJSON(data);
  } catch (err) {
    return Promise.resolve({
      code: 999,
      msg: '網絡超時',
    });
  }
};
複製代碼

完善解構賦值

上面的JSON.stringify、JSON.parse是多餘和浪費性能的???下面咱們用代碼來講話。

// types.ts
export class OrderBase {
  creatTime: string;
  payTime: string;
  // 其餘省略
}

export class OrderDetail {
  orderNo: string;
  userNo: string;
  businessCode: string;
  imgList: string[];
  base: OrderBase;
}
複製代碼

鑑於目前ts愈來愈火,在中大型項目中使用愈來愈多,因此我這裏也引入ts,且不會提示ts錯誤。

// order.ts
function test() {
  const orderinfo: OrderDetail = JSON.parse('{}');
  const { imgList, base } = orderinfo;

  let imgurl = [];
  if (imgList && imgList.length > 0) {
    imgurl = imgList.map(item => (item += '?xxx'));
  }
  const { payTime } = base || {};

  let showpay = false;
  if (payTime && payTime.length > 0) {
    showpay = true;
  }
}
test();
複製代碼

在現代JavaScript項目中ES6寫法已經普遍使用,若是你熟悉和習慣ES6寫法,特別是解構賦值,剛纔的代碼必定會讓你感到不舒服的!

function test() {
  const orderinfo: OrderDetail = JSON.parse('{}');
  const { imgList = [], base: { payTime = '' } = {} } = orderinfo;
  const imgurl = imgList.map(item => (item += '?xxx'));
  const showpay = payTime !== '';
}
test();
複製代碼

沒錯,這纔是標準的ES6寫法,解構賦值的默認值、嵌套解構等常用時,必須在解析接口響應處等源頭,利用反序列化時將值爲null的屬性從對象中移除掉。

JavaScript 項目中最多見的十大錯誤。

我以爲解構賦值和解構默認值的出現,至少讓前端少了10%的代碼量!!!ts的出現,加上es六、es201七、es2020等發佈,正確使用新特性,讓咱們逐步從低級bug中解脫出來。

無論接口規範定義得再好,也不要相信接口數據會100%按照你的要求來。——先後端平常扯皮之接口部分

重構解構賦值

上面例子簡單的說了解構賦值及默認值的好處,就是簡化代碼邏輯,少寫bug。但你也可能意識到,若是不少地方都要用解構,是否是都要寫一遍默認值?!好像是的。

因此理想狀況是,只在一處設置對象的默認值後,其餘地方如嵌套解構、JSON的序列化和反序列化等,就能夠直接使用默認值了!下面開始記錄改造過程。

TypeScript項目裏確定要是從類型定義開始下手,即要在對象聲明時定義默認值。因ts有類型推導,因此有了默認值的部分屬性會被顯示移除類型聲明。注意,因添加了默認值,因此只能使用class,且聲明文件不會被註解,不能用interface。

export class OrderBase {
  creatTime: string = '';
  payTime: string = '';
}

export class OrderDetail {
  orderNo: string = '';
  userNo = '';  // 根據默認值推導出userNo的類型爲string
  imgList: string[] = [];
  base: OrderBase;
}
複製代碼

那如今測試效果,發現userNo竟然是undefined,意思是類型聲明裏的默認值未生效。

function test() {
  const orderinfo: OrderDetail = JSON.parse('{}');
  const { base: { payTime = '123' } = {}, userNo } = orderinfo;
  console.log(payTime, userNo); // 打印:123 undefined
}
test();
複製代碼

由於是ts項目,實際運行代碼是編譯以後的代碼,而不是編寫的代碼。

// tsconfig.json中"target": "es2017"時的效果
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function test() {
    const orderinfo = JSON.parse('{}');
    const { base: { payTime = '123' } = {}, userNo } = orderinfo;
    console.log(payTime, userNo);
}
test();


// tsconfig.json中"target": "es5"時的效果
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function test() {
    var orderinfo = JSON.parse('{}');
    var _a = orderinfo.base, _b = (_a === void 0 ? {} : _a).payTime, payTime = _b === void 0 ? '123' : _b, userNo = orderinfo.userNo;
    console.log(payTime, userNo);
}
test();
複製代碼

通過編譯以後,test裏的代碼好像與聲明文件不要緊了,更別說使用定義的默認值。能夠思考想想這是爲何!

緣由是:

  1. class當作類型去聲明變量時,用法和interface同樣效果,編譯後對應代碼會被註解。
  2. 添加了類型聲明的變量,數據類型並未發生變化,而不是變成類的一個實例。

正確作法時,讓變量由一個JSON對象變成一個類實例。被類實例化後,就可使用類的一切特性和自定義函數。

屬性回填

const a = new OrderDetail();
console.log(a.userNo); // 打印:''
a.userNo = 'abc';
console.log(a.userNo); // 打印:'abc'
複製代碼

太闊怕了!

Object.assign

const obj = {
  userNo: 'a1',
  base: { creatTime: '2020-01-13 18:32:58' },
};
const a = Object.assign(new OrderDetail(), obj);
console.log(a.userNo); // 打印:'a1'
a.userNo = 'abc';
console.log(a.userNo); // 打印:'abc'
console.log(a.base.creatTime); // 打印:'2020-01-13 18:32:58'
console.log(a.base.payTime); // 打印:undefined
複製代碼

相比屬性回填,你們應該更能接受經過assign去實例化。另外,assign只能一層深拷貝,因此不要想着原型鏈上的操做。

immutable record

Record是一種僅記錄JS對象中已註冊過的成員的數據結構,且支持默認值。

A record is similar to a JS object, but enforces a specific set of allowed string keys, and has default values.

const { Record } = require('immutable')
const ABRecord = Record({ a: 1, b: 2 })
const myRecord = ABRecord({ b: 3,  x: 10 })     // ts中此處會提示錯誤

myRecord.size // 2
myRecord.get('a') // 1
myRecord.get('b') // 3
const myRecordWithoutB = myRecord.remove('b')   // remove後,b會被重置爲2,而不是undefined
myRecordWithoutB.get('b') // 2
myRecordWithoutB.size // 2
myRecord.get('x') // undefined
複製代碼

immutable record僅會記錄初始化時已註冊的key,且能永遠保證你key-value存在。 和Object.assign相比,Record的數據結構更強勢,自帶的方法會爲你的數據管理設計提供大放異彩的可能。

// 方式一:有類型聲明
type PersonProps = {name: string, age: number};
const defaultValues: PersonProps = {name: 'Aristotle', age: 2400};
const PersonRecord = Record(defaultValues);
class Person extends PersonRecord<PersonProps> {
  getName(): string {
    return this.get('name')
  }

  setName(name: string): this {
    return this.set('name', name);
  }
}

// 方式二:無類型聲明
class ABRecord extends Record({ a: 1, b: 2 }) {
  getAB() {
    return this.a + this.b;
  }
}

var myRecord = new ABRecord({b: 3})
myRecord.getAB() // 4
複製代碼

對比上面兩種方式,區別在因而否是集成了 TypeScript。結合 Record 的文檔,會發現 Record 實際上是工廠模式的一種應用。先經過默認值方式註冊到工廠,而後內部橋接了一些自定義函數。(本人未研究 Record 源碼,如有不對請指出)

Record裏也有專門說明 Choosing Records vs plain JavaScript objects

  • Runtime immutability:運行時不可變性,這符合immutable的一向原則,數據的只讀性。
  • Value equality:值相等,即Record提供equals來判斷兩個對象是否嚴格相等。
  • API methods:額外的API方法,如getIn、equals、hashCode、toJS-深層、toObject-淺層、toJSON-淺層等。
  • Default values:默認值。爲每一個鍵提供默認值,即時被remove以後。
  • Serialization:序列化。即提供toJS-深層、toObject-淺層、toJSON-淺層,和JSON對象之間的序列化和反序列化。

因此我在React項目中一直使用immutable,爲數據管理和頁面Render設計了不少優化方案。

數據對象的類實例化

前面提到的重構解構賦值,目的就是將JavaScript中的JSON Object變爲Class Object,如標題所說的『另類』就是指將原有的 JSON 字符串與JavaScript 值(對象或者數組)互轉,變成 JSON 字符串或JavaScript 值(對象或者數組)與Class互轉。而後對class進行擴展,好比封裝、繼承、重載等,讓JavaScript的強語言之路走得更遠!

在MVC模型裏,相比utils函數、String/Number擴展函數等方式,使用class管理更容易減小項目的內部耦合度,也更方便擴展和維護。好比性別:

export enum SexType {
  none = 0,
  man = 1,
  woman = 2,
}

export class UserProfile {
  userNo: string = '';
  userName: string = '';
  sex: SexType = 0;

  static sexMap = {
    [SexType.none]: '保密',
    [SexType.man]: '男',
    [SexType.woman]: '女',
  };

  public get sexT(): string {
    return UserProfile.sexMap[this.sex] || '保密';
  }
  
  // 其餘自定義函數
}

const myinfo = Object.assign(new UserProfile(), {
  userNo: '1',
  userName: 'test',
  sex: 1,
});

console.log(myinfo.sex, myinfo.sexT); // 打印:1 '男'
myinfo.userName = '1'; // ok - userName是可修改的
複製代碼

可是,若是引入immutable-record方式呢?

// index.ts - 聲明文件
import { Record } from 'immutable';

export enum SexType {
  none = 0,
  man = 1,
  woman = 2,
}

interface IRecordInfo {
  createTime: string;
  updateTime?: string;
}

const IRecordInfoDefault: IRecordInfo = {
  createTime: '',
  updateTime: '',
};

export class RecordInfo extends Record(IRecordInfoDefault) {} // 建議使用class,即時沒有擴展屬性
// export const RecordInfo = Record(IRecordInfoDefault); // 不建議變量方式,不然不能把 RecordInfo 當作類型去聲明變量

interface IUserProfile extends IRecordInfo {
  userNo: string;
  userName: string;
  sex: SexType;
}

const UserProfileDefault: IUserProfile = {
  userNo: '',
  userName: '',
  sex: SexType.none,
  ...IRecordInfoDefault,
};

export class UserProfile extends Record(UserProfileDefault) {
  sexMap = {
    [SexType.none]: '保密',
    [SexType.man]: '男',
    [SexType.woman]: '女',
  };

  public get sexT(): string {
    return this.sexMap[this.sex] || '保密';
  }

  // 其餘自定義函數
}

export interface ITsTest {
  id: string;
}

const ITsTestDefault: ITsTest = {
  id: '',
};

export const TsTest = Record(ITsTestDefault);

const myinfo = new UserProfile({ userNo: '1', userName: 'test', sex: 1 });
console.log(myinfo.sex, myinfo.sexT); // 打印:1 '男'

myinfo.userName = '1'; // error - userName是隻讀的,不可修改
const myinfonew = myinfo.set('userName', '1'); // ok - 返回新的 immutable 對象
複製代碼

你應該注意到,引入immutable record後,在類型聲明上可能會增長代碼量,但我以爲這是值得的!緣由有下:

  • 不影響聲明類型,可讀性強。
  • 從源頭嚴格控制數據模型,防止無關數據。
  • 從 JSON 對象到 Class 對象,下降設計上的耦合度。

React是以數據驅動頁面的模式,即MVC中經過改變M,自動去更新V。因此引入immutable record能提高M,減小V、C中的邏輯。對比本身平常的React代碼,能看出哪些優化空間嗎?!

// index.ts - 業務文件
import React from 'react';

import { RecordInfo, UserProfile, TsTest } from '../types';

interface DivBaseProps {
  data: RecordInfo;
}

const DivBase = (props: DivBaseProps) => {
  const { data } = props;
  console.log(data, data.hashCode()); // 如前面聲明類型時,建議用class方式
  return (
    <div> <p>{data.createTime}</p> <p>{data.updateTime}</p> </div>
  );
};

interface HomeState {
  detail: UserProfile;
  extra: RecordInfo;
}

export default class Home extends React.PureComponent<any, HomeState> {
  constructor(props: any) {
    super(props);
    this.state = {
      detail: new UserProfile(), // ok
      extra: new RecordInfo(), // ok
      // detail: new RecordInfo(), // error - 缺失數據
      // extra: new UserProfile(), // ok - 因 UserProfile 從 RecordInfo繼承而來
      // extra: new TsTest(), // error - 缺失數據
    };
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({ detail: new UserProfile({ sex: 1 }) });
    }, 1000);
  }

  render() {
    const { detail } = this.state;
    return <DivBase data={new RecordInfo(detail)}></DivBase>; // ok
    // return <DivBase data={{ ...detail }}></DivBase>; // error
  }
}
複製代碼

總結

本文先介紹JavaScript自帶的JSON序列化和反序列化的基本使用,配合ES6中解構賦值及默認值,優化了在對空值判斷的處理方式。而後利用類實例化,對JSON對象進一步反序列化,同時利用class進行高內聚低耦合的代碼管理。

由於TypeScript提供編譯基礎,如今JavaScript的編碼方式逐步向強語言靠攏,固然Running time仍是未改變,期待TypeScript可輸出WebAssembly!

參考資料:
1.序列化和反序列化
2.序列化與反序列化
3.探索如何使用 JSON.stringify() 去序列化一個 Error
4.Record

相關文章
相關標籤/搜索