将一个 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' }}"
}
}