関数
まとめ
関数は処理をまとめるために使用する。
関数式を利用した際に、関数内部から関数外部への参照は基本しない。
それぞれ使いやすいタイミングがあるので、それを見極めてください。
関数
最初にまとめが来ていますが、何となく頭の片隅に置きながら読み進めてください。
関数、と聞いて一番最初に浮かぶのは、中学や高校で習った一次関数や二次関数でしょうか。
y = x + 1
y = 2x2
y = f(x)
などなど…
プログラミング上ではどのように表現するのかをご紹介します。
定義1(関数文)
まずは実際に定義してみましょう。
test1(); // function test 10
// console.log(counter); // Uncaught ReferenceError: Cannot access 'counter' before initialization
let counter = 0;
function test1() { // function 関数の名前(引数リスト){ 処理 }
let counter = 10;
console.log('funtion test ' + counter);
counter ++;
}
test1(); // function test 10
console.log(counter); // 0
JavaScript | MDNで、一番最初に紹介されているもの、関数宣言(関数文)と呼ばれるものです。
この書き方で特徴的なのは、関数のホスティング(関数の巻き上げ)という現象です。
通常、変数は宣言後に使用しないとエラーになってしまいます。
(2行目)初期化前にアクセスできないよ!っというエラーですね。
しかし、1行目では test1(); という関数を実行する処理は動作しています。
これが、関数のホスティングというものになります。
つまり、4行目のような書き方をすると、関数を定義する行よりも前に関数を実行する処理を書くことができるということです。
また、関数の特徴としてもう一つ、変数のスコープがあります。
3行目で counter という変数を宣言しているにもかかわらず、5行目でも counter という変数を宣言しています。
変数の章で紹介したように、同じ変数名を二度宣言することはできません。
ですが、関数内では変数のスコープによって、同じ変数名の変数を宣言することができます。
その結果どうなったかというと、1行目と9行目では counter の値は 10 です。
10行目の counter の値は 0 のままです。
これは、1,9行目では5行目の counter を参照しているからであり、また同時に、7行目で counter の値を加算しても次に実行される際には、5行目の counter を再び宣言・初期化しているため、2度目の実行でも 5行目の counter は 10 のままだったということになります。
つまり、関数内(4-8行目)の変数は、関数の外部とは切り離された場所にある。ということです。
なので、10行目で counter の中身を表示していますが、関数内部とは切り離された場所にあるので、何事もなく 0 が表示された。ということです。
定義2(名前付き関数式)
実は JavaScript において、関数を定義する方法はいくつかあります。
下記からは関数式と呼ばれるものです。
式、と呼ばれるものなので、式の中で関数を定義し、変数に代入できます。
// test2(); // Uncaught ReferenceError: Cannot access 'test2' before initialization
let testValue = 'testValue';
// 名前付きの関数式を定義する
// const 変数名 = function 関数名(引数リスト) { 処理 }
// test2 に testFunc(tv) {...} という関数を代入する
const test2 = function testFunc(tv) {
tv += ' test2()';
testValue += ' test2()';
console.log(tv);
console.log(testValue);
console.log('----------------');
};
test2();
// undefined test2()
// testValue test2()
test2(testValue);
// testValue test2() test2()
// testValue test2() test2()
// console.log(tv); // Uncaught ReferenceError: tv is not defined
1行目をコメントアウトしてありますが、コメントアウト(//)を外して実行してみるとエラーになることがわかると思います。
関数式は式が評価されるまでは、関数として扱われないため、初期化前はアクセスできないよ!っと怒られます。
よって、関数式として関数を定義すると関数のホスティングがされないということがわかります。
定義の方法はコメントの通りです。
関数内の処理自体はシンプルで、
引数に渡された tv という変数に 'test2()' という文字列を追加する処理
testValue という変数に 'test2()' という文字列を追加する処理
コンソールに出力
といった流れになっています。
14行目で test2 の関数を実行していますが、結果は undefined test2() (10行目の処理)と testValue test2() (11行目の処理)となっています。
undefined というのは変数の宣言はされているが、値が未定義である際に自動的に代入されるものです。
つまり、引数 tv は変数の宣言をされているが実行時に代入がされなかったため、自動的に undefined が代入されていたということになります。
よって、8行目で undefined を自動的に文字列として扱い、 test2() と結合をして tv に代入しています。
9行目では testValue と 'test2()' を結合しています。
そして11行目で testValue の出力をしています。
2行目で宣言している通り、 'testValue' という文字列が初期値として代入されているので、こちらは問題なさそうです。
17行目では引数(testValue)を渡して関数を実行しています。
その結果は両方とも testValue test2() test2() となっています。
理由としては、引数に渡した testValue は、関数内では tv に代入して扱われているからです。
さて、定義1の説明とは少し異なっている点があることに気が付いたでしょうか。
それは、関数内の変数は、関数の外部とは切り離された場所にある。 はずなのにも関わらず、
関数外の変数(testValue)に関数の内部で処理(testValue += 'test2()')を実行できているという点です。
// test2(); // Uncaught ReferenceError: Cannot access 'test2' before initialization
let testValue = 'testValue';
// 名前付きの関数式を定義する
// const 変数名 = function 関数名(引数リスト) { 処理 }
// test2 に testFunc(tv) {...} という関数を代入する
const test2 = function testFunc(tv) {
tv += ' test2()';
testValue += ' test2()';
console.log(tv);
console.log(testValue);
console.log('----------------');
};
test2();
// undefined test2()
// testValue test2()
test2(testValue);
// testValue test2() test2()
// testValue test2() test2()
// console.log(tv); // Uncaught ReferenceError: tv is not defined
17行目では確かに引数として testValue を渡していますが、関数内部では変数 tv として扱っています。
14行目に関しては引数に testValue を渡していないにも関わらず、関数内部で testValue を評価できています。
理解の手助けとなるのは1行目と20行目です。
1行目では関数式を評価する前に、関数を実行しようとしているためエラーとなり、20行目では tv を出力しようとしています。
20行目の結果もエラーとなり、tv が見つからないよ!っと怒られます。
つまり、関数式においても関数外部から関数内部への参照はできないということになります。
また、定義1の関数宣言と違い、関数のホスティングもされません。
ここで、以前分岐処理の中で紹介しました、JavaScript においては、ソースコードを上から順に評価・解釈し実行することについてです。
定義1を見てもらった通り、関数を実行する際は少し変わっていて、実行しようとしている関数名が他の箇所で関数宣言されているかを調べます。
式(let x = x + 1; や let str = '文字列';) は評価する前(正確には変数の宣言をする前)に参照しようとするとエラー(初期化前にアクセスできないエラー)になりました。
関数式も式なので、同様のことが言えます。
つまり、関数式として関数を定義した場合、式が評価されるまで関数ではないということです。(二度目)
そして、関数式は式であり関数なので、関数内部からは関数外部の変数を参照でき、変数のスコープによって関数の内部の変数には関数の外部からは参照できなかったということです。
定義3(無名関数式)
さて、混乱することが増えてきましたが、次の定義方法を見ていきます。
今回も関数式として関数を定義します。
さきほどの、関数だけど式のような挙動をする点に注意しながら実行してみます。
// 三角形の面積を求める関数を作ってみる
let area = 0;
let width = 10;
let height = 5;
// 無名の関数式を定義する
// const(let) 変数名 = function(引数リスト) { 処理 }
// 変数名(今回はcalcTriangleArea)は const である必要はないが、間違えて上書きした際にバグの原因となるので、
// 基本は let よりも const の方が望ましい
const calcTriangleArea = function(w, h) {
console.log(width);
console.log(height);
width = 100;
const area = w * h / 2;
// return ... とすることで関数から値を返すことができる。今回は計算した面積を呼び出し元に返す。
return area;
};
// 結果を表示するための関数
const drawResult = function(w, h, a) {
console.log(`底辺${w}・高さ${h}の面積は${a}です。`);
};
// 実行
area = calcTriangleArea(width, height);
drawResult(width, height, area); // 底辺100・高さ5の面積は25です
height = 10;
area = calcTriangleArea(width, height);
drawResult(width, height, area); // 底辺100・高さ10の面積は500です
calcTriangleAare(w, h) は引数に底辺と高さを渡すと三角形の面積を返してくれます。
関数の内部で関数外部の値を利用したい場合は、引数に渡してあげましょう。
こうすることで、底辺が10・高さが5以外の三角形の面積(23行目から)を求めることができるからです。
関数の最大の利点は処理を使いまわすことができる点です。
三角形の面積を求める度に計算式を書くよりも、関数の引数に底辺と高さを渡し、計算結果を得た方が読みやすいコードになります。
複雑な処理になればなるほど、関数にした方が簡略化できることが多いです。
ですが、よく見ると表示される底辺の値が変です。
これは12行目をハイライトしている通り、関数の内部で width に 100を代入しているため、プログラム的には正しい挙動となります。
何を伝えたいのかというと、関数式の特徴である、関数の内部から関数の外部の変数を参照する際は注意した方が良いということです。
底辺or高さの値を間違えているのか、または計算方法を間違えているのかを判断しにくくなるためです。
25行目で関数を実行した際、実際に計算したかった三角形の底辺が10で高さが10だった場合、求めたい面積ではなくなってしまいます。
定義4(アロー関数式)
私が紹介する関数の定義では最後になります。
アロー関数式というものになります。
こちらも関数式になるので、関数のホスティングがされず、関数内から関数外の変数にアクセスできます。
アロー関数式はTicTacToeで実際に使用しています。
// ボタンの生成
const button1 = document.createElement('button');
button1.innerHTML = '魔王が滅ぶボタン!';
let str = '魔王は滅びました。'
let counter = 0;
// アロー関数による定義
// const 変数名 = 引数 => { 処理 } 引数が一つの場合は () を省略できる
// const 変数名 = () => { 処理 } 引数がない場合・もしくは二つ以上の場合は () が必要
// const 変数名 = (引数1, 引数1) => { 処理 } 引数が複数ある場合は , で区切る
const button1ClickListener = e => {
alert(str);
e.target.innerHTML = str;
if(counter % 2 === 0) {
str = '魔王が復活しました。';
} else {
str = '魔王は滅びました。';
}
counter ++;
};
// リスナーの登録
button1.addEventListener('click', button1ClickListener);
// ボタンの追加
document.body.append(button1);
条件分岐にも出てきていた、魔王が滅んだり復活したりします。
別に魔王が嫌いというわけではありません。
このアロー関数式は主にリスナーを登録する際によく出てきます。
今回の場合だとボタンをクリックしたときの動作を登録しています。
いろいろあるので、試してみてください。
さて、おまじないのように書いていた、 console.log() や alert() も関数であることに気付いているかと思います。
定義していないのになぜ使えるのか、それは次のオブジェクトの章で紹介します。