js

TypeScript中应该禁止使用enum

TypeScript 添加了一个 enum 的数据类型结构,但是这个数据类型在 JavaScript 中并不存在,在编译过程中会被转换成 Object。并且 enum 类型可以完全被 union 类型替代。 所以我们推荐使用 union 来代替 enum。接下来我们使用一个例子来解释下 enum 如何被替换成 union。

# enum 定义数据类型

我们来封装一个常见的 fetch 函数,使用 enum 定义的代码如下。

enum HTTPRequestMethod {
  GET = 'GET',
  POST = 'POST'
}

function fetchJSON(url: string, method: HTTPRequestMethod) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8

编译成 JS 代码如下:

'use strict';
var HTTPRequestMethod;
(function(HTTPRequestMethod) {
  HTTPRequestMethod['GET'] = 'GET';
  HTTPRequestMethod['POST'] = 'POST';
})(HTTPRequestMethod || (HTTPRequestMethod = {}));
function fetchJSON(url, method) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8
9

这样编译的 JS 代码,定义了 HTTPRequestMethod 对象,但并不是 const 常量,有被更改的可能性,非常不优雅。

# union 定义数据类型

const HTTPRequestMethod = {
  GET: 'GET',
  POST: 'POST'
} as const;

type ValuesOf<T> = T[keyof T];
type HTTPRequestMethodType = ValuesOf<typeof HTTPRequestMethod>;

function fetchJSON(url: string, method: HTTPRequestMethodType) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8
9
10
11

编译成 js 代码之后是:

'use strict';
const HTTPRequestMethod = {
  GET: 'GET',
  POST: 'POST'
};
function fetchJSON(url, method) {
  return fetch(url, { method }).then(response => response.json());
}
1
2
3
4
5
6
7
8

# 总结

对比 enum 和 union 定义生成的代码,我们能明显感受到 union 类型生成的代码更优雅。而且 enum 还有其他缺点,大家可以查看下参考链接。

# 参考

さようなら、TypeScript enum (opens new window)

Why it is not good to use enums? (opens new window)

Const Assertions in Literal Expressions in TypeScript (opens new window)

JS中关于深拷贝和浅拷贝

JS 有基本数据类型和复合数据类型,基本数据类型包括 null,undefined,boolean,number,string,symbol。针对这些数据类型都是值拷贝。复合数据类型包括 object,以及衍生的内置数据类型,Array, Map, Set 等等。默认对复合数据类型的赋值操作都是引用拷贝,就是浅拷贝。

# JSON.stringify 实现深拷贝

一种常用的方式是使用 JSON 接口先转换成字符串再解析成对象。

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));
1

虽然 V8 引擎优化了 JSON 的解析速度,但是这种方式还是有一些限制条件的,如果数据中存在 Map, Date 等 JS 内置数据结构就没法正常复制。

# 手动实现 deepClone

export const isObject = (obj: any): boolean => {
  return obj !== null && typeof obj === 'object';
};

export const objectToString = Object.prototype.toString;
export const toTypeString = (value: unknown): string =>
  objectToString.call(value);

export const isMap = (val: unknown): val is Map<any, any> =>
  toTypeString(val) === '[object Map]';
export const isSet = (val: unknown): val is Set<any> =>
  toTypeString(val) === '[object Set]';
export const isDate = (val: unknown): val is Date =>
  toTypeString(val) === '[object Date]';
export const isRegExp = (val: unknown): val is RegExp =>
  toTypeString(val) === '[object RegExp]';

