【JavaScript】 関数の定義と変数代入(関数式)の違い
更新日:2024/02/27
僕がJavaScriptと学び始めたとき、関数の定義について次のように感じました。
function a(){};
とか
const a = function (){};
とか、いくつも関数定義の方法用意しとくな!
わかりにくい!!
サンプルプログラムによって関数定義の方法が異なっていて、読み解くのに苦労したものです。
しかし実は、この二つの書き方は目的が異なることを最近になって知りました。
今回はJavaScriptの関数定義についてまとめてみます。
関数宣言と関数式
関数を定義するには、関数宣言と関数式の二つの方法があります。
この二つの違いを説明する前に、そもそも「JavaSceiptの関数とは何か?」という点についてお伝えします。
JavaScriptの関数とは
JavaSceiptの関数は、functionオブジェクト(関数オブジェクト)というオブジェクトの一種です。
functionオブジェクトは名前の後に( )をつけることで、関数コードを実行できるという特徴があります。
しかし基本的には自分で作成したオブジェクトと同じものです。
変数に代入したり、引数として渡すことができるのです。
では、関数宣言と関数式についてお伝えします。
関数宣言
functionから始まる文が関数宣言です。
関数宣言はfunctionオブジェクトを作成して、そのオブジェクトのnameプロパティに関数名をセットします。
そして自動的に変数が作成され、functionオブジェクトと関連付けられます。
次のコードは、messageという引数を受け付けるkansuという名前のfunctionオブジェクトを作成しています。
関数宣言
function kansu( message ) {
alert( message );
}
// ↑kansuという名前の変数が作成される
kansu( kansu.name );
実行すると、アラートで「 kansu 」と表示されます。
関数宣言は、即時関数のような使い方はできません。
関数宣言をそのまま実行
function f(){
let a = 0;
}();
上のコードは、次のようなエラーが発生します。
SyntaxError: expected expression, got ')'
「関数宣言を( )で囲うと即時関数として実行できるのでは?」という疑問を持つ方もいると思います。
次項でお伝えしていますが、( )で囲うと関数式になります。
関数式とは
次のコードは関数式です。
関数式
const kansu = function( message ){
alert( message );
};
kansu( kansu.name );
式なので、{ }の最後に";"が必要です。
ただし、";"が欠落していてもJavaScriptのシステムが自動補完してくれます。
";"は必要ないのではなく、エラーにならないだけということを覚えておいてください。
実行すると先ほどと同じように、アラートで「 kansu 」と表示されます。
このコードは、変数kansuに、"function(message){・・・}" という文字列を入れているのではありません。
具体的には、名前のないfunctionオブジェクトが作成され、変数kansuに代入されます。
このとき、functionオブジェクトのnameプロパティに変数名がセットされます。
ただし他の変数に代入した場合、nameプロパティは変更されません。
次のコードを、上記のコードの後に記述して実行してみます。
const kansu2 = kansu;
kansu2( kansu2.name );
アラートで「 kansu 」と表示されるので、nameプロパティが変更されていないのがわかります。
変数への代入するだけでなく、次のようなコールバックに指定した関数も、関数式です。
関数式
setTimeout ( function( ){
alert( "3秒経過" );
},3000);
function(){・・・} で作成されたfunctionオブジェクトが、引数としてsetTimeoutに渡されています。
関数宣言かどうかは、文がfunctionで始まるかどうかによって決まります。
文がfunctionで始まるなら、関数宣言です。
functionで始まらないなら、関数式です。
次の文は ( で始まっているので、関数式になります。
関数式
(function kansu(message){
alert(message);
});
kansu("test");//ReferenceError:kansu is not defined
この関数式により、関数オブジェクトが作成されます。
しかし関数定義なら自動的にkansuという名前の変数が作成されますが、関数式なので作成されません。
そのため、kansuという名前の変数が存在しないので、"kansuが未定義です" という意味のエラーが表示されます。
(から始まるコードは、あまり役に立たないように見えますが、即時関数で使用することが多いです。
即時関数
(function (message){
alert(message);
})("test");
作成されたfunctionオブジェクトを即座に( )で実行しています。
上のコード程度では、「alert("test")でいいのでは?」という疑問が出てきますが、テクニックとして知っておいてください。
即時関数については、次のページを確認してみてください。
■【JavaScript】 即時関数の挙動について調べてみた
関数宣言と関数式の違い
関数宣言と関数式はどんな点が異なるのでしょうか。
関数宣言は後ろで記述されていてもOK
関数宣言されている関数は、宣言前でも実行できます。
JavaScriptの仕様では、コード評価時に関数宣言がブロックの一番上に移動します。
この動作を巻き上げ(ホイスティング)といいます。
JavaScript:関数宣言の巻き上げ
function kansuB(a){ kansuA(a); }; // 実行時に巻き上げで移動(しているように見える)
function kansuA(a){ console.log(a);}; // 実行時に巻き上げで移動(しているように見える)
kansuA("a"); // 宣言が後ろでも実行できる
kansuB("b"); // 宣言が後ろでも実行できる
function kansuB(a) { // コード上の位置 ↑実行時上へ移動
kansuA(a);
}
function kansuA(a){ // コード上の位置 ↑実行時上へ移動
console.log(a);
}
関数式は後ろで記述されているとNG
関数式も、関数宣言と同様に巻き上げがおこります。
ですが実際には、エラーが表示されます。
JavaScript:関数式の巻き上げ
kansuA("a");//ReferenceError: can't access lexical declaration `kansuA' before initialization
kansuB("b");//ReferenceError: can't access lexical declaration `kansuB' before initialization
const kansuB = function(a){ // コード上の位置
kansuA(a);
};
const kansuA =function(a){ // コード上の位置
console.log(a);
};
巻き上げで、次のような変数の移動がおこっています。
しかし移動されただけで、変数に値がなにも入っていません。
何もセットされていない変数を参照したために、エラーが表示されたようです。
JavaScript:関数式の巻き上げ
const kansuB; // 実行時に巻き上げで変数宣言のみ移動 値未設定
const kansuA; // 実行時に巻き上げで変数宣言のみ移動 値未設定
kansuA("a"); // ReferenceError: can't access lexical declaration `kansuA' before initialization
kansuB("b"); // ReferenceError: can't access lexical declaration `kansuB' before initialization
const kansuB = function(a){ // コード上の位置
kansuA(a);
}
const kansuA = function(a){ // コード上の位置
console.log(a);
};
ちなみにconstではなくてvarを使用した場合、値としてundefindがセットされます。
そのためコンソールに「undefind」と表示されます。
いずれにしても、関数式は定義前に使用するべきではありません。
ただし、関数内で使用することは可能です。
上のコードでは、kansuB内でkansuAを実行しています。
この書き方については問題ありません。
問題はkansuBをどこで実行するかです。
JavaScript:関数式の巻き上げ
const kansuB = function(a){ // コード上の位置
kansuA(a);
}
kansuB( "a" ); // ReferenceError: can't access lexical declaration `kansuA' before initialization
const kansuA = function(a){ // コード上の位置
console.log(a);
};
kansuB( "b" ); // b
上のコードでは、最初のkansuB呼び出しでエラーになります。
理由は、 kansuAがまだ未定義のためです。
2番目のkansuB呼び出し時点では、 kansuAに関数オブジェクトがセットされているため、エラーになりません。
巻き上げについては、次のページを読んでみてください。
■【JavaScript】 変数・関数の巻き上げってなんだ?
関数宣言の注意点:上書きができる
同名の関数宣言は上書きされます。
次のコードは、エラーになりません。
JavaScript
function a(){
console.log("1");
}
function a(){
console.log("2");
}
a(); // 結果: 2
複数の外部スクリプトを読み込んでいると、同名の関数が定義されている可能性が大きくなります。
デバッグが難しくなるので、即時関数などを使って、グローバル汚染しないように注意する必要があります。
例えば次のようにスクリプトを即時関数内に封じ込めることで、他のスクリプトの影響を減らすことができます。
JavaScript
スクリプト1
(()=>{
function a(){
console.log("1");
}
a();
})();
スクリプト2
(()=>{
function a(){
console.log("2");
}
a();
})();
関数式の注意点:作成時の変数が維持される
関数オブジェクトは、作成時に同スコープの変数への参照を保持します。
JavaScript
(function(){
let a = 0; // ・・・ (1)
const kansuA = function(){
console.log(a); // どこでkansuAを実行しても、このaは(1)
};
function kansuB(callBack){
let a = 100;
callBack(); // kansuAが実行される
}
kansuA(); // 定義と同じ階層で実行 → 結果 : 0
kansuB(kansuA); // kansuB内でkansuAを実行→結果 : 0→kansuB内のaはkansuAで参照されない!
})();
上の例では、kansuA作成時に外側の「let a = 0;」への参照を関数オブジェクト(kansuA)内に作成します。
kansuBにkansuAを渡していますがkansuAの実行では、「let a = 100;」は参照されません。
kansuAが保持している変数aへの参照が使用されます。
JavaScriptはレキシカル環境というものが採用されており、関数が定義された時の状態を維持し続けるのです。
関数宣言も同じことが言えますが、コールバック関数として無名関数を使用するときなど、特に注意が必要な知識です。
これはクロージャーという概念を元にしたものです。
詳しくは次のページを読んでみてください。
■【JavaScript】 クロージャとは?要点をまとめてみる
更新日:2024/02/27
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。