首页/文章/javascript

六种动态运行js字符串的方案对比

2024-08-27
306511 分钟
...

先有问题再有答案

  1. 动态运行js字符串的方案有哪些?
  2. 这些方案在运行环境,代码来源,访问权限,执行时机上有什么差异?
  3. 为什么我们需要动态执行js字符串?
  4. 为什么大家都不建议使用动态运行js字符串的方案?
  5. 如果必须要选择一种方案 我们应该推荐哪种方案?

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();";

        
        var fun = new Function(jsString);

        
        var result = fun();
    }
    test();

需要注意的是 通过new Function() 创建的函数不会继承其创建时的局部作用域。
这种特殊的作用域行为,与普通 JavaScript 函数的作用域闭包机制不同。

优点:

  1. 隔离作用域:使用 new Function() 创建的函数不会访问到局部作用域(闭包作用域),它们只能访问全局变量, 或者显示传入的变量参数。
  2. 避免变量污染 : 字符串内部定义的变量在外部无法获取,不会污染全局 。

缺点

  1. 安全风险:执行从不可信来源动态生成的代码总是风险很高的操作。如果代码中包含恶意内容,则可能导致安全漏洞,如XSS攻击或远程代码执行。
  2. 性能问题:使用 new Function() 生成的函数可能比普通函数执行效率低,因为它们在运行时需要经过编译和解析过程。而且,由于它们通常无法被JavaScript引擎优化,因此可能会影响应用的整体性能。
  3. 代码维护性:由字符串生成的代码难以阅读和维护。错误更难被追踪,代码逻辑也更难被其他开发者理解。
  • 运行环境:全局作用域
  • 代码来源:参数形式为字符串,由开发者预先定义
  • 访问权限:只能访问全局对象
  • 同步/异步:同步执行
  • 安全性和性能:安全性差,可能被用于执行恶意代码;性能同样较差

使用 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>
    
    var code = `self.onmessage = function(e) {
  var result = e.data[0] + e.data[1];
  self.postMessage(result);
  self.close();
}`;

    
    var blob = new Blob([code], { type: 'application/javascript' });

    
    var worker = new Worker(URL.createObjectURL(blob));

    
    worker.onmessage = function (e) {
        console.log('Result: ' + e.data);
    };

    
    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 语句需要动态改变作用域链,可能会带来额外的性能开销

总结

优点

  1. 灵活性和动态性:可以在运行时根据条件或数据生成并执行代码,从而提高代码的适应性和灵活性
  2. 按需加载和执行:可以延迟加载和执行代码,减少初始页面加载时间,并按需加载资源
  3. 条件逻辑实现:可以根据运行时的条件动态生成并执行代码,实现复杂的逻辑控制
  4. 低代码平台:在低代码或无代码平台中,动态执行 JavaScript 代码允许用户定义的逻辑以编程方式运行,而无需编写传统的代码

缺点:

动态运行字符串的方案都会有安全风险,这些风险主要有以下两个因素引起:

  1. 代码来源问题:如果你执行的代码是来自不受信任的来源,或者可以被第三方干预,那么存在代码中植入恶意逻辑的风险,这可能会导致一系列严重的安全问题,如注入攻击,跨站脚本攻击等。
  2. 访问和修改外部数据的风险:动态执行代码通常具有访问和操作(例如:修改、删除)外部数据的能力,如果这个过程被安全机制所忽视或者是被恶意利用,那么也可能引发安全漏洞。比如,恶意代码可能会尝试访问敏感信息(如密码、令牌等),或者修改应用的关键数据。

性能问题

  1. 解析和编译:与直接调用事先定义好的函数相比,动态执行字符串代码需要额外的解析(将字符串形式的代码解析成可执行的代码)和编译(编译成机器代码)步骤。这些额外的步骤需要时间,尤其是在必须频繁执行字符串代码的场合,性能损耗将会更加明显。

  2. 优化难度:现代JavaScript引擎通常对常规的JavaScript代码进行优化,以提高执行效率。然而,对于动态生成且运行的代码,在执行前是不可见的,引擎无法提前知道代码的结构,难以进行相同水平的优化。 另外,因为每次执行动态脚本都是一次全新的脚本执行,这使得引擎难以利用以往的代码信息进行优化。

  3. 垃圾回收:动态执行的代码可能会频繁创建新的对象和作用域,依赖于这些对象和作用域的动态代码则可以导致JavaScript引擎更频繁地进行垃圾回收,而垃圾回收是影响应用性能的一个关键因素。

