Broadly speaking, JavaScript has four kinds of functions:
- Regular function: can return anything; always runs to completion after invocation
- Generator function: returns a
Generator
object; can be paused and resumed with the yield
operator - Async function: returns a
Promise
; can be paused and resumed with the await
operator - Async generator function: returns an
AsyncGenerator
object; both the await
and yield
operators can be used
For every kind of function, there are three ways to define it:
- Declaration
-
function
, function*
, async function
, async function*
- Expression
-
function
, function*
, async function
, async function*
- Constructor
-
Function()
, GeneratorFunction()
, AsyncFunction()
, AsyncGeneratorFunction()
In addition, there are special syntaxes for defining arrow functions and methods, which provide more precise semantics for their usage. Classes are conceptually not functions (because they throw an error when called without new
), but they also inherit from Function.prototype
and have typeof MyClass === "function"
.
const multiply = new Function("x", "y", "return x * y");
function multiply(x, y) {
return x * y;
}
const multiply = function (x, y) {
return x * y;
};
const multiply = function funcName(x, y) {
return x * y;
};
const multiply = (x, y) => x * y;
const obj = {
multiply(x, y) {
return x * y;
},
};
All syntaxes do approximately the same thing, but there are some subtle behavior differences.
- The
Function()
constructor, function
expression, and function
declaration syntaxes create full-fledged function objects, which can be constructed with new
. However, arrow functions and methods cannot be constructed. Async functions, generator functions, and async generator functions are not constructible regardless of syntax. - The
function
declaration creates functions that are hoisted. Other syntaxes do not hoist the function and the function value is only visible after the definition. - The arrow function and
Function()
constructor always create anonymous functions, which means they can't easily call themselves recursively. One way to call an arrow function recursively is by assigning it to a variable. - The arrow function syntax does not have access to
arguments
or this
. - The
Function()
constructor cannot access any local variables — it only has access to the global scope. - The
Function()
constructor causes runtime compilation and is often slower than other syntaxes.
For function
expressions, there is a distinction between the function name and the variable the function is assigned to. The function name cannot be changed, while the variable the function is assigned to can be reassigned. The function name can be different from the variable the function is assigned to — they have no relation to each other. The function name can be used only within the function's body. Attempting to use it outside the function's body results in an error (or gets another value, if the same name is declared elsewhere). For example:
const y = function x() {};
console.log(x);
On the other hand, the variable the function is assigned to is limited only by its scope, which is guaranteed to include the scope in which the function is declared.
A function declaration also creates a variable with the same name as the function name. Thus, unlike those defined by function expressions, functions defined by function declarations can be accessed by their name in the scope they were defined in, as well as in their own body.
A function defined by new Function
will dynamically have its source assembled, which is observable when you serialize it. For example, console.log(new Function().toString())
gives:
function anonymous(
) {
}
This is the actual source used to compile the function. However, although the Function()
constructor will create the function with name anonymous
, this name is not added to the scope of the body. The body only ever has access to global variables. For example, the following would result in an error:
new Function("alert(anonymous);")();
A function defined by a function expression or by a function declaration inherits the current scope. That is, the function forms a closure. On the other hand, a function defined by a Function
constructor does not inherit any scope other than the global scope (which all functions inherit).
globalThis.p = 5;
function myFunc() {
const p = 9;
function decl() {
console.log(p);
}
const expr = function () {
console.log(p);
};
const cons = new Function("\tconsole.log(p);");
decl();
expr();
cons();
}
myFunc();
Functions defined by function expressions and function declarations are parsed only once, while a function defined by the Function
constructor parses the string passed to it each and every time the constructor is called. Although a function expression creates a closure every time, the function body is not reparsed, so function expressions are still faster than new Function(...)
. Therefore the Function
constructor should generally be avoided whenever possible.
A function declaration may be unintentionally turned into a function expression when it appears in an expression context.
function foo() {
console.log("FOO!");
}
doSomething(
function foo() {
console.log("FOO!");
},
);
On the other hand, a function expression may also be turned into a function declaration. An expression statement cannot begin with the function
or async function
keywords, which is a common mistake when implementing IIFEs (Immediately Invoked Function Expressions).
function () {
console.log("FOO!");
}();
function foo() {
console.log("FOO!");
}();
Instead, start the expression statement with something else, so that the function
keyword unambiguously starts a function expression. Common options include grouping and using void
.
(function () {
console.log("FOO!");
})();
void function () {
console.log("FOO!");
}();