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"}

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

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

思路

查阅 MDN 发现 JSON.stringify 和 JSON.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.stringify和 JSON.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' }}"
  }
}
相关推荐
程序员大阳2 小时前
副队长JavaScript教程(50)-DOM:网页开发的起点
javascript·dom·es·节点·概念·document·关系
龙亘川4 小时前
综合素质评价业务建模:从过程性评价到成长档案全链路闭环
数据库·智慧城市·数据可视化
香芋芋圆7 小时前
iD-editor 详解|OSM官方在线编辑器,基于D3.js的轻量地图编辑器
开发语言·javascript·编辑器
晗夜未央9 小时前
任务队列反复失败,先分清重试、阻塞和结果待核对
开发语言·javascript
jktantan9 小时前
每个节点 41 字节:把一棵百万节点树拆进 14 个 typed array
前端·javascript·vue.js·算法·typescript
我命由我1234510 小时前
访客系统形式(有用户概念、无用户概念)
java·开发语言·前端·javascript·python·产品运营·产品经理
程序员大阳11 小时前
副队长JavaScript教程(65)-详解JSON:轻量级数据交换格式
javascript·json·序列化·反序列化·stringify·parse
paopaokaka_luck12 小时前
基于springboot+vue的益暖微光公益服务平台(DeepSeek公益助手、善款信息追溯、志愿服务积分闭环、ECharts运营分析、活动状态自动流转)
java·前端·javascript·spring boot·mysql·架构·echarts
栀椩12 小时前
Electron 内嵌页面跨域问题(CORS/PNA)解决全记录
前端·javascript·electron
SEO_juper13 小时前
Chrome 扩展一键查看当前页面 SEO 信息:不用切页面,点一下全出来
前端·javascript·chrome