建议

如果有场景必须要使用动态运行js字符串的情况下,应优先考虑动态创建 script 标签new Function() 和 Web Worker。

动态创建 script 标签:这种方式相对安全,可以用于加载外部的 JavaScript 代码,适用于需要动态加载并立即执行脚本的场景。但要尽量避免使用外部不可信的源。

new Function() 提供了较好的安全性和灵活性,适用于需要动态执行但不需要访问外部局部变量的场景。

Web Worker 适合于计算密集型任务,可以避免阻塞 UI 线程,适合于需要长时间运行的后台任务

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-10
手把手教你实现一个EventEmitter
你是不是也遇到过这种情况:代码里各种事件监听和触发,回调函数套了一层又一层,最后自己都理不清哪个事件先触发、哪个后触发了? 别担心,今天我就带你从零开始实现一个自己的事件触发器(EventEmitter),让你彻底掌握事件驱动的精髓,代码清晰度直接提升200%! 为什么要懂EventEmitter? 先说说为什么我们要关心这个。Node.js的核心就是事件驱动,比如文件读写、网络请求,都是通过事件来处理的。不会EventEmitter,...
学习面试
javascript2025-08-09
Web Worker 多线程魔法
你是否曾遇到过网页因为复杂计算而卡顿的情况?今天,让我们一起探索Web Worker的神奇世界,看看如何用多线程技术解决性能瓶颈,并实现流畅的图片压缩功能! 为什么需要Web Worker? 想象一下,你在一个繁忙的餐厅里,只有一位服务员(主线程)负责点餐、上菜、结账所有工作。当客人突然暴增时,这位服务员就会手忙脚乱,其他客人只能干等着… 这就是JavaScript的现状—— 单线程 !所有任务都在一个线程中排队执行,一旦遇到复杂计算(...
学习面试
javascript2025-08-07
🌐AST抽象语法树
AST 是前端开发中一个非常重要的概念,它就像是代码的“编译器”,能够帮助我们理解和操作代码。本文将深入探讨 AST 的原理和应用,带你从零开始了解 AST。 一、什么是 AST? AST(抽象语法树)是一种树形结构,用于表示程序代码的语法结构。它将代码分解为一系列的节点,每个节点代表代码中的一个元素,如变量、函数、表达式等。AST 是编译器和解释器的基础,也是代码分析和转换的关键工具。 1\. AST 的结构 AST 的结构通常包括以...
学习面试
javascript2025-08-05
打字机效果是如何实现的
分析效果 使用 AI 对话的时候,文字是一个字一个字蹦出来的?它不是等所有内容生成完毕后一次性展示,而是以一种流畅、实时的“打字机”效果呈现,这种体验的背后,隐藏着一种高效的Web技术。它就是 服务器推送事件(Server Sent Events,简称 SSE) 什么是 SSE 从根本上说,SSE 是一种允许服务器 单向 、 持续 地向客户端(浏览器)推送数据的Web技术。 可以把它想象成一个 电台广播 : 服务器 是广播电台,持续不断...
学习面试
javascript2025-08-04
Promise 从原理到实现
前言 是 异步编程的核心概念之一,它为我们提供了一种更优雅的方式来处理异步操作。 本文将全面剖析 的工作原理,并逐步实现一个符合 规范的 。 正文 一、Promise 基础概念 代表一个异步操作的最终完成(或失败)及其结果值。它有 种状态: 1. pending : ,既不是成功,也不是失败状态 2. fulfilled :意味着操作 完成 3. rejected :意味着操作 状态一旦改变就不能再变,只能从 变为 ,或从 变为 。 二...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录