ECMAScript 函数之动态执行脚本

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

准备知识

函数类别,具体看表格。 kind类别,后续协议中会提到。

示例函数 协议英文名 中文 kind
function (){} Function 普通函数 normal
function*(){} GeneratorFunction 生成器函数 generator
async function(){} AsyncFunction 异步函数 async
async function*(){} AsyncGeneratorFunction 异步生成器函数 asyncGenerator

[[ThisMode]]

函数对象的内置属性。定义了如何在函数的形式参数和代码主体中解释this引用。

协议描述参见 Internal Slots of ECMAScript Function Objects的表格。

[[ThisMode]] lexical 当前函数为箭头函数。函数没有自己的this, 上下文从函数环境记录的外围去借this。
strict 严格模式。完全按照函数调用所提供的方式使用,不会对this进行包装或者改变。
global 值为 global 意味着当 null 或者 undefined 为 this 绑定值时会被解析为对全局对象的引用。

HostEnsureCanCompileStrings

就是说宿主环境,(nodejs 环境和浏览器环境)都是宿主环境,允不允许开发人员将字符串解释为 ECMAScript 代码并对其进行计算。

浏览器宿主环境 html的协议规范在 8.1.6.2 HostEnsureCanCompileStrings(realm), 协议本身又指向了 CSP的协议内容 4.4.1. EnsureCSPDoesNotBlockStringCompilation(realm, source)

所以浏览器环境,CSP的配置,决定了开发者能不能编译字符串代码并运行。

恶魔终结者-CSP

evalnew Function 可能会因为CSP策略的设置,而不被允许被调用。详情参见 Content Security Policy (CSP)6.1.10. script-src

所以你把相关代码贴入 控制太的时候,很可能并不能正常执行, 出现如下截图内容:

那么怎么办呢?

  • nodejs 环境执行
  • chrome 打开 chrome://webui-test/, 然后执行

动态执行脚本两种方式

动态执行脚本有两种方法

  • eval
    eval是传入源码文本,直接执行
  • new Function
    new Function是返回一个函数,然后可以执行这个函数
javascript 复制代码
eval(`console.log(1)`)   // 1

const fn = new Function(`console.log(1)`)
fn()                    // 1

eval

协议调用链路

直接调用和间接调用

PerformEval ( code, strictCaller, direct )

第一个code参数很好理解, 后面的两个参数就很有意思

  • strictCaller:是否是严格模式下调用。
  • direct: 是不是直接调用。 what ? 这又有什么作用呢? 会影响函数的执行逻辑。

那么问题来了,直接调用的定义是啥? 从协议 13.3.6.1 Runtime Semantics: Evaluation 可以挖出定义

直接调用条件:

  1. 是一个引用记录
  2. 不是属性引用。 window.eval 不行, object.eval 也不行。
  3. 引用名必须等于 "eval"。 所以 var evval = window.eval已经不行
  4. 值必须等于内置的 eval函数。

满足上面条件的 引用 eval 进行调用,就称为直接调用。 1和4非常容易满足,关键是 2,3。 如下的均是直接调用

直接调用:

javascript 复制代码
eval("1");    	  // 直接调用

(eval)("1");      // 直接调用


var eval = window.eval;
var obj = {
  name: 'name',
  test(){
      // 直接调用
      console.log(eval(`this.name`))
  }
};
obj.test()

with ({ eval }) {        
   var obj = {
      name: 'name',
      test(){
          // 直接调用
          console.log(eval(`this.name`))
      }
  };
    obj.test()
}

间接调用:

javascript 复制代码
var eeval = globalThis.eval;   // 间接调用, 变量名不等于 eval
eeval("1")

window.eval("1")  // 间接调用, 因为是属性引用

调用时的环境记录

直接调用和间接调用 环境记录的设置是不相同的,最直接的体现是外围的环境记录不一样。

  • 直接调用,会新建一个申明环境记录,其外围环境记录是当前执行上下文的词法环境记录。
  • 间接调用,会新建一个申明环境记录,其外围环境记录是全局环境记录。

19.2.1.1 PerformEval ( x, strictCaller, direct ) 协议描述:

外在的表现之一就是 this的不同。看如下的示例,直观的感觉一下,环境的不同。

javascript 复制代码
window.eName = 'Global eName';

var obj = {
  direct(){
    console.log(eval(`(this.eName)`));         // 直接调用
  },
  notDirect(){
    console.log(window.eval(`(this.eName)`));  // 间接调用
  },
	eName: 'Object eName'
}

obj.direct();    // Object eName
obj.notDirect()  // Global eName

eval 是恶魔, 基本不会使用,了解这个直接调用和间接调用,基本就满足日常了。 真要动态执行脚本,现在都推荐使用的是 new Function

new Function

基本使用

