Stringify 和 Parse 带函数的 JavaScript 对象

将一个 JavaScript 对象转换为字符串可以通过 JSON.stringify来完成,但是该方法有一个限制:会过滤掉 value 为函数的 key,导致没有办法通过 JSON.parse来获得原始对象。本篇文章就介绍如何解决这个问题:如何 Stringify 和 parse 带函数的 JavaScript 对象。

背景

在介绍解决办法之前,首先简单介绍一下该问题的背景,有助于大家了解一个实际的应用场景。

在最新发布的 G2 5.0 中推出了一种全新的 API 形式:Spec API。该 API 是通过一个 JavaScript 对象去声明一个可视化图表。

css 复制代码
const bar = {
  type: 'interval',
  data: [
    { genre: 'Sports', sold: 275 },
    { genre: 'Strategy', sold: 115 },
    { genre: 'Action', sold: 120 },
    { genre: 'Shooter', sold: 350 },
    { genre: 'Other', sold: 150 },
  ],
  encode: {
    x: 'genre',
    y: 'sold',
    color: (d) => (d.sold > 150 ? 'high' : 'low'),
  },
};

然后调用 chart.options(bar) 去渲染图表,最后得到的可视化效果如下:

该 API 的优点之一就是:图表可以被持续化存储, 也就是说可以把上面的 bar 对象转换为 String 保存下来,在需要使用的时候再进行解析。

而因为图表的声明本质上就是一个"带有函数的 JavaScript 对象",所以就是要解决"如何 Stringify 和 parse 带函数的 JavaScript 对象" 这个问题。

存在问题

了解了背景,我们通过一个简单的例子来再认识一下存在的问题。以如下的对象 add 为例:

javascript 复制代码
const add = {
  name: 'add',
  callback: (a, b) => a + b,
};

当调用 JSON.stringify之后发现:callback这个字段已经被过滤掉了。

sql 复制代码
const str = JSON.stringify(add); // "{"name":"add"}"

这样 JSON.parse就没有办法获得原始的对象。

ini 复制代码
const obj = JSON.parse(str); // {name: "add"}

所以我们需要定义两个新的方法 stringifyparse可以达到如下的效果。

csharp 复制代码
typeof stringify(add) === 'string' // true;
parse(stringify(add)).callback(1, 2) // 3;

思路

查阅 MDN 发现 JSON.stringifyJSON.parse 都接受第二个参数,都会在返回最后结果之前对该对象的每一个 key 和 value 进行处理。类似一个钩子函数(Hook),可以让我们针对性的定制化 stringify 和 parse 的逻辑。

所以在当前这个场景中,只需要在 stringify 的过程中显示地将函数值转换成可以识别的字符串,然后在 parse 的时候识别该字符串,并且调用 window.eval将该字符串转换成函数实例即可。

解决方案

将函数转换成字符串调用函数的 function.toString方法即可,同时为了将转换成字符串的函数和普通的字符串区分开来,我们将前者用标签 <func>包裹起来。当让这里标记的方式不是唯一的,只要能和后面的 parse 逻辑匹配即可。

javascript 复制代码
function stringify(obj) {
  return JSON.stringify(obj, (_, value) => {
    if (typeof value !== 'function') return value;
    return `<func>${value.toString()}</func>`;
  });
}

使用该 stringify转换上述 add 对象得到如下的结果。可以发现 callback 字段已经正确得被保留下来了!

sql 复制代码
stringify(add); // "{"name":"add","callback":"<func>(a, b) => a + b</func>"}"

那么这之后就需要正确地解析该字符串了。这里我们只对可识别的字符串的值进行处理:获取函数代码,并且转换成函数对象实例返回。

javascript 复制代码
function parse(str) {
  return JSON.parse(str, (_, value) => {
    // 匹配可识别字符串值
    if (typeof value !== 'string') return value;
    const match = Array.from(value.matchAll(/<func>(.*?)</func>/g));
    if (match.length === 0) return value;

    // 实例化函数
    const [, foo] = match[0];
    return eval(foo);
  });
}

最后运行代码,可以发现 add.callback可以正常被调用。

scss 复制代码
parse(stringify(add)).callback(1, 2); // 3

小结

我们通过 JSON.stringifyJSON.parse 的第二个参数解决了 Stringify 和 Parse 带函数的 JavaScript 对象问题,也为持续化存储 G2 5.0 Spec 提供了一个思路。

当然对于第二个问题还有别的解决思路:设计一套表达式语法。这可以使得整个 G2 的 Spec 可以完全变成一个 JSON 对象,比如如下。这就要求 G2 5.0 的 runtime 能正确的解析这套语法,这个也是 G2 5.0 未来的工作之一,感兴趣的小伙伴可以参与讨论和共建。

css 复制代码
{
  "type": "interval",
  "data": [
    { genre: "Sports", sold: 275 },
    { genre: "Strategy", sold: 115 },
    { genre: "Action", sold: 120 },
    { genre: "Shooter", sold: 350 },
    { genre: "Other", sold: 150 },
  ],
  "encode": {
    "x": "genre",
    "y": "sold",
    "color": "{{ d.sold > 150 ? 'high' : 'low' }}"
  }
}
相关推荐
十一吖i3 小时前
vue3表格显示隐藏列全屏拖动功能
前端·javascript·vue.js
徐同保4 小时前
tailwindcss暗色主题切换
开发语言·前端·javascript
生莫甲鲁浪戴5 小时前
Android Studio新手开发第二十七天
前端·javascript·android studio
细节控菜鸡7 小时前
【2025最新】ArcGIS for JS 实现随着时间变化而变化的热力图
开发语言·javascript·arcgis
拉不动的猪8 小时前
h5后台切换检测利用visibilitychange的缺点分析
前端·javascript·面试
桃子不吃李子8 小时前
nextTick的使用
前端·javascript·vue.js
Devil枫10 小时前
HarmonyOS鸿蒙应用:仓颉语言与JavaScript核心差异深度解析
开发语言·javascript·ecmascript
惺忪979810 小时前
回调函数的概念
开发语言·前端·javascript
前端 贾公子10 小时前
Element Plus组件v-loading在el-dialog组件上使用无效
前端·javascript·vue.js
天外飞雨道沧桑10 小时前
JS/CSS实现元素样式隔离
前端·javascript·css·人工智能·ai