>
NOTE:关于概念完整性以及它在“体系性”中的价值,参见《[加餐3:让JavaScript运行起来](https://time.geekbang.org/column/article/175261)》。
在ECMAScript 6之后,有赖于类继承体系的提出,JavaScript中的函数也获得了“子类化”的能力,于是用户代码也可以派生函数的子类了。例如:
```
class MyFunction extends Function {
// ...
}
```
但是用户代码无法重载“函数的执行”能力。很明显,这是执行引擎自身的能力,除非你可以重写引擎,否则重载执行能力也就无从谈起。
>
NOTE:关于类、派生,以及它们在对原生构造器进行派生时的贡献,请参见《[第15讲](https://time.geekbang.org/column/article/179238)》。
除了这种用户自定义的子类化的函数之外,JavaScript中一共只有四种可以动态创建的函数,包括:**一般函数**(Function)、**生成器函数**(GeneratorFunction)、**异步生成器函数**(AsyncGeneratorFunction)和**异步函数**(AsyncFunction)。又或者说,用户代码可以从这四种函数之任一开始来派生它们的子类,在保留它们的执行能力的同时,扩展接口或功能。
但是,这四种函数在JavaScript中有且只有`Function()`是显式声明的,其他三种都没有直接声明它们的构造器,这需要你用如下代码来得到:
```
const GeneratorFunction = (function* (){}).constructor;
const AsyncGeneratorFunction = (async function* (){}).constructor
const AsyncFunction = (async x=>x).constructor;
// 示例
(new AsyncFunction)().then(console.log); // promise print 'undefined'
```
### 函数的三个组件
我们提及过函数的三个组件,包括:**参数**、**执行体**和**结果**。其中“结果(Result)”是由代码中的return子句负责的,而其他两个组件,则是“动态创建一个函数”所必须的。这也是上述四个函数(以及它们的子类)拥有如下相同界面的原因:
Function (p1, p2, … , pn, body)
>
NOTE:关于函数的三个组件,以及基于它们的变化,请参见《[第8讲](https://time.geekbang.org/column/article/171617)、[第9讲](https://time.geekbang.org/column/article/172636)、[第10讲](https://time.geekbang.org/column/article/174314),它们分别讨论“三个组件”、改造“执行体”,以及改造“参数和结果”》。
其中,用户代码可以使用字符串来指定p1…pn的形式参数(Formals),并且使用字符串来指定函数的执行体(Body)。类似如下:
```
f = new Function('x', 'y', 'z', 'console.log(x, y, z)');
// 测试
f(1,2,3); // 1 2 3
```
JavaScript也允许用户代码将多个参数合写为一个,也就是变成类似如下形式:
```
f = new Function('x, y, z', ...);
```
或者在字符串声明中使用缺省参数等扩展风格,例如:
```
f = new Function('x = 0, ...args', 'console.log(x, ...args)');
f(undefined, 200, 300, 400); // 0 200 300 400
```
### 动态函数的创建过程
所有的四种动态函数的创建过程都是一致的,它们都将调用内部过程[CreateDynamicFunction()](https://tc39.es/ecma262/#sec-createdynamicfunction)来创建函数对象。但相对于静态声明的函数,动态创建(CreateDynamicFunction)却有自己不同的特点与实现过程。
>
NOTE:关于对象的构造过程,请参见《[第13讲(13 | new X)](https://time.geekbang.org/column/article/177397)》。
JavaScript在创建函数对象时,会为它分配一个称为“allocKind”的标识。相对于静态创建,这个标识在动态创建过程中反而更加简单,正好与上述四种构造器一一对应,也就不再需要进行语法级别的分析与识别。其中除了`normal`类型(它所对应的构造器是`Function()`)之外,其他的三种都不能作为构造器来创建和初始化。所以,只需要简单地填写它们的内部槽,并置相应的原型(原型属性F.prototype以及内部槽F.[[Prototype]])就可以了。
最后,当函数作为对象实例完成创建之后,引擎会调用一个称为“函数初始化(FunctionInitialize)”的内置过程,来初始那些与具体实例相关的内部槽和外部属性。
>
NOTE:在ECMAScript 6中,动态函数的创建过程主要由FunctionAllocate和FunctionInitialize两个阶段完成。而到了ECMAScript 9中,ECMAScript规范将FunctionAllocate的主要功能归入到OrdinaryFunctionCreate中(并由此规范了函数“作为对象”的创建过程),而原本由FunctionInitialize负责的初始化,则直接在动态创建过程中处理了。
然后呢?然后,函数就创建完了。
是的!“好像”什么也没有发生?!事实上,在引擎层面,所谓的“动态函数创建”就是什么也没有发生,因为执行引擎并不理解“声明一个函数”与“动态创建一个函数”之间的差异。
我们试想一下,如果一个执行引擎要分别理解这两种函数并尝试不同的执行模式或逻辑,那么这个引擎的效率得有多差。
## 作为一个函数
通常情况下,接下来还需要一个变量来引用这个函数对象,或者将它作为表达式操作数,它才会有意义。如果它作为引用,那么它跟普通变量或其他类型的数据类似;如果它作为一般操作数,那么它应该按照上一讲所说的规则,转换成“值类型”才能进行运算。
>
NOTE:关于引用、操作数,以及值类型等等,请参见《[第01讲(01 | delete 0)](https://time.geekbang.org/column/article/164312)》。
所以,如果不讨论“动态函数创建”内在的特殊性,那么它的创建与其他数据并没有本质的不同:创建结果一样,对执行引擎或运行环境的影响也一样。而这种“没有差异”反而体现了“函数式语言”的一项基本特性:函数是数据。也就是说,函数可以作为一般数据来处理,例如对象,又例如值。
函数与其他数据不同之处,仅在于它是可以调用的。那么“动态创建的函数”与一般函数相比较,在调用/执行方面有什么特殊性吗?
答案是,仍然没有!在ECMAScript的内部方法`Call()`或者函数对象的内部槽`[[Call]] [[Construct]]`中,根本没有任何代码来区别这两种方式创建出来的函数。它们之间毫无差异。
>
NOTE:事实上,不惟如此,我尝试过很多的方式来识别不同类型的函数(例如构造器、类、方法等)。除了极少的特例之外,在用户代码层面是没有办法识别函数的类型的。就现在的进展而言,`isBindable()`、`isCallable()`、`isConstructor()`和`isProxy()`这四个函数是可以实现的,其他的类似`isClassConstructor()`、`isMethod()`和`isArrowFunction()`都没有有效的识别方式。
>
NOTE:如上的这些识别函数,需要在不利用toString()方法,以及不调用函数的情况下来完成。因为执行函数会带来未知的结果,而toString方法的实现在许多引擎中并不标准,不可依赖。
不过,如果我们将时钟往回拔一点,考察一下这个函数被创建出来之前所发生的事情,那么,我们还是能找到“唯一一点不同”。而这,也将是我在“动态语言”这个系列中为你揭示的最后一个秘密。
## 唯一一点不同
在“函数初始化(FunctionInitialize)”这个阶段中,ECMAScript破天荒地约定了几行代码,这段规范文字如下:
>
Let realmF be the value of F’s [[Realm]] internal slot.
Let **scope** be realmF.[[GlobalEnv]].
Perform **FunctionInitialize**(F, Normal, parameters, body, **scope**).
](https://jinshuju.net/f/TmdBMP)
多谢你的收听,最后邀请你填写这个专栏的[调查问卷](https://jinshuju.net/f/TmdBMP),我也想听听你的意见和建议,我将继续答疑解惑、查漏补缺,与你回顾这一路行来的苦乐。
再见。
>
NOTE:编辑同学说还有一个“结束语”,我真不知道怎么写。不过,如果你觉得意犹未尽的话,到时候请打开听听吧(或许还有好货叱)。
by aimingoo.