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' }}"
  }
}
相关推荐
aPurpleBerry2 分钟前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端40 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x44 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v2 小时前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记2 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜2 小时前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=2 小时前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css