If you’ve had any contact with JavaScript code, you’re probably very familiar with how to define and call functions, but are you aware of of how many different ways you can define a function? This is a common challenge of writing and maintaining tests in Test262—especially when a new feature comes into contact with any existing function syntax, or extends the function API. It is necessary to assert that new or proposed syntax and APIs are valid, against every existing variant in the language.
The following is an illustrative overview of the existing syntactic forms for functions in JavaScript. This document will not cover Class Declarations and Expressions, as those forms produce an object that is not “callable” and for this article, we’ll only be looking at forms that produce “callable” function objects. Additionally, we will not be covering non-simple parameter lists (parameter lists which include default parameters, destructuring, or a trailing comma), as that’s a subject worthy of its own article.
The Old Ways
Function Declaration and Expression
The most well known and widely used forms are also the oldest: Function Declaration and Function Expression. The former was part of the original design (1995) and appeared in the first edition of the specification (1997) (pdf), while the latter was introduced in third edition (1999) (pdf). Looking closely, you’ll see that you can extract three different forms from them:
// Function Declaration
function BindingIdentifier() {}
// Named Function Expression
// (BindingIdentifier is not accessible outside of this function)
(function BindingIdentifier() {});
// Anonymous Function Expression
(function() {});
Keep in mind that Anonymous Function Expressions might still have a “name”; Mike Pennisi explains in depth in his article “What’s in a Function Name?“.
Function
Constructor
When discussing the language’s “function API”, this is where it starts. When considering the original language design, the syntactic Function Declaration form could be interpreted as the “literal” form to the Function
constructor’s API. The Function
constructor provides a means for defining functions by specifying the parameters and body via N string arguments, where the last string argument is always the body (it’s important to call out that this is a form of dynamic code evaluation, which may expose security issues). For most use cases, this form is awkward, and therefore its use is very uncommon—but it’s been in the language since there since the first edition of ECMAScript!
new Function('x', 'y', 'return x ** y;');
The New Ways
Since the publication of ES2015, several new syntactic forms have been introduced. The variants of these forms are vast!
The not-so-anonymous Function Declaration
Here’s a new form of anonymous Function Declaration, which is recognizable if you have experience working with ES Modules. While it might appear to be very similar to an anonymous Function Expression, it actually does have a bound name which is "*default*"
.
// The not-so-anonymous Function Declaration
export default function() {}
Incidentally, this “name” is not a valid identifier itself and there is no binding created.
Method Definitions
Readers will immediately recognize that the following forms define Function Expressions, anonymous and named, as the value of a property. Note that these are not distinct syntactic forms! They are examples of the previously-discussed function expression, written within an object initializer. This was originally introduced in ES3.
let object = {
propertyName: function() {},
};
let object = {
// (BindingIdentifier is not accessible outside of this function)
propertyName: function BindingIdentifier() {},
};
Introduced in ES5, accessor property definitions:
let object = {
get propertyName() {},
set propertyName(value) {},
};
Starting with ES2015, JavaScript provides a shorthand syntax to define methods, in both a literal property name and computed property name form, as well as accessors:
let object = {
propertyName() {},
["computedName"]() {},
get ["computedAccessorName"]() {},
set ["computedAccessorName"](value) {},
};
You may also use these new forms as definitions for prototype methods in class declarations and expressions:
// Class Declaration
class C {
methodName() {}
["computedName"]() {}
get ["computedAccessorName"]() {}
set ["computedAccessorName"](value) {}
}
// Class Expression
let C = class {
methodName() {}
["computedName"]() {}
get ["computedAccessorName"]() {}
set ["computedAccessorName"](value) {}
};
…And definitions for static methods:
// Class Declaration
class C {
static methodName() {}
static ["computedName"]() {}
static get ["computedAccessorName"]() {}
static set ["computedAccessorName"](value) {}
}
// Class Expression
let C = class {
static methodName() {}
static ["computedName"]() {}
static get ["computedAccessorName"]() {}
static set ["computedAccessorName"](value) {}
};
Arrow Functions
Originally one of the most contentious features of ES2015, arrow functions have become well-known and ubiquitous. Arrow function grammar is defined such that it provides two separate forms under the name ConciseBody: AssignmentExpression (when there is no curly brace {
following the arrow, and FunctionBody when the source contains zero or more statements. The grammar also allows optionally describing a single parameter without surrounding parenthesis, whereas zero or greater than one parameter will require parenthesis. (This grammar allows writing arrow functions in a multitude of forms).
// Zero parameters, with assignment expression
(() => 2 ** 2);
// Single parameter, omitting parentheses, with assignment expression
(x => x ** 2);
// Single parameter, omitting parentheses, with function body
(x => { return x ** 2; });
// A covered parameters list, with assignment expression
((x, y) => x ** y);
In the last form shown above, the parameters are described as a covered parameters list, because they are wrapped inside parentheses. This provides a syntax to flag a parameters list or any special destructuring patterns as in ({ x }) => x
.
The uncovered form – the one without the parentheses – is only possible with a single identifier name as the parameter in the arrow function. This single identifier name can still be prefixed by await
and yield
when the arrow function is defined inside async functions or generators, but that’s the farthest we get without covering the parameters list in an arrow function.
Arrow Functions can, and frequently do, appear in as the assignment value of an Initializer or Property Definition, but that case is covered by the Arrow Function Expression forms illustrated above and as in the following example:
let foo = x => x ** 2;
let object = {
propertyName: x => x ** 2
};
Generators
Generators have a special syntax that adds to every other form, except for arrow functions and setter/getter method definitions. You can have the similar forms of function declarations, expressions, definitions and even the constructor. Let’s try to list them all here:
// Generator Declaration
function *BindingIdentifer() {}
// Another not-so-anonymous Generator Declaration!
export default function *() {}
// Generator Expression
// (BindingIdentifier is not accessible outside of this function)
(function *BindingIdentifier() {});
// Anonymous Generator Expression
(function *() {});
// Method definitions
let object = {
*methodName() {},
*["computedName"]() {},
};
// Method definitions in Class Declarations
class C {
*methodName() {}
*["computedName"]() {}
}
// Static Method definitions in Class Declarations
class C {
static *methodName() {}
static *["computedName"]() {}
}
// Method definitions in Class Expressions
let C = class {
*methodName() {}
*["computedName"]() {}
};
// Method definitions in Class Expressions
let C = class {
static *methodName() {}
static *["computedName"]() {}
};
ES2017
Async Functions
After being in development for several years, Async Functions will be introduced when ES2017 – the 8th edition of the EcmaScript Language Specification – is published in June of 2017. Despite this fact, many developers have already been using this feature thanks to early implementation support in Babel!
Async Function syntax provides a clean and uniform way of describing an asynchronous operation. When called, an Async Function object will return a Promise object that will be resolved when the Async Function returns. Async Functions may also pause execution of the function when an await
expression is contained within, which can then be used as the return value of the Async Function.
The syntax is not much different, prefixing functions as we know from the other forms:
// Async Function Declaration
async function BindingIdentifier() { /**/ }
// Another not-so-anonymous Async Function declaration
export default async function() { /**/ }
// Named Async Function Expression
// (BindingIdentifier is not accessible outside of this function)
(async function BindingIdentifier() {});
// Anonymous Async Function Expression
(async function() {});
// Async Methods
let object = {
async methodName() {},
async ["computedName"]() {},
};
// Async Method in a Class Statement
class C {
async methodName() {}
async ["computedName"]() {}
}
// Static Async Method in a Class Statement
class C {
static async methodName() {}
static async ["computedName"]() {}
}
// Async Method in a Class Expression
let C = class {
async methodName() {}
async ["computedName"]() {}
};
// Static Async Method in a Class Expression
let C = class {
static async methodName() {}
static async ["computedName"]() {}
};
Async Arrow Functions
async
and await
aren’t limited to common Declaration and Expression forms, they can also be used with Arrow Functions:
// Single identified parameter followed by an assignment expression
(async x => x ** 2);
// Single identified parameter followed by a function body
(async x => { return x ** 2; });
// A covered parameters list followed by an assignment expression
(async (x, y) => x ** y);
// A covered parameters list followed by a function body
(async (x, y) => { return x ** y; });
Post ES2017
Async Generators
Post ES2017, async
and await
keywords will be extended to support new Async Generator forms. Progress on this feature can be tracked via the proposal’s github repository. As you have likely guessed, this is a combination of async
, await
, and the existing Generator Declaration and Generation Expression forms. When called, an Async Generator returns an iterator, whose next()
method returns Promise to be resolved with an iterator result object, instead of returning the iterator result object directly.
Async Generators can be found in many places you might already find a Generator Function.
// Async Generator Declaration
async function *BindingIdentifier() { /**/ }
// The not-so-anonymous Async Generator Declaration
export default async function *() {}
// Async Generator Expression
// (BindingIdentifier is not accessible outside of this function)
(async function *BindingIdentifier() {});
// Anonymous Function Expression
(async function *() {});
// Method Definitions
let object = {
async *propertyName() {},
async *["computedName"]() {},
};
// Prototype Method Definitions in Class Declarations
class C {
async *propertyName() {}
async *["computedName"]() {}
}
// Prototype Method Definitions in Class Expressions
let C = class {
async *propertyName() {}
async *["computedName"]() {}
};
// Static Method Definitions in Class Declarations
class C {
static async *propertyName() {}
static async *["computedName"]() {}
}
// Static Method Definitions in Class Expressions
let C = class {
static async *propertyName() {}
static async *["computedName"]() {}
};
A Complex Challenge
Every function form represents a challenge not only for learning and consuming, but also for implementation and maintenance in JS runtimes and Test262. When a new syntactic form is introduced, Test262 must test that form in conjunction with all pertinent grammar rules. For example, it’s unwise to limit testing of default parameter syntax to simple function declaration form and assume it will work in all other forms. Every grammar rule must be tested and writing those tests is an unreasonable task to assign to a human. This lead to the design and implementation of a test generation tool. Test generation provides a way to ensure that coverage is exhaustive.
The project now contains a series of source files that comprise of different test cases and templates, for example how arguments
is checked on each function form, or the function forms tests, or even more beyond the function forms, where both destructuring binding and destructuring assignment are applicable.
Although it might result in dense and long pull requests, the coverage is always improved and new bugs might always be caught.
So why is it important to know all the function forms?
Counting and listing all the function forms is probably not that important unless you need to write tests on Test262. There is already a condensed list of templates for many of these forms listed here. New tests can easily use the existing templates as a starting point.
Ensuring that the EcmaScript specification is well tested is the main priority of Test262. This has direct impact on all JavaScript runtimes and the more forms we identify, the more comprehensive the coverage will be, which helps new features integrate more seamlessly, regardless of the platform you’re using.