仮引数 (parameter) と実引数 (argument)があり、意味が若干異なりますが、 MDN web docs では多くの場合同じ意味で使用されています。簡単に言うと、
js
function formatNumber(num) {
return num.toFixed(2);
}
formatNumber(2);
この例では、 num 変数は関数の「仮引数」 (parameter) と呼ばれ、関数定義の括弧で囲まれたリスト内で宣言されます。関数は num 仮引数が数値であることを期待しています。ただし、 JavaScript では実行時検証コードを書かない限り、これを強制することはできません。 formatNumber(2) の呼び出しにおいて、数値 2 は関数の「実引数」 (argument) です。これは関数呼び出し時に実際に渡される値です。 実引数の値は、関数本体内で対応する引数名または arguments オブジェクトを通じてアクセスできます。
引数は常に値渡しであり、決して参照渡しではありません。これは、関数が引数に再代入を行っても、関数外では値が変更されないということです。より正確には、オブジェクト引数は共有渡しです。つまり、オブジェクトのプロパティが変更される場合、その変更は関数外にも影響するということです。例えば、
js
function updateBrand(obj) {
// オブジェクトの変更は関数外からも見える
obj.brand = "Toyota";
// 仮引数に再代入してみても、関数外での変数の値には影響しない
obj = null;
}
const car = {
brand: "Honda",
model: "Accord",
year: 1998,
};
console.log(car.brand); // Honda
// オブジェクト参照を関数へ渡す
updateBrand(car);
// updateBrand が car を変更した
console.log(car.brand); // Toyota
this キーワードは、関数がアクセスされたオブジェクトを参照します。現在実行中の関数を参照するのではなく、関数本体内であっても関数値は名前で参照する必要があります。
大まかに言えば、 JavaScript には 4 種類の関数があります。
- 通常関数: 何でも返すことができます。呼び出し後、常に完了するまで実行します。
- ジェネレーター関数:
Generator オブジェクトを返します。 yield 演算子で一時停止や再開ができます。
- 非同期関数:
Promise を返します。 await 演算子で一時停止や再開ができます。
- 非同期ジェネレーター関数:
AsyncGenerator オブジェクトを返します。 await と yield の両方の演算子を使用できます。
どの種類の関数でも、それを定義する方法は複数あります。
- 宣言
-
function, function*, async function, async function*
- 式
-
function, function*, async function, async function*
- コンストラクター
-
Function(), GeneratorFunction(), AsyncFunction(), AsyncGeneratorFunction()
さらに、アロー関数 と メソッド を定義するための特別な構文があり、これらはその用法に明確な意味づけを提供します。クラスは概念的には関数ではありません(new なしで呼び出されるとエラーが発生するため)。しかし、それらは Function.prototype を継承し、 typeof MyClass === "function" となります。
js
// コンストラクター
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;
},
};
すべての構文はおおむね同じことをしますが、微妙な動作の違いがいくつかあります。
Function() コンストラクター、function 式、および function 宣言構文は、正規の関数オブジェクトを生成します。これらは new を使用して構築可能です。ただし、アロー関数とメソッドは構築できません。非同期関数、ジェネレーター関数、および非同期ジェネレーター関数は、構文に関わらず構築できません。
function 宣言は、巻き上げられる関数を作成します。それ以外にも構文では関数は巻き上げられず、関数の値は定義後にのみ見えるようになります。
- アロー関数と
Function() コンストラクターは常に無名関数を生成するため、それらは簡単に再帰的に自身を呼び出すことができません。アロー関数を再帰的に呼び出す一つの方法は、それを変数に代入することです。
- アロー関数の構文では、
arguments や this は持ちません。
Function() コンストラクターはローカル変数にアクセスできません。グローバルスコープのみにアクセス可能です。
Function() コンストラクターでは実行時コンパイルが発生するため、それ以外の構文よりも多くの場合、処理が遅くなります。
function 式では、関数名と関数が代入される変数には違いがあります。関数名は変更できませんが、関数が代入される変数は再代入できます。関数名は、関数が代入される変数とは異なる名前を付けることが可能です。両者は、互いに関連のありません。関数名は関数本体内でのみ使用できます。関数本体外で使用しようとするとエラーが発生します(または、同じ名前が別の場所で宣言されている場合は別の値が取得されます)。 例えば、
js
const y = function x() {};
console.log(x); // ReferenceError: x is not defined
一方、関数が代入される変数は、そのスコープによってのみ制限されます。このスコープには、関数が宣言されたスコープを含むことが保証されています。
関数宣言は、関数名と同じ名前の変数も作成します。したがって、関数式で定義されたものとは異なり、関数宣言で定義された関数は、自身の本体内だけでなく、定義されたスコープ内でもその名前でアクセスできます。
new Function で定義された関数は、そのソースが動的にアセンブルされます。これはシリアライズ時に確認できます。例えば、console.log(new Function().toString()) は次のような結果になります。
js
function anonymous(
) {
}
これは関数をコンパイルするために実際に使用されるソースです。ただし、 Function() コンストラクターは anonymous という名前の関数を生成しますが、この名前は本体スコープには追加されません。本体は常にグローバル変数にしかアクセスできません。例えば、次のものはエラーになります。
js
new Function("alert(anonymous);")();
関数式または関数宣言によって定義された関数は、現在のスコープを継承します。つまり、その関数はクロージャを形成します。一方、 Function コンストラクターによって定義された関数は、グローバルスコープ(すべての関数が継承する)以外のスコープを継承しません。
js
// p はグローバル変数
globalThis.p = 5;
function myFunc() {
// p はローカル変数
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();
// Logs:
// 9 (関数定義による 'decl' の場合(現在のスコープ))
// 9 (関数式による 'expr' の場合(現在のスコープ))
// 5 (コンストラクターによる 'cons' の場合(グローバルスコープ))
関数式や関数宣言で定義された関数は一度だけ構文解析されますが、 Function コンストラクターで定義された関数は、コンストラクターが呼び出されるたびに渡された文字列を毎回構文解析します。関数式はそれぞれクロージャを生成しますが、関数本体は再解析されないため、関数式は new Function(...) よりも高速です。したがって、可能な限り Function コンストラクターの使用は避けるべきです。
関数宣言は、式コンテキスト内で現れた場合、意図せず関数式に変換されることがあります。
js
// 関数宣言
function foo() {
console.log("FOO!");
}
doSomething(
// 引数として渡された関数式
function foo() {
console.log("FOO!");
},
);
一方、関数式は関数宣言に変換されることもあります。式文は function や async function キーワードで始めることはできません。これは IIFE(即時実行関数式)を実装する際によく見られる間違いです。
js
function () { // SyntaxError: Function statements require a function name
console.log("FOO!");
}();
function foo() {
console.log("FOO!");
}(); // SyntaxError: Unexpected token ')'
代わりに、式文を別の方法で始まるようにし、 function キーワードで確実に関数式が始まるようにします。一般的な選択肢には、グループ化や void の使用があります。
js
(function () {
console.log("FOO!");
})();
void function () {
console.log("FOO!");
}();
一言で言えば、使わないでください。
厳格モードのコード以外では、ブロック内の関数宣言は奇妙な動作をします。次の例を見てください。
js
if (shouldDefineZero) {
function zero() {
// 危険: 互換性リスク
console.log("これはゼロです。");
}
}
厳格モードにおけるこの意味づけは明確に指定されています。 zero は常に if ブロックのスコープ内でのみ存在します。 shouldDefineZero が false の場合、ブロックは決して実行されないため、 zero は定義すべきでは決してありません。しかし、過去にはこれは未指定のままにされていたため、厳格モード以外ではブラウザーによって実装が異なっていました。より詳細な情報については、function宣言のリファレンスを参照してください。
条件付きで関数を定義するより安全な方法は、変数に関数式を代入することです。
js
// var を使用すると、グローバル変数として利用可能になり、
// 最上位の関数宣言に近い動作になる
var zero;
if (shouldDefineZero) {
zero = function () {
console.log("これはゼロです。");
};
}