export function deepClone<T>(val: T): T {
  if (!isObject(val)) {
    return val;
  }
  if (Array.isArray(val)) {
    return val.map(deepClone) as typeof val;
  }
  if (isDate(val)) {
    return new Date(val) as typeof val;
  }
  if (isMap(val)) {
    return new Map(val) as typeof val;
  }
  if (isSet(val)) {
    return new Set(val) as typeof val;
  }
  if (isRegExp(val)) {
    return new RegExp(val.source, val.flags) as typeof val;
  }
  return Object.keys(val).reduce((acc, key) => {
    acc[key as keyof T] = deepClone(val[key as keyof T]);
    return acc;
  }, {} as T);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

手动实现 deepClone 可以处理 Date, Map, Set, RegExp 这些数据结构,但这也不是完美的解决方案,例如二进制的数据结构 ArrayBuffer 就没在处理范围,但对于绝大多数场景也足够用了。

# 使用 structuredClone

const myDeepCopy = structuredClone(myOriginal);
1

最新的浏览器标准添加了 structuredClone 函数,可以实现常见数据结构的复制,但是有些数据结构也是不能复制的,例如 Function。不过 Function 也没有复制的必要。 由于这个函数最近才添加,实际使用中可能还需要搭配 polyfill (opens new window)

# 总结

实际开发工作中大家可以根据自己的需要选择合适的深拷贝解决方案。

# 参考

structuredClone (opens new window)

Deep-copying in JavaScript using structuredClone (opens new window)

cloneDeep (opens new window)

前端开发地图应用的调研

我司的产品终于发布了新版本,所以忙碌的开发暂时告一段落。最近在做下一期的规划,然后就是要做前端网站来满足用户的需求。由于我们的产品是和地图强相关的,所以也对地图做了很多调研。

# 基本需求

产品需要在国内和国外使用,费用合理,可以换地图的贴图并且能在地图上绘制各种信息。例如多边形或圆形还有贝塞尔曲线。

# MapKit JS

苹果居然也出了网页版本的地图产品MapKit JS (opens new window),可谓是良心。但是目前还处于 Beta 版本。如果以后要考虑产品的全平台化,显然不是一个很好的选择。不过苹果的产品可以在国内国外使用不用太担心地图偏移问题,而且可以贴图和绘制图形,但是目前网页版还不能绘制贝塞尔曲线,iOS 客户端倒是可以。

# MapBox

MapBox 是一个地图服务提供商,经过调研 MapBox 使用最新的 WebGL 技术来渲染,摆脱了传统的下 tile 来贴图的方式,渲染速度得到了大大的提升。也能解决国内和国外的地图显示问题,但是画图功能就只有画线和多边形。不过提供了底层的画图层的接口,需要自己写 WebGL 的 shader。这就增加了开发成本。。。

# GoogleMap

谷歌地图当然好,但是我最后才说。因为谷歌无法在国内使用,这种情况下我就不得不写两套接口来对应,例如国内高德地图,国外谷歌地图,会大大增加开发成本。而且谷歌地图最近刚升级付费条款,费用是按照请求次数来收,所以用户大量增长之后的开销也会非常大。不过谷歌也不能画贝塞尔曲线。。。

# 地理坐标系

由于是需要在地图上绘制新的 tiles,所以自然就涉及到坐标的转换和计算。如何计算一个经纬度落在哪一张 tiles 上,以及在不同缩放级别下 tiles 的正常显示和重绘。这些都是需要自己来实现的。 这里有一个还算有名的官方介绍,并给出了 Python 的源码。可以参考Google Maps: Coordinates, Tile Bounds and Projection (opens new window)来进行实现。

# 坐标系转换

每个国家都有自己的坐标系系统,虽然 GPS 使用的WGS 84 (opens new window)标准非常流行,但是这个是美国制定的。每个国家当然都需要根据自己国家需要来定制自己的标准。例如中国就有北京 54 坐标系,西安 80 坐标系。北京 54 和西安 80 是参心坐标系,大地原点分别在苏联和西安。难以表达高度信息,目前国家正在推广2000 国家大地坐标系 (opens new window),这个和 WGS84 一样是地心坐标系,即以地球质量中心为原点。日本也有自己的平面直角坐标系,我们当然需要各种坐标转换,还好有现成的开源项目proj4js (opens new window)

# 总结

各个地图服务商都各有优劣,但却没有一款完美的。主要是用途也比较特殊,可能在地图服务上进行二次开发的可能性比较高吧。不过提供地图服务的也没几家可以选择的。。。