详细了解JavaScript函数

原文: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"]() {}
};

异步箭头函数

asyncawait并不是只局限在常规函数的声明或者表达式中,它们同样适用于箭头函数:

// 单一参数,赋值表达式
(async x => x ** 2);

// 单一参数,函数体
(async x => { return x ** 2; });

// 参数列表,赋值表达式
(async (x, y) => x ** y);

// 参数列表,函数体
(async (x, y) => { return x ** y; });

与ES2017结合

异步Generator函数

和ES2017结合,asyncawait将会被扩展支持异步函数。这一特性的发展可以追踪推荐的github仓储。正如你猜想到的,异步Generator函数的语法是结合asyncawait以及已经存在的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运行环境,并且我们测试的越多,覆盖范围就会越广,这有助于新特性更无缝隙地引入,不管你使用的是什么平台。


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流