こんにちは。ECF Tech担当
Michiharu.Tです。
JavaScript中級編 第3回をお送りしたいと思います。今回は関数再入門というテーマで、関数の様々な特徴や留意点を見ていきます。フレームワークなどを活用したフロントエンド開発などでつまづきやすいポイントをまとめていますので、ぜひ、ご活用頂ければと思います。
本連載の章立ての一覧については下記のリンクから確認できます。
3-1 様々な関数定義
JavaScriptには様々な関数定義があります。定義方法によって動作が異なるケースもありますので、定義方法ごとの特徴を理解することが重要となります。
3-1-1 基本の関数定義
まずは基本となる関数の定義方法を確認しましょう。
function 関数名(引数...){ //何らかの処理 return 戻り値; }
プログラム例を示します。
プログラム例
function tashizan(x, y){ return x + y; } console.log(tashizan(8,4));
実行結果
12
引数で受け取った2つの値を加算し、結果を戻り値として返します。
3-1-2 関数オブジェクト
JavaScriptでは関数もオブジェクトの1つとして扱います。オブジェクトなので、関数を変数に代入するといったことも可能です。プログラム例を示します。
プログラム例
let tashizan = function(x, y){ return x + y; } console.log(tashizan(8,4)); let work = tashizan; console.log(work(7,2));
実行結果
12 9
関数を定義し、変数tashizan
に代入しています。代入された関数は、変数名を関数名代わりにして呼び出すことができます。tashizan(8,4)
で2つの値の計算結果を得ることができます。例ではさらにlet work = tashizan;
となっています。これにより、変数workにも同じ関数が代入されます。これによりwork
も関数名代わりに使うことができます。
3-1-3 アロー関数
関数を短いコードで書きたい場合は、アロー関数という書き方もあります。基本的な文法は次のようになります。
(引数...) => { //何らかの処理 return 戻り値; }
3-1-2に示した足し算関数をアロー関数で書くと次のようになります。
プログラム例
let tashizan = (x,y) => { return x + y; } console.log(tashizan(7,3));
実行結果
10
アロー関数はさらに、次のようなケースで記述を省略できることから様々な省略表記があります。
- 引数が1つの場合は、引数部の
()
は省略可能 - 処理部が1つの式で記述できる場合は、処理部の
{}
は省略可能
たとえば次の3つの関数定義は、どれもxを2乗して返す処理を実現します。
プログラム例
let beki1 = (x) => { return x * x; } let beki2 = x => { return x * x; } let beki3 = x => x * x; console.log(beki1(2)); console.log(beki2(3)); console.log(beki3(4));
実行結果
4 9 16
アロー関数の注意点
アロー関数ではthisを使えません。たとえば次のようなプログラムは予期しない実行結果を招きます。
プログラム
let taro = { name: "太郎", age: 20, introduce: () => { console.log('私は'+this.name+'です。'); } } taro.introduce();
実行結果
私はundefinedです。
オブジェクトを作成して変数taro
に代入しています。オブジェクトにはintroduceメソッドが定義されています。introduceメソッドは自己紹介文を表示するメソッドで、アロー関数を使って定義されています。thisキーワードを使ってnameプロパティを参照し、「私は太郎です。」と表示されることを期待していますが、意図した結果となっていません。
3-1-4 クロージャ
クロージャは「関数」と「関数が定義された周囲環境」を併せ持つ概念です。関数閉包などと訳されます。プログラムを見ながらの方が理解しやすいので、まずは例を示します。
プログラム
function counter(){ let count = 0; function countup(){ count = count + 1; console.log(count); } return countup; } const mycounter = counter(); mycounter(); mycounter(); mycounter();
実行結果
1 2 3
mycounterを呼び出すたびに1ずつ値が増加していることがわかります。これを理解するにはmycounterがどんなオブジェクトかを考える必要があります。プログラムを見ると、counter関数の戻り値はcountup
、つまり内部で定義された関数なのでmycounter = counter();
の実行時、mycounterは下図のように関数部分をオブジェクトとして持つ。と考えるかもしれません。
ですがこれだと、図のように関数内のcount
はどの変数を指すかがわかりません。なので実際には、下図のように関数が定義された1つ外側のブロックも含めた形でオブジェクトは存在している。と考える必要があります。
これがクロージャの概念です。「関数」と「その関数が定義された環境」を1つにまとめている。と考えると良いでしょう。したがってプログラム中のmycounter
が示すオブジェクトには変数countが残り続け、実行するたびに1ずつ加算されることになります。
クロージャを使うメリットには、
- 名前空間を局所化できる。
- 単一の関数のように呼び出せる。
などがあります。
3-1-5 即時関数
即時関数は関数定義と実行を同時に行う1回きりの関数のことです。基本的な書き方は次のようになります。
(function(引数){ //何らかの処理 })();
プログラム例を示します。
プログラム
(function(x,y){ let ans = x + y; console.log(ans); })(5,3);
実行結果
8
即時関数のメリットは、1度しか使わない関数などを定義する際に関数名をつけずに作成して呼び出せる点です。通常は1度しか使わない関数でも関数名が必要です。それがいくつもあると毎回重複しない関数名をつける必要がありますが、即時関数を使うことでその煩わしさを解消できます。
最近のJavaScriptはモジュール化などのしくみが備わっているため、書く必要性が少なくなりましたが、古いシステムのJavaScriptでは使われている表現です。
3-1-6 コールバック関数
関数の定義方法というわけではないのですが、よく用いる関数の利用方法としてコールバック関数を紹介しておきます。コールバック関数とは、何らかのタイミングで後から呼び出してもらう関数のことです。次のような用途に使用できます。
- データのダウンロードが終わった時点で関数を呼び出す。
- 一定時間経過後に関数を呼び出す。
コールバック関数を使ったプログラム例を示します。
プログラム
function alarm(){ console.log('一定時間が経過しました。'); } setTimeout(alarm,1000);
実行結果
一定時間が経過しました。
実行結果の内容が約1秒後に表示されます。
setTimeout関数は下のように記述することで、指定した時間
経過後に関数
を呼び出します。時間はミリ秒で指定します。
setTimeout(関数,時間)
例ではsetTimeout(alarm,1000);
と記述していますので、1秒(1000ミリ秒)経過後にalarm関数を呼び出します。このような形で、あとから何らかのきっかけで呼び出される関数をコールバック関数と言います。Webアプリケーションにおけるフロント開発では、サーバーとのデータ通信によく用いられます。
3-2 関数とthis
3-2-1 thisの原則
JavaScriptにおいて、関数を利用するときのthisの扱いは少々複雑です。ここではいくつかのパターンを見ながら、thisの挙動について確認します。
プログラム
let taro = { name:'たろう', introduce:function(){ console.log(this.name + 'です。'); } } taro.introduce(); let work = taro.introduce; work();
実行結果
たろうです。 です。
taro.introduce()
の呼び出しでは、意図どおり「たろうです。」が実行結果として表示できています。ですが、introduceメソッドを代入した変数work
を使った呼び出しでは「undefinedです。」が表示されてしまっています。
JavaScriptのthisは関数実行時の文脈で決まります。具体的には〇〇.関数()
と書いた時に、〇〇
をthisとして実行します。taro.introduce()
の場合は〇〇
がtaro
なので、this.name
は「たろう」となります。一方、work()
の場合は〇〇
がありません。〇〇
が無い場合、thisはグローバルオブジェクトとみなされます。グローバルオブジェクトにはnameプロパティは定義されていないため、this.name
が未定義(undefined)となります。
3-2-2 bind関数
bind関数を使うと、関数とオブジェクトを結びつけておくことができます。プログラム例で動作を見てみましょう。
プログラム
let taro = { name:'たろう', introduce:function(){ console.log(this.name + 'です。'); } } let hanako = { name:'はなこ'}; taro.introduce(); let work = taro.introduce.bind(hanako); work();
実行結果
たろうです。 はなこです。
bind関数は次のように利用します。
関数オブジェクト.bind(結び付けたいオブジェクト)
例ではtaro.introduce.bind(hanako)
のように使用しています。taro.introduce
が指しているのが関数オブジェクトで、hanako
が結び付けたいオブジェクトです。
これにより変数work
に代入される関数にhanako
のオブジェクトを結びつけています。hanako
オブジェクトはnameプロパティを持っていますので、work()
の実行時にnameプロパティが参照され、「はなこです。」が表示されるようになります。
taro.introduce
の記述は違和感を感じる方もいるかもしれません。taro.introduce
と書いてはいますが、単に関数オブジェクトを指す変数と考えると良いでしょう。3-2-3 call関数,apply関数
関数とオブジェクトを結びつける関数として、callやapply関数を使うこともできます。bind関数との違いは、関数実行時に結びつけを行う点です。プログラム例を示します。
プログラム
let taro = { name:'たろう', introduce:function(prefix){ console.log(prefix + this.name + 'です。'); } } let hanako = { name:'はなこ'}; taro.introduce('僕は'); let work = taro.introduce; work.call(hanako,'私は');
実行結果
僕はたろうです。 私ははなこです。
call関数を使って、hanako
オブジェクトを結びつけた上で関数を実行しています。call関数の使い方の基本形は次のようになります。
関数名.call(結びつけたいオブジェクト,関数本来の引数....);
この例では変数work
への代入時にtaro.introduce
とだけ記述していますので、代入時点では特定オブジェクトの結びつけは行っていません。変数work
の指す関数を呼び出す際にwork.call(hanako,'私は');
としています。call関数の1つめの引数は結びつけたいオブジェクトでここではhanako
としています。2つめ以降は関数本来の引数として渡す値です。introduceメソッドは元々引数としてprefix
を持っていますので、その引数に渡す値として'私は'
を指定しています。
apply関数はcall関数とほぼ同じ動作をします。両者の違いは関数本来の引数を配列で渡せる点です。つまり、call関数を利用している部分は次のように書きかえることができます。
//work.call(hanako,'私は'); work.apply(hanako,['私は']);
3-2-4 コールバック関数とthis
次にコールバック関数においてthisが問題となる例を示します。
プログラム
function Person(name){ this.name = name; this.introduce = function(){ console.log(this.name + 'です。'); }; this.delayIntroduce = function(){ setTimeout(this.introduce,1000); } } taro = new Person('たろう'); taro.delayIntroduce();
実行結果
undefinedです。
Personは人物を生成するコンストラクタです。名前を表すname
プロパティと2つのメソッドが定義されています。introduceメソッドはname
プロパティを表示するだけのメソッドです。もう1つのdelayIntroduceメソッドは、1秒遅れてintroduceメソッドを実行することを想定しています。
delayIntroduceメソッドが実行されたら、name
プロパティを参照して名前を表示してほしいところですが、実際にtaroのオブジェクトを生成して実行してみると「undefinedです。」と表示されてしまいます。
この原因はsetTimeout(this.introduce,1000);
の部分にあります。1秒後に実行する関数としてthis.introduce
を渡しています。this.introduceが指すのは単に関数オブジェクトです。setTimeout関数はコールバック関数を呼び出す際、thisがグローバルオブジェクトになる形で呼び出してしまうため、このような結果となってしまいます。
意図した動作にするためには次のように修正します。
//setTimeout(this.introduce,1000); setTimeout(this.introduce.bind(this),1000);
コールバック関数を設定する際にbind関数を呼び出し、関数にthis
(このオブジェクト)を結びつけています。これにより、意図した動作をするようになります。
おわりに
今回は以上となります。最後までご覧いただきありがとうございました。複雑な内容ですが、プログラム例を試したり変更したりしながら、その挙動に慣れていくと良いと思います。ひきつづき、どうぞよろしくお願いいたします。