动态代码执行
JavaScript 动态代码执行
我们写的 JS 代码,主要执行在浏览器环境和 node 环境,也叫宿主环境。宿主环境通过加载机制获取到我们的代码,然后使用 JS 引擎解释执行。这是正常的 JS 代码执行流程。有些场景下,js 代码是通过程序动态生成的,此时我们已经运行在 JS 引擎内部,没有宿主环境帮我们执行代码,就需要 JS 引擎提供的动态执行代码的能力。
动态执行代码普遍存在两个缺点,一是安全性问题,传递的函数体字符串如果包含非法代码也会被执行。二是执行性能,动态执行时会解析代码,存在一定的时间消耗。eval 还会影响到 js 引擎的优化过程,导致效率降低非常多。eval is evil 说的就是使用 eval 可能导致很严重的问题。从上面几种方法可以看到,理解 js 动态执行代码时的作用域是关键。一是是否存在自己的作用域,二是其父作用域是当前作用域还是全局作用域。只要记住这两个问题的答案,在使用时就不会出现大问题。
new Function
Function 构造函数创建一个函数对象,这个函数对象和使用函数声明和函数表达式创建的一样,区别是函数的解析时机不同。Function 构造函数是在执行时解析,后者是在脚本加载时解析。Function 创建的函数有自己的作用域,其父作用域是全局作用域,只能访问全局变量和自己的局部变量,不能访问函数被创建时所在的作用域。需要注意在 node 环境和 esm 环境存在模块作用域,模块作用域不是全局作用域。Function 创建的函数也不能访问模块作用域。
var a = -100;
(function () {
var a = 1;
// 函数执行时的父作用域时全局作用域
new Function("console.log(a)")(); // -100
// 内部的 this 是 window
var nfunc = new Function("return this");
console.log(nfunc()); // Window
// 作为对象的方法时, this 是当前对象
var obj = { nfunc: nfunc };
console.log(obj.nfunc()); // {nfunc: ƒ}
})();
eval
eval 没有自己的作用域,而是使用执行时所在的作用域,在 eval 中初始化语会将变量加入到当前作用域。由于变量是在运行时动态添加的,导致 v8 引擎不能做出正确的判断,只能放弃优化策略。在严格模式下,eval 有自己的作用域,这样就不会污染当前作用域。
(function () {
// eval 没有自己的作用域,使用当前作用域。
var a = 100;
eval("console.log(a)"); // 100
// 初始化语句会添加变量到当前作用域上,也就是会污染当前作用域。这是 v8 引擎没法优化这段代码的原因,也是性能差的原因。
eval("var b = 20");
console.log(b); // 20
})();
(function () {
"use strict";
// 严格模式下,eval 有自己的作用域,父作用域是当前作用域。
var a = 100;
eval("console.log(a)"); // 100 当前作用域上的 a
// 严格模式下,eval 有自己的作用域,初始化语句将变量添加到自己的作用域内。执行完后当前作用域被销毁
eval("var b = 20");
console.log(b); // 1 全局作用域上的 b
// 返回 eval 代码段产生的闭包
var innerb = eval("var b = 20; (function () { return b })")();
console.log("innerb", innerb); // innerb 20
})();
值得注意的是,eval 如果不使用 direct call 的方式调用,其使用的作用域将会变为全局作用域。
var a = 0;
(function () {
var a = 100;
var fn = eval;
// 非 direct call 的调用方式
fn("console.log(a)"); // 0
})();
setTimeout
setTimeout 用来设置定时器,其第一个参数可以传入函数,也可以传入代码片段。传入函数时,函数的作用域是正常的函数作用域。传入代码片段时,没有自己的作用域,其执行时作用域是全局作用域。
var a = -100;
(function () {
var a = 0;
// setTimeout 执行的代码段,没有自己的作用域,运行在全局作用域中
var dynameicCode = "console.log(a)";
setTimeout(dynameicCode, 10); // -100
// setTimeout 执行的代码段,初始化语句会添加变量到 window 上
var dynameicCode = "var b = 200;";
setTimeout(dynameicCode, 10);
setTimeout(function () {
console.log("window.b", window.b); // window.b 200
}, 20);
})();
script.textContent
动态创建 script 节点,也是一种动态执行语句的方式。其创建的 script 和普通 script 没有区别,代码的作用域是全局作用域。需要注意 script 应该使用 document.createElement(‘script’) 创建并插入到文档中。使用 innerHTML 插入 script 的方式,脚本不会执行。
(function () {
var a = 1;
var s = document.createElement("script");
s.textContent = "console.log(a)";
document.documentElement.append(s);
})();
onclick=“xxx”
html 元素的 onclick 属性也支持设置 js 代码,这种特性被称为 Inline event handlers。这种方式执行的代码存在自己的作用域,父作用域是全局作用域。也就是说初始化语句不会污染全局作用域。
<script>
var a = -100;
var b = 2;
</script>
<!-- 点击按钮输出 -100 200 -->
<button onclick="var b = 200; console.log(a, b);">click me</button>
<!-- 执行成功后,在控制台检查,全局作用域内并没有变量 b -->