前言
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
eval
和 new 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 )
- EvalDeclarationInstantiation(body, varEnv, lexEnv, privateEnv, strictEval)
直接调用和间接调用
PerformEval ( code, strictCaller, direct )
第一个code参数很好理解, 后面的两个参数就很有意思
- strictCaller:是否是严格模式下调用。
- direct: 是不是直接调用。 what ? 这又有什么作用呢? 会影响函数的执行逻辑。
那么问题来了,直接调用的定义是啥? 从协议 13.3.6.1 Runtime Semantics: Evaluation 可以挖出定义

直接调用条件:
- 是一个引用记录
- 不是属性引用。 window.eval 不行, object.eval 也不行。
- 引用名必须等于
"eval"
。 所以var evval = window.eval
已经不行 - 值必须等于内置的
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
}
限制
- 只能创建普通函数,不能创建生成器函数,异步函数,异步生成器函数
- 返回的函数的 name 属性均是
anonymous
- 默认的执行上下文是 全局执行上下文。 这个时候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}'
经典案例
- webapck
很多能可能谁说,正经人谁会这么写。 其实两年以前,最近两年不好说,你天天用。大名鼎鼎的 webpack 的事件模块 tapable 就很巧妙的用了动态函数。


如果你真用到动态函数,简称你为 "高玩" 吧。
- fast-json-stringify
第二个库也是周下载量超过百万的 fast-json-stringify, 其比系统内置的 JSON.stringify
速度快,开销小,
其原理就是 根绝 JSON Schema 预先生成处理函数。 至于什么是 JSON Schema ,会有单独的文章介绍。

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

这个提个问题, NodeJS 环境有没有这个限制呢?
动态创建其他类型函数
示例函数 | 英文 | 中文 |
---|---|---|
function*(){} |
GeneratorFunction | 生成器函数 |
async function(){} |
AsyncFunction | 异步函数 |
async function*(){} |
AsyncGeneratorFunction | 异步生成器函数 |
上面三种函数类型,你通过 new Function
是无法创建的,要想动态创建这些类型的函数,怎么办呢?
思考:
- 函数本身也是函数的实例, 所以函数也是被构造出来的,
- 函数都有一个
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") :一行让严格模式形同虚设的破坏性设计(下)