js

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)

# 总结

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

如何封装第三方vue组件

终于算是正式接触 SPA 的前端工作了,最近在解决很多 Vue 组件的问题,其中就有一个针对第三方组件的定制化需求,花了不少时间来折腾。

# 解决 v-model 绑定

这次封装的就是 element 的前端 vue 框架,因为框架本身对 iOS 兼容性不好,所以需要二次封装。封装的是一个 select 组件。所以需要数据的双向绑定,官方教程也已经解释了。 v-model (opens new window) 本质上就是绑定一个值和监听相应的事件。 这次本质是封装的一个 input 元素,所以我们需要手动绑定 value 和监听事件。

<template>
  <el-select v-bind:value="value" v-on="$listeners"></el-select>
</template>
1
2
3

官方文档也解释了这个内置变量的用法vm-listeners (opens new window)

# 继承父元素的属性

父元素的属性继承可以使用v-bind="$attrs"来完成。所以完成组建的透明封装只需要加上三个指令。

<template>
  <el-select v-bind="$attrs" v-bind:value="value" v-on="$listeners">
  </el-select>
</template>
1
2
3
4

虽然知道答案之后很简单,但是探索的过程中还是比较花时间的。希望能帮助大家解决这个问题,如果有什么疏漏之处,也请大家指正。