六种动态运行js字符串的方案对比先有问题再有答案 动态运行js字符串的方案有哪些? 这些方案在运行环境,代码来源,访问权
先有问题再有答案
动态运行js字符串的方案有哪些?
这些方案在运行环境,代码来源,访问权限,执行时机上有什么差异?
为什么我们需要动态执行js字符串?
为什么大家都不建议使用动态运行js字符串的方案?
如果必须要选择一种方案 我们应该推荐哪种方案?
eval 函数(不建议)
<script>
var outerName = 'outer'
function test() {
let localName = 'local' // 可以访问局部变量
const jsString = "var innerName = 'inner'; " + " console.log('这是一段动态执行的js字符串', outerName, innerName, localName) ";
eval(jsString)
console.log('外部打印 innerName:', innerName)
}
test();
</script>
- 运行环境:当前作用域,
eval()函数会在其所在的函数作用域去执行代码
,而不是创建一个新的作用域。所以,如果eval()函数在函数内部调用时可以访问函数的局部变量,而且还会污染全局对象。 - 代码来源:只接受字符串,由开发者预先定义
- 访问权限:可以访问或修改当前作用域中的任何对象
- 同步/异步:同步执行
- 安全性和性能:安全性差,可能被用于执行恶意代码;性能相较于其它方式较差,因为它需要运行时解析字符串
使用script元素
示例
<script type="module">
// 定义要执行的 JavaScript 字符串
const jsString = "var innerName = 'hahha'; " + " console.log('这是一段动态执行的js字符串', innerName) ";
// 创建一个新的 <script> 元素
const script = document.createElement('script');
// 将 type 属性设置为 text/javascript,告诉浏览器这是 JavaScript 代码
script.type = 'text/javascript';
// 将 JavaScript 字符串设置为 script 元素的 textContent
script.textContent = jsString;
// 将 script 元素添加到页面的 head 或 body 中
document.head.appendChild(script);
console.log('外部打印 innerName', innerName)
</script>
我们创建了一个新的 <script>
元素,将其 type
属性设置为 'text/javascript'
,并将我们的 JavaScript 字符串赋值给 textContent
属性。最后,我们将这个 <script>
元素添加到文档的 <head>
中,这种方法会立即执行 <script>
元素中的代码,因此它可以用于按需执行或延迟执行 JavaScript 代码。然而,这种方法也意味着一旦 <script>
元素被添加到 DOM 中,代码就会被执行,这可能不适用于所有场景。
webpack动态加载的原理就是这个方案....
- 运行环境:全局作用域
- 代码来源:script 元素的内容或加载的外部脚本
- 访问权限:可以访问和修改全局作用域中的任何对象
- 同步/异步:异步执行
- 安全性和性能:安全性取决于代码的来源(内部脚本通常是安全的,但应仔细处理外部脚本以防止XSS攻击等);性能一般
使用 new Function() 构造函数
var outerName = 'outer'
function test() {
let localName = 'local' // 不能访问局部变量
const jsString = "function sayHello() {" +
" var innerName = 'inner'; " +
" console.log('这是一段动态执行的js字符串', outerName, innerName ) " +
"}" +
"sayHello();";
// 使用new Function()来创建一个新的函数
var fun = new Function(jsString);
// 调用这个新创建的函数
var result = fun();
}
test();
需要注意的是 通过new Function() 创建的函数不会继承其创建时的局部作用域。 这种特殊的作用域行为,与普通 JavaScript 函数的作用域闭包机制不同。
优点:
- 隔离作用域:使用 new Function() 创建的函数不会访问到局部作用域(闭包作用域),它们只能访问全局变量, 或者显示传入的变量参数。
- 避免变量污染 : 字符串内部定义的变量在外部无法获取,不会污染全局 。
缺点
- 安全风险:执行从不可信来源动态生成的代码总是风险很高的操作。如果代码中包含恶意内容,则可能导致安全漏洞,如XSS攻击或远程代码执行。
- 性能问题:使用 new Function() 生成的函数可能比普通函数执行效率低,因为它们在运行时需要经过编译和解析过程。而且,由于它们通常无法被JavaScript引擎优化,因此可能会影响应用的整体性能。
- 代码维护性:由字符串生成的代码难以阅读和维护。错误更难被追踪,代码逻辑也更难被其他开发者理解。
- 运行环境:全局作用域
- 代码来源:参数形式为字符串,由开发者预先定义
- 访问权限:只能访问全局对象
- 同步/异步:同步执行
- 安全性和性能:安全性差,可能被用于执行恶意代码;性能同样较差
使用 setTimeout 或 setInterval
<script>
var outerName = 'outer'
function test() {
let localName = 'local' // 不能访问局部变量
const jsString = "var innerName = 'inner'; " + " console.log('这是一段动态执行的js字符串', outerName, innerName) ";
setTimeout(jsString, 2 * 1000)
setTimeout(() => {
console.log('外部打印 innerName:', innerName)
}, 4 * 1000)
}
test();
</script>
向 setTimeout 函数提供一个字符串作为参数时,该函数会把这个字符串当作脚本来解析和执行。这类似于使用 new Function(),也就是说 setTimeout 也是全局作用域运行 不会访问创建它的局部作用域。
字符串内部定义的遍历 外部依然可以访问 会存在污染全局变量的问题。
- 运行环境:全局作用域
- 代码来源:以字符串形式传入,由开发者预先定义
- 访问权限:可以访问全局作用域中的所有变量
- 同步/异步:异步执行
- 安全性和性能:安全性差,可能被用于执行恶意代码;性能一般
使用 Web Workers
<script>
// 创建一个可执行的 JavaScript 代码字符串
var code = `self.onmessage = function(e) {
var result = e.data[0] + e.data[1];
self.postMessage(result);
self.close();
}`;
// 创建一个 Blob 对象来作为 worker 的源
var blob = new Blob([code], { type: 'application/javascript' });
// 根据 Blob 对象创建一个 object URL,并创建 worker
var worker = new Worker(URL.createObjectURL(blob));
// 设置 worker 的消息回调
worker.onmessage = function (e) {
console.log('Result: ' + e.data);
};
// 向 worker 发送数据
worker.postMessage([1, 2]);
</script>
- 运行环境:独立的后台线程,隔离于主线程的执行环境
- 代码来源:外部脚本或由Blob构建的js代码
- 操作对象的访问:无法访问 DOM 和全局对象,但可以通过 postMessage API 进行主线程和 worker 之间的通信
- 同步/异步:异步执行
- 安全性和性能:安全性较好,因为无法访问 DOM;有利于实现复杂或耗时任务的高效处理,不影响主线程
使用 with 语句(不推荐)
JavaScript 的 with 语句本身并不是用来执行 JavaScript 字符串的。它主要被用来修改一个特定的执行环境的作用域。
然而,我们可以将 with 语句与 eval 函数结合使用来在特定的对象环境中执行 JavaScript 字符串。
<script>
var someObject = {
someProperty: 'Hello, world!'
};
with (someObject) {
eval('console.log(someProperty)');
}
</script>
在这个例子中,with 语句更改了 eval 中代码的作用域,使其可以直接访问 someObject 的属性。因此,当 eval 执行字符串 'console.log(someProperty)' 时,它将输出 'Hello, world!'。
- 运行环境:代码内部的指定对象环境作用域
- 代码来源:直接在 with 语句块内部编写
- 访问权限:可以访问和修改指定对象的属性
- 同步/异步:同步执行
- 安全性和性能:可能引发作用域混淆和错误,被认为是 JavaScript 的坏实践,并且在严格模式下被禁止使用。对于性能,with 语句需要动态改变作用域链,可能会带来额外的性能开销
总结
优点
- 灵活性和动态性:可以在运行时根据条件或数据生成并执行代码,从而提高代码的适应性和灵活性
- 按需加载和执行:可以延迟加载和执行代码,减少初始页面加载时间,并按需加载资源
- 条件逻辑实现:可以根据运行时的条件动态生成并执行代码,实现复杂的逻辑控制
- 低代码平台:在低代码或无代码平台中,动态执行 JavaScript 代码允许用户定义的逻辑以编程方式运行,而无需编写传统的代码
缺点:
动态运行字符串的方案都会有安全风险,这些风险主要有以下两个因素引起:
- 代码来源问题:如果你执行的代码是来自不受信任的来源,或者可以被第三方干预,那么存在代码中植入恶意逻辑的风险,这可能会导致一系列严重的安全问题,如注入攻击,跨站脚本攻击等。
- 访问和修改外部数据的风险:动态执行代码通常具有访问和操作(例如:修改、删除)外部数据的能力,如果这个过程被安全机制所忽视或者是被恶意利用,那么也可能引发安全漏洞。比如,恶意代码可能会尝试访问敏感信息(如密码、令牌等),或者修改应用的关键数据。
性能问题:
-
解析和编译:与直接调用事先定义好的函数相比,动态执行字符串代码需要额外的解析(将字符串形式的代码解析成可执行的代码)和编译(编译成机器代码)步骤。这些额外的步骤需要时间,尤其是在必须频繁执行字符串代码的场合,性能损耗将会更加明显。
-
优化难度:现代JavaScript引擎通常对常规的JavaScript代码进行优化,以提高执行效率。然而,对于动态生成且运行的代码,在执行前是不可见的,引擎无法提前知道代码的结构,难以进行相同水平的优化。 另外,因为每次执行动态脚本都是一次全新的脚本执行,这使得引擎难以利用以往的代码信息进行优化。
-
垃圾回收:动态执行的代码可能会频繁创建新的对象和作用域,依赖于这些对象和作用域的动态代码则可以导致JavaScript引擎更频繁地进行垃圾回收,而垃圾回收是影响应用性能的一个关键因素。
建议
如果有场景必须要使用动态运行js字符串的情况下,应优先考虑动态创建 script 标签, new Function()
和 Web Worker。
动态创建 script 标签:这种方式相对安全,可以用于加载外部的 JavaScript 代码,适用于需要动态加载并立即执行脚本的场景。但要尽量避免使用外部不可信的源。
new Function()
提供了较好的安全性和灵活性,适用于需要动态执行但不需要访问外部局部变量的场景。
Web Worker 适合于计算密集型任务,可以避免阻塞 UI 线程,适合于需要长时间运行的后台任务
转载自:https://juejin.cn/post/7407334387933052969