原文:http://www.zcfy.cc/article/the-many-faces-of-functions-in-javascript-bocoup-2943.html
如果你曾经接触过JavaScript编程,你一定不会陌生如何定义并且调用一个函数。但是你知道在JavaScript中有多少种定义函数的方法吗?如果想要在Test262中编写和维护这些方法的测试,那可真是一个很大的挑战,尤其当一些新特性和现有函数语法相关,或者扩展了函数的API时。但是,想要断言新提出或被提案的语法、API有效时,测试所有既存变式又是非常必要的。 下面会针对JavaScript中已经存在的函数定义方式进行一个概述。本文不包含Class声明和表达式,因为这些方式创建的对象是“不可调用的”,本文旨在那些可生成“可调用”对象的函数定义方式。也就是说,我们不会研究那些复杂的参数列表(包含默认参数、结构赋值或者尾后逗号),因为那足够另起文章介绍了。
以前的方式
函数声明以及函数表达式
最为出名以及应用最广的同样也是这些旧方式:函数声明和函数表达式。前者设计(1995)和出现在第一版的规范(1997)(pdf)中。后者则是出现在第三版中(1999)(pdf)。仔细研究,你会从它们当中提取出三种不同的方式。
// 函数声明
function BindingIdentifier() {}
// 命名函数表达式
// (BindingIdentifier在函数外部是访问不到的)
(function BindingIdentifier() {});
// 匿名函数表达式
(function() {});
值得注意的是,匿名函数表达式仍然可能有名字,Mike Pennisi在什么是函数名称?中有深度解释。
Function
构造函数
当研究一门语言的”function API”的时候,也就到了这门语言的底层。在这门语言的设计之初,函数声明方式可以被理解为是Function
构造函数API的最直接实现。Function
构造函数提供了一种定义函数的方式:通过指明Function
的参数,其中最后一个参数就是函数的函数体(必须要说明的是,这是一种动态代码方式,可能存在安全问题)。在大多数情况下,这种方式是不合适的,所以用的人很少,但是在第一版的ECMAScript中,这种方式就出现了。
new Function('x', 'y', 'return x ** y;');
新的方式
自从ES2015发布以来,几种新的定义函数方式被引入进来,这些方式的变式更是非常繁多。
另类匿名函数
这是一种新式的匿名函数。如果你曾经接触过ES的模块化,那么你很有可能已经接触过这种定义函数的方式了。尽管这种方式看起来和匿名函数的定义方式很像,但是他确实有自己的名字:default
// 另类匿名函数声明
export default function() {}
顺便一提,这个名字并不是专属的标识,并没有进行绑定。
方法定义
下面这些方式定义的函数表达式、匿名函数或者命名函数,都是某个对象的属性。注意这些并不是新的语法,只是应用上面提及的那些语法,写在了某个对象的初始化器中。这种方式最早引入在ES3中。
let object = {
propertyName: function() {},
};
let object = {
// (BindingIdentifier不能再函数外部调用)
propertyName: function BindingIdentifier() {},
};
下面是存取器属性,引入在ES5。
let object = {
get propertyName() {},
set propertyName(value) {},
};
在ES2015中,JavaScript中提供了一种定义方法的简洁语法,不管是直接命名的方式还是计算属性名的方式,都可以使用,而且,存取器同样适用。
let object = {
propertyName() {},
["computedName"]() {},
get ["computedAccessorName"]() {},
set ["computedAccessorName"](value) {},
};
你也可以把这些定义属性或者方法的新方式应用在创建类时。
// 类声明
class C {
methodName() {}
["computedName"]() {}
get ["computedAccessorName"]() {}
set ["computedAccessorName"](value) {}
}
// 类表达式
let C = class {
methodName() {}
["computedName"]() {}
get ["computedAccessorName"]() {}
set ["computedAccessorName"](value) {}
};
…在定义静态方法时,同样可以使用。
// 类声明
class C {
static methodName() {}
static ["computedName"]() {}
static get ["computedAccessorName"]() {}
static set ["computedAccessorName"](value) {}
}
// 类表达式
let C = class {
static methodName() {}
static ["computedName"]() {}
static get ["computedAccessorName"]() {}
static set ["computedAccessorName"](value) {}
};
箭头函数
箭头函数首次出现在ES2015中,尽管起初饱受争议,但是现在已经被广泛应用了。箭头函数的定义根据是否简写有两种不同的语法:赋值表达式(在箭头后面没有花括号)和函数体(当函数包含零个或者多个表达式时)。语法规定,当函数只有一个参数时,可以不用小括号括起来,但是当没有参数或者多余一个参数时,就必须用小括号括起来了。(这种语法就决定了箭头函数会有多种定义形式)
// 零参数, 赋值表达式
(() => 2 ** 2);
// 一个参数, 可以省略小括号, 赋值表达式
(x => x ** 2);
// 一个参数, 可以省略小括号, 函数体
(x => { return x ** 2; });
// 多个参数, 赋值表达式
((x, y) => x ** y);
上面的最后一种形式中,参数是用参数列表来表示的,因为它们用小括号括了起来。类似于用小括号来标识参数列表的语法,还有其他形式,诸如({ x }) => x
。 如果参数不用小括号括起来,那么只能给参数起一个独一的标识符名称,以在箭头函数中使用。当箭头函数被定义为异步函数或者Generator函数时,这个标识符名称还可以加上await
或者 yield
的前缀,但那也已经是在不用括号情形中,考虑足够深远的了。 箭头函数可以并且也经常出现在初始化器或者对象属性的定义中,但是这种情况大部分使用的是上面介绍的赋值表达式形式,举例如下:
let foo = x => x ** 2;
let object = {
propertyName: x => x ** 2
};
Generators
Generator函数的语法是在其他定义函数的方式上加点东西,但箭头函数和存取器方法除外。你可以使用和之前函数声明,函数表达式,函数定义甚至是构造函数等相似的方式。所有方法列举如下:
// Generator 声明
function *BindingIdentifer() {}
// 另类匿名 Generator 声明
export default function *() {}
// Generator 表达式
// (BindingIdentifier只能在函数内部调用)
(function *BindingIdentifier() {});
// 匿名 Generator 表达式
(function *() {});
// 方法定义
let object = {
*methodName() {},
*["computedName"]() {},
};
// 在类声明中定义方法
class C {
*methodName() {}
*["computedName"]() {}
}
// 在类声明中定义静态方法
class C {
static *methodName() {}
static *["computedName"]() {}
}
// 在类表达式中定义方法
let C = class {
*methodName() {}
*["computedName"]() {}
};
// 在类表达式中定义静态方法
let C = class {
static *methodName() {}
static *["computedName"]() {}
};
ES2017
异步函数
经过几年的发展,异步函数将会发布ES2017——第八版EcmaScript语言规范——规范会在2017年6月在正式发布。但其实,很多开发者早已经开始使用异步函数了,这还要归功于Babel的支持。 异步函数语法提供了一个干净的、统一的方式来描述异步操作。当被调用时,异步函数会返回一个Promise对象。当异步执行结束后,这个Promise对象即会被相应执行。当函数中含有await
表达式时,异步函数就会暂停执行,这时,await
表达式结果就会作为异步函数的返回值。 异步函数的语法并没有太多的不同,只是在我们熟知的那些方式前面加上一个前缀:
// 异步函数声明
async function BindingIdentifier() { /**/ }
// 另类匿名异步函数声明
export default async function() { /**/ }
// 命名异步函数表达式
// (BindingIdentifier只能在函数内部调用)
(async function BindingIdentifier() {});
// 匿名异步函数表达式
(async function() {});
// 异步方法
let object = {
async methodName() {},
async ["computedName"]() {},
};
// 类声明中的异步方法
class C {
async methodName() {}
async ["computedName"]() {}
}
// 类声明中的静态异步方法
class C {
static async methodName() {}
static async ["computedName"]() {}
}
// 类表达式中的异步方法
let C = class {
async methodName() {}
async ["computedName"]() {}
};
// 类表达式中的静态异步方法
let C = class {
static async methodName() {}
static async ["computedName"]() {}
};
异步箭头函数
async
和 await
并不是只局限在常规函数的声明或者表达式中,它们同样适用于箭头函数:
// 单一参数,赋值表达式
(async x => x ** 2);
// 单一参数,函数体
(async x => { return x ** 2; });
// 参数列表,赋值表达式
(async (x, y) => x ** y);
// 参数列表,函数体
(async (x, y) => { return x ** y; });
与ES2017结合
异步Generator函数
和ES2017结合,async
和 await
将会被扩展支持异步函数。这一特性的发展可以追踪推荐的github仓储。正如你猜想到的,异步Generator函数的语法是结合async
、await
以及已经存在的Generator函数声明和表达式而来。当异步Generator函数被调用的时候,会返回一个迭代器,这个迭代器的next()
方法会返回一个Promise对象来处理迭代器的返回对象,而不是直接返回迭代器的结果对象。 异步Generator函数已经开始在很多地方使用,你很有可能已经碰到过。
// 异步Generator声明
async function *BindingIdentifier() { /**/ }
// 另类匿名异步Generator声明
export default async function *() {}
// 异步Generator表达式
// (BindingIdentifier只能在函数内部访问)
(async function *BindingIdentifier() {});
// 匿名异步Generator表达式
(async function *() {});
// 匿名异步Generator方法定义
let object = {
async *propertyName() {},
async *["computedName"]() {},
};
// 类声明中异步Generator原型方法定义
class C {
async *propertyName() {}
async *["computedName"]() {}
}
// 类表达式中异步Generator原型方法定义
let C = class {
async *propertyName() {}
async *["computedName"]() {}
};
// 类声明中异步Generator静态方法定义
class C {
static async *propertyName() {}
static async *["computedName"]() {}
}
// 类表达式中异步Generator静态方法定义
let C = class {
static async *propertyName() {}
static async *["computedName"]() {}
};
一个复杂的挑战
每一种函数定义方式都伴随着两方面的挑战:学习、使用成本;完善、维护JS运行环境和Test262。每当引入一种新的定义方式,Test262必须测试与之相关的所有语法规则。只使用默认参数语法测试简单的函数声明方式,就设想它也适用于其他所有形式的做法是不明智的。测试以及编写每一个语法规则的任务对于单个人来讲是不合理的,这就导致了测试生成工具的设计和实现。测试生成工具能够确保覆盖所有相关的语法规则。 这个项目现在包含了一系列的源文件,这些源文件是由不同的测试案例和模板组成的。比如说,各种函数形式中的参数检查,或者函数形式测试或者不仅限于函数形式,解构赋值绑定和解构赋值都是可用的。 尽管这可能是一个复杂并且长期的任务,但是测试的覆盖范围会随之增加,新bug也总能被捕捉到。
所以为什么知道所有的函数定义形式如此重要?
如果你不为Test262编写测试的话,列出所有的函数定义形式可能并不是那么重要。现在已经有了一系列的包含这里所列形式的模板,新的测试可以很容易地使用现有的这些模板作为测试的起点。 确保EcmaScript规范测试是Test262优先考虑的问题。这直接影响所有的JavaScript运行环境,并且我们测试的越多,覆盖范围就会越广,这有助于新特性更无缝隙地引入,不管你使用的是什么平台。