javascript 复制代码
const sum = new Function(`num1`, `num2`, `return num1 + num2`);
console.log(sum(1, 2));  // 3

前面的参数都是形参名,最后一个参数是函数体的源码文本。

如果只有一个参数呢? 那就是生成的函数没有形参。

如果没有参数呢? 那就是生成的函数没有形参,函数体也没有语句。

协议实现逻辑

其底层调用的是 CreateDynamicFunction,注意第三个参数传递的是 kind 传递的是 normal, 具体映射查看准备知识的表格。 所以其只能创建普通函数。

上图注意到有两个 ParseText出现,这是要把源码文本转为对应的解析节点。 还会做静态语义检查,一个检查额是 参数语义,一个检查的是函数体的语义。 如果有错误,抛出的是语法错误 SyntaxError 。

比如如下代码:很明显,参数 var aaa是不正确的。

javascript 复制代码
new Function(`var aaa`, 'return aaa')
// Uncaught SyntaxError: Unexpected token 'var'

一起看看函数的拼接逻辑:

其实还有三个\n没有标记

要是写入文件保存下来,就是下面的代码

javascript 复制代码
function anonymous(a,b
) {
a+b
}

限制

  1. 只能创建普通函数,不能创建生成器函数,异步函数,异步生成器函数
  2. 返回的函数的 name 属性均是 anonymous
  3. 默认的执行上下文是 全局执行上下文。 这个时候this 是 全局对象。 这里说的是默认,因为之后还是可以改变的额。
javascript 复制代码
window.name = 'window name';
var logName = new Function(`return console.log(this.name)`);
logName();                                // window name
logName.bind({name: "object name"})()     // object name

变体

javascript 复制代码
const fn = Function.apply(null, ['a', 'b', 'return a + b']);

fn(10, 20)    // 30
fn.toString() // 'function anonymous(a,b\n) {\nreturn a + b\n}'

经典案例

  1. webapck

很多能可能谁说,正经人谁会这么写。 其实两年以前,最近两年不好说,你天天用。大名鼎鼎的 webpack 的事件模块 tapable 就很巧妙的用了动态函数。

如果你真用到动态函数,简称你为 "高玩" 吧。

  1. fast-json-stringify

第二个库也是周下载量超过百万的 fast-json-stringify, 其比系统内置的 JSON.stringify速度快,开销小,

其原理就是 根绝 JSON Schema 预先生成处理函数。 至于什么是 JSON Schema ,会有单独的文章介绍。

  1. generate-function

这个库比较老了,但是周下载量依然在百万级别。作用就是动态生成函数。

这个提个问题, NodeJS 环境有没有这个限制呢?

动态创建其他类型函数

示例函数 英文 中文
function*(){} GeneratorFunction 生成器函数
async function(){} AsyncFunction 异步函数
async function*(){} AsyncGeneratorFunction 异步生成器函数

上面三种函数类型,你通过 new Function是无法创建的,要想动态创建这些类型的函数,怎么办呢?

思考:

  1. 函数本身也是函数的实例, 所以函数也是被构造出来的,
  2. 函数都有一个 constructor属性,指向其构造函数。
javascript 复制代码
(function*{}).constructor           // ƒ GeneratorFunction() { [native code] }
(async function(){}).constructor    // ƒ AsyncFunction() { [native code] }
(async function*(){}).constructor   // ƒ AsyncGeneratorFunction() { [native code] }  

所以呢, 演示一个AsyncFunction, 大家都秒成懂王,懂的都懂。

javascript 复制代码
const GeneratorFunction = (function*(){}).constructor;
const AsyncFunction = (async function(){}).constructor;
const AsyncGeneratorFunction = (async function*(){}).constructor;

const asyncFun = new AsyncFunction("return (await 10)");
asyncFun().then( res=> console.log('asyncFun:', res))  // asyncFun: 10

引用

20 | (0, eval)("x = 100") :一行让严格模式形同虚设的破坏性设计(上)
21 | (0, eval)("x = 100") :一行让严格模式形同虚设的破坏性设计(下)

相关推荐
高木的小天才5 分钟前
鸿蒙中的并发线程间通信、线程间通信对象
前端·华为·typescript·harmonyos
Danta1 小时前
百度网盘一面值得look:我有点难受🤧🤧
前端·javascript·面试
OpenTiny社区1 小时前
TinyVue v3.22.0 正式发布:深色模式上线!集成 UnoCSS 图标库!TypeScript 类型支持全面升级!
前端·vue.js·开源
dwqqw1 小时前
opencv图像库编程
前端·webpack·node.js
Captaincc2 小时前
为什么MCP火爆技术圈,普通用户却感觉不到?
前端·ai编程
海上彼尚2 小时前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
阿虎儿3 小时前
MCP
前端
layman05283 小时前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝3 小时前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML3 小时前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能