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") :一行让严格模式形同虚设的破坏性设计(下)

相关推荐
pixle020 分钟前
Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码
前端·3d·echarts
麻芝汤圆1 小时前
MapReduce 入门实战:WordCount 程序
大数据·前端·javascript·ajax·spark·mapreduce
juruiyuan1113 小时前
FFmpeg3.4 libavcodec协议框架增加新的decode协议
前端
Peter 谭3 小时前
React Hooks 实现原理深度解析:从基础到源码级理解
前端·javascript·react.js·前端框架·ecmascript
周胡杰4 小时前
鸿蒙接入flutter环境变量配置windows-命令行或者手动配置-到项目的创建-运行demo项目
javascript·windows·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
LuckyLay5 小时前
React百日学习计划——Deepseek版
前端·学习·react.js
gxn_mmf5 小时前
典籍知识问答重新生成和消息修改Bug修改
前端·bug
hj10435 小时前
【fastadmin开发实战】在前端页面中使用bootstraptable以及表格中实现文件上传
前端
乌夷5 小时前
axios结合AbortController取消文件上传
开发语言·前端·javascript
晓晓莺歌5 小时前
图片的require问题
前端