Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wuyunqiang opened this issue Sep 29, 2024 · 0 comments
Open

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

wuyunqiang opened this issue Sep 29, 2024 · 0 comments

Comments

@wuyunqiang
Copy link
Owner

先有问题再有答案

  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>

截屏2024-08-20 20.56.33.png

  • 运行环境:当前作用域,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 函数的作用域闭包机制不同。

优点:

  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 也是全局作用域运行 不会访问创建它的局部作用域。

截屏2024-08-20 20.53.58.png

字符串内部定义的遍历 外部依然可以访问 会存在污染全局变量的问题。

  • 运行环境:全局作用域
  • 代码来源:以字符串形式传入,由开发者预先定义
  • 访问权限:可以访问全局作用域中的所有变量
  • 同步/异步:异步执行
  • 安全性和性能:安全性差,可能被用于执行恶意代码;性能一般

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

总结

优点

  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 线程,适合于需要长时间运行的后台任务

@github-staff github-staff deleted a comment Sep 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant