这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」
JS 中字符串化数据的解析
在工作中我们遇到后端传过来的JSON字符串时,我们通常使用 JSON.parse()来进行解析。这样做本身没有问题,但是会在某些情况下出现报错。
翻车现场
最近在处理字符串化的数据时,代码报错了,现场情况是怎么样的呢,大家请看下面的模拟场景
这是为什么呢,一般遇到这种情况我会查看MDN文档,看看自己的用法是否是有问题。果然,其中有两条很醒目:
而且标准的JSON格式中也明确规定
Objects and Arrays
Property names must be double-quoted strings; trailing commas are forbidden.
于此再看上图,我们犯了两个错误
- jsonStr 字符串内的
name和arr应该用 "double-quote" 包裹起来 [1,2,3,]中的末尾,需要删除。(trailing commas are forbidden)。
嗯。。。那么问题来了,我们在解析字符串的过程中,可能会遇到类似上面的不符合JSON规范的字符串,那么我们该如何有效的解析出其中的数据呢?
首先映入我脑海的是 eval()这个方法,因为 eval() 函数会将传入的字符串当做 JavaScript 代码进行执行。
那么我们看看效果如何呢?
const jsonStr = '{name: "hello", arr:[1,2,3,]}'
eval(jsonStr)
// ERROR: Uncaught SyntaxError: Unexpected token ':' at <anonymous>:1:6
en... 为什么会报错呢,查看一下报错原因是 意外的 token :。思考了一下eval会解析我们传入的字符串,而我们的字符串是以 {}为开头和结尾,这样JS引擎会把它当成一个语句块来处理,所以必须强制性的将它转换成一种表达式。
如何转换为表达式呢?
最简单的方式是两边加入括号。
我们来实验一下
const jsonStr = '{name: "hello", arr:[1,2,3,]}'
eval(`(${jsonStr})`)
// return {name: 'hello', arr: Array(3)}
那我们再试一下其他的能让JS引擎把它当成表达式的写法。
const jsonStr = '{name: "hello", arr:[1,2,3,]}'
eval(`!${jsonStr}`)
// return false; 虽然得不到我们想要的解析出来的结果,而是 false, 但也验证了我们的猜想,刚刚的报错的确是因为在解析的时候把{}当成代码块来处理了。
看来这样似乎是解决了我们的问题。
那么问题又来了,这个eval的方法有没有什么副作用呢? 在 MDN Web Docs给出了明确信息,在这里我摘录两条比较重要的放到下面。
- 如果你间接的使用
eval(),比如通过一个引用来调用它,而不是直接的调用eval。 从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。 eval()是一个危险的函数, 它使用与调用者相同的权限执行代码。如果你用eval()运行的字符串代码被恶意方(不怀好意的人)修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。更重要的是,第三方代码可以看到某一个eval()被调用时的作用域,这也有可能导致一些不同方式的攻击。相似的Function就不容易被攻击。
看来在编写代码过程中,我们尽量不要使用eval,如果使用了,也要确保自己写有eval内容块的代码不会被用户或者第三方代码访问到。
如果用户或者第三方写的东西我们使用了eval调用会有什么后果呢,一段代码来演示一下
const userData = '{name: "bamboo", age: alert("It is a secret!")}'
eval(`(${userData})`)
当你把代码复制到浏览器的控制台,我们可以看到在浏览器中调用了 alert方法。如果这个alert方法是一个恶意攻击代码,那么会变得非常糟糕。
Function
如果你实在需要使用 eval来解析类JSON代码,那么建议使用 Function,因为这个方法会稍微多一些安全保证,而且执行效率上会比eval高很多。
// 使用 eval 的糟糕代码
function looseJsonParse(obj){
return eval("(" + obj + ")");
}
console.log(looseJsonParse(
"{a:(4-1), b:function(){}, c:new Date()}"
))
// 不使用 eval的更好的代码
function looseJsonParse(obj){
return Function('"use strict";return (' + obj + ')')();
}
console.log(looseJsonParse(
"{a:(4-1), b:function(){}, c:new Date()}"
))
比较上面的两个代码片段,两个代码片段似乎是以相同的方式工作,但再想一想:eval的这个代码的速度要慢得多。 注意c: new Date()在执行体中。 在没有eval的函数中,对象在全局范围内被用来进行计算,因此浏览器可以放心的假设Date是来自window.Date的而不是一个名为Date的局部变量。
因此,在eval()版本的代码中,浏览器被迫进行高代价的查找调用以检查是否存在名为Date()的任何局部变量。 与Function()相比,这是非常低效的。
在类似的情况下,如果您确实希望能够从Function()内部的代码调用Date函数,该怎么办? 你应该躲避并退回到eval()吗? 绝对不是,永远不要这么做。而是尝试用闭包等方法来解决问题。
function Date(n){
return ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"][n%7 || 0];
}
function runCodeWithDateFunction(obj){
return Function('"use strict";return (' + obj + ')')()(
Date
);
}
console.log(runCodeWithDateFunction(
"function(Date){ return Date(5) }"
))
这就是我们这次学习的内容,如果转移字符串尽量用JSON.parse,在遇到JSON.parse无法解析的内容时,我们可以使用eval或者Function,但我就尽量使用Function而不是eval,来提升代码的安全性和效率。而且无论使用eval或者Function都不要用它来直接解析用户或者第三方的数据。