eval
函数
const jsStr = `
var innerDataVar = 'innerDataVar'
const innerDataConst = 'innerDataConst'
console.log('[Inner]:', globalData)
console.log('[Inner]:', localData)
`
const globalData = 'globalData'
function test() {
const localData = 'localData'
// (1) eval
eval(jsStr)
console.log('[Outer]:', innerDataVar)
console.log('[Outer]:', innerDataConst)
}
test()
// [Inner]: globalData
// [Inner]: localData
// [Outer]: innerDataVar
// Uncaught ReferenceError: innerDataConst is not defined
- 运行环境:函数作用域,可以访问或修改当前作用域中的变量
- 外部访问内部数据:
eval
会开辟一个块级作用域 - 同步/异步:同步执行
配合 with
语句
const someObject = {
someProperty: 'Hello, world!'
}
with (someObject) {
eval('console.log(someProperty)')
}
// Hello, world!
在这个例子中,with
语句更改了 eval
中代码的作用域,使其可以直接访问 someObject
的属性
<script>
元素
const jsStr = `
var innerDataVar = 'innerDataVar'
const innerDataConst = 'innerDataConst'
console.log('[Inner]:', globalData)
console.log('[Inner]:', localData)
`
const globalData = 'globalData'
function test() {
const localData = 'localData'
// (2) <script> 元素
// 创建一个新的 <script> 元素
const script = document.createElement('script')
script.type = 'text/javascript'
// 将 JS 字符串设置为 script 元素的 textContent
script.textContent = jsStr
// 将 script 元素添加到页面的 head 或 body 中
document.head.appendChild(script)
console.log('[Outer]:', innerDataVar)
console.log('[Outer]:', innerDataConst)
}
test()
// [Inner]: globalData
// Uncaught ReferenceError: localData is not defined
// [Outer]: innerDataVar
// [Outer]: innerDataConst
Webpack 动态加载的原理就是这个方案
- 运行环境:全局作用域,不可访问函数作用域中的变量
- 外部访问内部数据:可访问
- 同步/异步:取决于何时将
<script>
添加到 DOM 中。上述代码就是同步的
new Function()
构造函数
const jsStr = `
var innerDataVar = 'innerDataVar'
const innerDataConst = 'innerDataConst'
console.log('[arguments]:', arguments)
console.log('[Inner]:', globalData)
console.log('[Inner]:', localData)
return 'result'
`
const globalData = 'globalData'
function test() {
const localData = 'localData'
// (3) new Function()
// 使用 new Function() 来创建一个新的函数
const func = new Function(jsStr)
// 调用这个新创建的函数
const result = func(1, 2)
console.log('[Outer]:', innerDataVar)
console.log('[Outer]:', innerDataConst)
console.log('[result]:', result)
}
test()
// [arguments]: Arguments(2) [1, 2, length: 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// [Inner]: globalData
// Uncaught ReferenceError: localData is not defined
// Uncaught ReferenceError: innerDataVar is not defined
// Uncaught ReferenceError: innerDataConst is not defined
// [result]: result
- 运行环境:全局作用域,不可访问函数作用域中的变量。只能访问全局变量,或者显示传入的变量参数
- 需要注意的是通过
new Function()
创建的函数不会继承其创建时的函数作用域 - 与普通 JavaScript 函数的作用域闭包机制不同
- 需要注意的是通过
- 外部访问内部数据:不可访问
- 同步/异步:同步执行
setTimeout
/ setInterval
函数
const jsStr = `
var innerDataVar = 'innerDataVar'
const innerDataConst = 'innerDataConst'
console.log('[Inner]:', globalData)
console.log('[Inner]:', localData)
`
const globalData = 'globalData'
function test() {
const localData = 'localData'
// (4) setTimeout / setInterval
setTimeout(jsStr, 0)
console.log('[Outer]:', innerDataVar)
console.log('[Outer]:', innerDataConst)
setTimeout(() => {
console.log('[setTimeout]:', innerDataVar)
console.log('[setTimeout]:', innerDataConst)
}, 1000)
}
test()
// Uncaught ReferenceError: innerDataVar is not defined
// Uncaught ReferenceError: innerDataConst is not defined
// [Inner]: globalData
// Uncaught ReferenceError: localData is not defined
// [setTimeout]: innerDataVar
// [setTimeout]: innerDataConst
- 运行环境:全局作用域
- 外部访问内部数据:可访问
- 同步/异步:异步执行
Web Worker
// 创建一个可执行的 JS 代码字符串
const jsStr = `
self.onmessage = function(e) {
var result = e.data[0] + e.data[1];
self.postMessage(result);
self.close();
}`
// 创建一个 Blob 对象来作为 worker 的源
const blob = new Blob([jsStr], { type: 'application/javascript' })
// 根据 Blob 对象创建一个 object URL,并创建 worker
const worker = new Worker(URL.createObjectURL(blob))
// 设置 worker 的消息回调
worker.onmessage = function (e) {
console.log('Result: ' + e.data)
}
// 向 worker 发送数据
worker.postMessage([1, 2])
console.log('[主线程]')
// [主线程]
// Result: 3
- 运行环境:独立的后台线程,隔离于主线程的执行环境
- 操作对象的访问:无法访问 DOM 和全局对象,但可以通过 postMessage API 进行主线程和 worker 之间的通信
- 同步/异步:异步执行
- 性能:有利于实现复杂或耗时任务的高效处理,不影响主线程
总结
优点
- 灵活性和动态性:可以在运行时根据条件或数据生成并执行代码,从而提高代码的适应性和灵活性
- 按需加载和执行:可以延迟加载和执行代码,减少初始页面加载时间,并按需加载资源
- 条件逻辑实现:可以根据运行时的条件动态生成并执行代码,实现复杂的逻辑控制
- 低代码平台:在低代码或无代码平台中,动态执行 JS 代码允许用户定义的逻辑以编程方式运行,而无需编写传统的代码
缺点
动态运行字符串的方案都会有安全风险,这些风险主要有以下两个因素引起:
- 代码来源问题:如果你执行的代码是来自不受信任的来源,或者可以被第三方干预,那么存在代码中植入恶意逻辑的风险,这可能会导致一系列严重的安全问题,如注入攻击,跨站脚本攻击等
- 访问和修改外部数据的风险:动态执行代码通常具有访问和操作(例如:修改、删除)外部数据的能力,如果这个过程被安全机制所忽视或者是被恶意利用,那么也可能引发安全漏洞。比如,恶意代码可能会尝试访问敏感信息(如密码、令牌等),或者修改应用的关键数据
性能问题:
- 解析和编译:与直接调用事先定义好的函数相比,动态执行字符串代码需要额外的解析(将字符串形式的代码解析成可执行的代码)和编译(编译成机器代码)步骤。这些额外的步骤需要时间,尤其是在必须频繁执行字符串代码的场合,性能损耗将会更加明显
- 优化难度:现代 JavaScript 引擎通常对常规的 JavaScript 代码进行优化,以提高执行效率。然而,对于动态生成且运行的代码,在执行前是不可见的,引擎无法提前知道代码的结构,难以进行相同水平的优化。另外,因为每次执行动态脚本都是一次全新的脚本执行,这使得引擎难以利用以往的代码信息进行优化
- 垃圾回收:动态执行的代码可能会频繁创建新的对象和作用域,依赖于这些对象和作用域的动态代码则可以导致 JavaScript 引擎更频繁地进行垃圾回收,而垃圾回收是影响应用性能的一个关键因素
建议
如果有场景必须要使用动态运行 JS 字符串的情况下,应优先考虑动态创建 <script>
标签、new Function()
和 Web Worker
- 动态创建
<script>
标签:这种方式相对安全,可以用于加载外部的 JS 代码,适用于需要动态加载并立即执行脚本的场景,但要尽量避免使用外部不可信的源 new Function()
提供了较好的安全性和灵活性,适用于需要动态执行但不需要访问外部局部变量的场景- Web Worker 适合于计算密集型任务,可以避免阻塞 UI 线程,适合于需要长时间运行的后台任务