今回は、JavaScriptの関数についてもう一度基礎から学び直してみました。
関数宣言と関数式の違いやスコープ、引数の扱い方、即時関数や再起関数などについて、がっつりまとめていきます。
基本的に、こちら( 関数 - Javascript|MDN ) の情報をもとに、基礎知識を整理していこうと思います。
ちなみに、以前別の記事ではJavaScriptの変数について学び直しております。
JavaScriptにおける「関数」とは
関数は、JavaScript の手続き ― つまり、タスクや値計算を実行する文の集まりです。関数を使うには、呼び出したいスコープ内のどこかでそれを定義する必要があります。
と記述されています。
つまり、計算などの処理を、ひとつにまとめたもののことで、どこかでそのまとまりを定義しておくことで、使いたい場所でそれらを呼び出すことができます。
また、JavaScript の関数に関する完全なリファレンスについての章 を見てみると、
JavaScript において、関数は第一級オブジェクトです。すなわち、関数はオブジェクトであり、他のあらゆるオブジェクトと同じように操作したり渡したりする事ができます。具体的には、関数は Function オブジェクトです。
と記述されています。
JavaScriptにおける関数とは、オブジェクトの一種だそうですね。知らなかったです。
JavaScriptでは、プリミティブ以外は実は全てオブジェクトとなっています。
プリミティブとは、オブジェクトでなくメソッドを持たないデータのことで、JSではstring型やint型といった6つのプリミティブデータ型が用意されています。こちらについても後日まとめてみようと思っています。
関数を定義する方法
前章で、関数はどこかに定義しておくものだと書きました。この章では、 関数の定義をどのようにして行うかを、まとめてみようと思います。
まずは、基本的な定義方法を3つまとめてみます。
1. 関数宣言
まずは一つ目の定義方法で、一般に 関数宣言と呼ばれるものです。
関数宣言は「 function」というキーワードを使用した構文のことで、 関数の名前・関数への 引数のリスト・関数の 処理内容をまとめた文の3つで構成されます。
function name([param[, param[, ... param]]]) {
statements
}
上記のような形式ですね。
name | 関数の名前。 |
---|---|
param | 関数に渡される引数の名前。関数は最大 255 個の引数を持つことが可能。 関数の引数は、文字列や数値だけでなく、オブジェクト全体を関数に渡すこともできます。 また、定義時に記述している引数の数を超えてもエラーにはならない。 |
statements | 関数の本体となる処理内容を構成する文。 |
では、例をひとつ。
例:数値を渡すとその数値の2乗を計算してくれる関数を定義する
function square(num) {
return num * num;
}
return に続けて、関数を呼び出した時に返す値を記述します。
関数の呼び出しは後述しますが、returnを使用すると計算結果を変数に代入したりすることができます。
2. 関数式
定義方法の2つ目です。functionキーワードでまとめた処理を 変数に代入する方法です。
先ほどと同じく、数値を2乗する関数を関数式で定義すると、以下のようになります。
var square = function(num) { return num * num };
これをみると、関数がオブジェクトなんだということがしっくりきますね。
上記の関数はfunctionキーワードのあとに名前が定義されていないので、 無名な関数となります。「square」というのはあくまで変数名であって、関数名ではありません。
もちろん、次のように名前をつけることもできます。
var funcSquare = function square(num) { return num * num };
こうすることで、squareという名前の関数を、funcSquareという変数に代入することになります。
名前付き関数のメリット
エラーに遭遇したとき、スタックトレースがその関数の名前を含めるため、エラーの発生源をより容易に特定できるようになります。
3. Function コンストラクタ
定義方法の3つ目は、 Functionコンストラクタによる関数の生成方法です。オブジェクトの生成と同じように、 new演算子 を使用します。
が、前提としてまず注意しなければならないのは、 この方法は非推奨とされていることです。
非推奨の理由として、このように説明されています。
文字列として関数本体が必要で、JS エンジンによる最適化を妨げたり、他の問題を引き起こしたりする場合があるためです。
new Function (arg1, arg2, ... argN, functionBody)
arg1, arg2, ... argN | 関数で仮引数名として使われる、0 個以上の名前。それぞれが、妥当な JavaScript 識別子に相当する文字列、もしくはそういった文字列のカンマで分割されたリストでなくてはなりません。 |
---|---|
functionBody | 関数定義を構成する JavaScript 文を含む文字列。 |
statements | 関数の本体となる処理内容を構成する文。 |
例:数値を2乗する関数
var square = new Function("num", "return num * num;");
関数の返り値について
定義した関数を呼び出す方法を次章でまとめていきますが、その前に、関数を呼び出した時に どのような値が返ってくるのかを確認しておきます。
返り値について、MDNでは次のように書かれています。
初期値以外の値を返すためには、返す値を指定する return 文が関数内になくてはなりません。return 文を持たない関数は初期値を返します。new キーワードとともに constructor が呼び出された場合、その this パラメータが初期値となります。それ以外の全ての関数がデフォルトで返す値は undefined です。
つまり、何を返すかは return文によって自分で定義しておく必要があり、何も定義しない場合、デフォルトで「 undefind」が返ってくる仕組みになっています。
ブロックレベル関数について
例えばif文を使用して、条件がtrueの時のみ関数を定義したい場合があるとします。
この時、以下のような記述は少し注意が必要です。
if (flag) {
//flagがtrueなら関数宣言を行いたい
function foo() {
console.log("Hello, World!");
}
}
何が悪いのか?と思ってしまったのですが、この方法では ES2015をサポートしていないブラウザでは、flagがfalseの時でも変数宣言が行われてしまうことがあるそうです。
今ではほとんどのブラウザが対応しているのでそこまで気に病む必要はないですが、IE8とかまで視野に入れている場合は以下のように記述するのが安全とのこと。
var fool; if (flag) { //関数式で定義する fool = function() { console.log("Hello, World!"); } }
引数の様々な扱い方
前章で関数定義の方法をまとめましたが、定義時の引数の扱い方で少し特殊なものがいくつかあるので、まとめていきます。
デフォルト仮引数
JavaScript では関数の仮引数はデフォルトでは undefined となりますが、デフォルト仮引数を設定することで、渡されなかった引数にも値をセットすることができます。
例:デフォルト仮引数を定義しない場合
function foo(hoge) {
console.log(hoge);
}
foo(); //-> undefined
foo("ほげほげ"); //-> "ほげほげ"
例:デフォルト仮引数を定義した場合
function foo(hoge = "ほげ") {
console.log(hoge);
}
foo(); //-> "ほげ"
foo(ほげ); //-> "ほげほげ"
arguments オブジェクト
関数に渡される引数は、配列のようなオブジェクトで管理されており、次のようにして渡された引数をインデックス番号で指定することが可能です。
例 : 引数の2個目だけreturnする
function foo(args) {
return arguments[1]; //引数が2つ未満のときは"undefined"が返されます
}
argumentsオブジェクトを使用することで、引数がいくつ渡されるか分からない場合に、いくつ渡しても処理できるようにすることができます。
例1:渡した引数をすべて「,」で繋ぐ
function foo() {
var text = "";
// 引数について繰り返し
for (let i = 0; i < arguments.length; i++) {
text += arguments[i] + ",";
}
return text;
}
foo("a","b","c"); // -> "a,b,c,"
JavaScriptでの関数は、 定義時の引数の数に関係なく、最大255個までの引数を渡すことができることに注意。
例2:何で文字つなぐかは第1引数で指定する
function foo(sep) {
var text = "";
// 引数について繰り返し
for (let i = 0; i < arguments.length; i++) {
text += arguments[i] + sep; //第1引数の sep でつなぐ
}
return text;
}
foo("/","a","b","c"); // -> "a/b/c/"
変数 arguments は "配列のような変数" であり、配列ではないことに注意。インデックスと length プロパティを持つだけで、配列操作のメソッドは持っていない。
残余仮引数
不特定多数の引数をとり、 引数の3番目以降をまとめて配列にしたい、というような場合に有効な、 残余仮引数という構文があります。
例を見るのが早いと思います。
例:3番目以降の引数を配列に変換する
function foo(a,b,...args) {
return(args);
}
var array = foo(1,2,3,4,5); //-> array = [3,4,5] (第3引数以降の配列)
引数の後半を( ,...args)というようにすることで、その関数内では「args」という変数を配列として扱うことが可能になります。
argumentsオブジェクトとは違い、上記のargsは配列となるので、配列メソッドも使用できます。
関数の呼び出し
関数は呼び出してあげないと処理が実行されません。
この章では、関数の呼び出しについてまとめていきます。
実際に呼び出してみます。方法は簡単。名前と、引数を記述してあげるだけです。
例:3を2乗させた値を取得する
//関数宣言
function square(num) {
return num * num;
}
//関数を呼び出して計算結果を変数に代入
var result = square(2);
//result = 4 となる
square(2) の部分で、2という引数を関数squareに渡しながら呼び出しています。
returnで計算結果が返ってくるので、その計算結果(2 * 2 )である4を変数resultに代入しています。
この例では計算をする関数だったので、関数を呼び出してそのまま計算結果を変数に代入しましたが、メッセージを出すだけなど、returnで値を返さなくていい場合は、以下のようになります。
例:アラートで好きなメッセージを出す
//関数宣言
function alertMessage(text) {
alert(text);
}
//"ほげほげ"とアラートを出す
alertMessage("ほげほげ");
関数名に"ほげほげ"というメッセージを渡して呼び出しています。
ちなみに、この関数を変数に代入すると、アラートが実行されつつ、変数には"undefined"が代入されます。
関数のスコープ
変数や関数を呼び出すことができる範囲(つまり、アクセス可能な範囲)というのがあり、それを スコープといいます。
変数のスコープについては第一回でまとめていますのでそちらをご覧ください。
関数のスコープは 自身が宣言された関数内です。もしくは、 トップレベルで宣言された場合、プログラム全体になります。
例:スコープ範囲について
//トップレベルでの宣言
function a(){
console.log("a");
}
a(); // "a";
b(); // エラー
c(); // エラー
//jQueryのready関数
$(function(){
function b(){
console.log("b");
}
a(); //"a"
b(); //"b"
c(); //エラー
});
//jQueryのload関数
$(window).on('load', function(){
function c(){
console.log("c");
}
a(); //"a"
b(); //エラー
c(); //"c"
});
関数宣言と関数式での巻き上げの違い
関数宣言での定義された関数を呼び出す場合、呼び出しの記述より後に関数宣言を記述していても、無事に関数を呼び出すことができます(もちろん、有効スコープ内での話です)。
しかし、関数式で定義する場合は、必ず定義した後に呼び出す必要があります。
例:定義方法による巻き上げの違い
fooX(); //"fooX"
fooY(); //エラー
function fooX(){
console.log("fooX");
}
var fooY(){
console.log("fooY");
}
つまり、トップレベルで関数宣言しておけば、その関数はどこでも呼び出すことが可能です。
例:定義方法による巻き上げの違い
foo(); //"foo"
$(function(){
foo(); //"foo"
});
function foo(){
console.log("foo");
}
関数が既に存在するか確認してから呼び出す
関数を定義すると、 windowオブジェクトがその関数名のプロパティを持つようになります。
そこで、 typeof 演算子を使用してそのプロパティの型を調べることで、関数が定義済みであるかを確認することができます。
例:関数foo()を宣言し、typeofで型を調べてみる
function foo(){};
console.log(typeof window.foo); //-> "function"
関数で既に定義されているなら "function" という文字列が返ってきます。未定義なら" undefined"、その他の型で使用されていればその型名が返ってきます。
これを利用して、
if ('function' === typeof window.foo) {
// foo() を使う
} else {
// foo()を使わず他のことをする
}
といった条件分岐が可能になります。
また、関数式で関数名を付けて定義した場合、アクセス可能なのは変数名だけという点に注意が必要です。
var funcFoo = function foo(){};
//関数名はundefinedとなる
console.log(typeof window.foo); //-> "undefined"
//変数名でチェック可能
console.log(typeof window.funcFoo); //-> "function"
関数宣言と関数式の相違点
巻き上げ可能かどうかという点に違いがある、というのは前章で書きました。これは基本的なことなので、知っていた方は多いと思います。
私自身も以前からその相違点のことは知っていたのですが、今回、他の点でも違いがあることを学んだので、その相違点についてもまとめていこうと思います。
関数名へのアクセス可能な範囲の違い
前章「 関数が既に存在するか確認してから呼び出す」の項で、 typeofを用いたwindowオブジェクトのプロパティ確認で違いが見られたように、関数名へのアクセスに少し違いがあるようです。
関数式で関数を名前付きで定義した場合、関数名は 関数本体の内部でのみ使用する事ができ、関数自身の外部でアクセスしようとするとエラーになります。
次のような関数式で関数を定義したとします。
var funcX = function x(){return 1;};
この時、変数名で関数を呼び出すと以下のように無事に1が返ってきます。
console.log(funcX()); // -> 1
また、変数名に()を付けずにアクセスすると、関数自身が文字列にシリアライズされたものが返ってきます。
console.log(funcX); // -> function x(){ return 1; };
このように、変数名ではアクセス可能ですが、実は関数名で呼び出すことができません。
以下はどちらも、"Uncaught ReferenceError: x is not defined"とエラーになります
console.log(x()); //エラー
console.log(x); //エラー
このように、関数式では関数名でのアクセスが外部から不可能となります。
一方、関数宣言の場合、
function x(){
return 1;
}
console.log(x()); //-> 1
console.log(x); //function x(){ return 1; }
特に必要のない知識かもしれませんが、こんな違いもあるんだなぁと。
即時関数
関数を一度だけしか使用しない場合、即時関数を使用することで、 関数を定義しながらそのまま即時に実行させることが可能になります。
即時関数は、functionキーワードごと()で囲んで定義します。
例:定義と同時に文字列を出力する即時関数
(function() {
console.log("hoge");
})();
//最後の()は以下の位置でもOK
(function() {
console.log("hoge");
}());
名前をつけることも可能ですが、外部から呼び出すことはできません。
(function foo() {
console.log("hoge");
})();
foo(); //エラー
このように、呼び出してもエラーになりますが、名前をつけておくことでより処理の内容が理解しやすくなるというメリットがあります。
引数の使用方法
即時関数での引数は末尾の()で渡すことができます。
var result = (function (x, y) {
return x + y;
})(2, 4);
//もしくは
var result = (function (x, y) {
return x + y;
}(2, 4));
// result = 6;
即時関数を使用するメリット
最も重要な点は、 スコープ汚染を防ぐことができるという点です。
仮に変数の定義で名前衝突などが起こったとしても、その即時関数の内部だけで変数が更新され、外部では変数に影響はありません。
その他の点では、関連する処理をまとめることで見やすくなり、あとから修正などがしやすいという点が挙げられます。
再帰関数
関数は自身を参照し、呼び出すことができ、そのような関数を 再帰関数と呼びます。
例1:1からnまでの数の和を求める
function addition1toN(n){
if(n == 1){
return 1;
}else{
return n + addition1toN(n - 1);
}
}
addition1toN(100); //->5050
この例では自身の関数名を呼び出して再帰しています。
この時の関数名へのアクセスは関数自身の内部でのアクセスなので、関数式で定義している場合でも、関数名で呼び出しが可能です。また、変数名でも呼び出しができます。
例2-1:階乗を計算する関数(関数名で再帰)
var funcFactorial = function factorial(n){
if ((n == 0) || (n == 1))
return 1;
else
return (n * factorial(n - 1)); //関数名でよびだせる
}
例2-2:階乗を計算する関数(変数名で再帰)
var funcFactorial = function factorial(n){
if ((n == 0) || (n == 1))
return 1;
else
return (n * funcFactorial(n - 1)); //変数名でもOK
}
無名関数での再帰
上記では関数に名前があり、その名前で自身を呼び出していました。しかし無名関数では名前がないので、自身を呼び出すことができません。
そんな場合には、 arguments.callee を使用します。
例3 : alertを出してから1からnまでの和を計算する
function additionWithAlert() {
alert('hogehoge!');
return function(n) {
if(n == 1){
return 1;
}else{
return n + arguments.callee(n - 1); //無名関数の呼び出し
}
}
}
additionWithAlert()(100); // "hogehoge!"のアラートの後、->5050
上記の例では、関数additionWithAlertの中で計算を実行する無名関数を定義しています。
この arguments.calleeは、無名関数でなくとも使用できます。
まとめ
今回は関数の基本的な部分をまとめてみました。
なかなか奥が深く、改めて知ることが多かったです。
ここではまとめきれなかったものがたくさんあるので、次回はこの続きをまとめていこうと思います。
内容としてはクロージャなどの少し複雑なものや、新しい関数定義方法のアロー関数などについてまとめようと思います。