JavaScript

JavaScriptにおける関数の呼び出し方のパターンとそれに伴う関数内のthisの変化

JavaScriptにおいて関数は第一級オブジェクトです。

つまり、JavaScriptでは関数を他のオブジェクトと同じように扱えてしまうということです。
変数によって参照したり、リテラルで宣言できたり、別の関数の引数として渡したり、他の言語に比べて関数を自由に取り扱うことが出来るのはこの特色に依るものです。
(そもそもJavaScriptにおけるオブジェクトとは、というのはこちらに仔細に書いてあります→ 「オブジェクトを利用する」)

色々なやり方で取り扱えるのは便利ですし、この柔軟性は有効に使いこなせれば強力な武器となるのですが、良くも悪くもやりたい放題出来てしまうのであまり後先考えずにコードを書いていると逆に痛い目を見ることもあります。
しかも、取り扱い方によって微妙に差異の生じる部分も存在します。
その代表例が関数内のthisの内容です。

ということで、この記事ではJavaScriptにおける関数についてと、関数内のthisの振る舞いについて書こうと思います。

関数の呼び出し方

JavaScriptにおいて、関数の呼び出し方は大きく分けて4パターンあります。
1.関数として呼び出す
2.オブジェクトのメソッドとして呼び出す
3.コンストラクタとして呼び出す
4.apply()、もしくはcall()で呼び出す

そして実は、関数内のthisの振る舞いは関数の呼び出し方によって変化します。

これから、この4パターンを具体的に説明し、さらにそれぞれで関数内のthisがどのように変化していくのか説明していきたいと思います。

1.関数を関数として呼び出す

「関数を関数として呼び出す」という文章だけ見ると禅問答のようですが、要は下記のような呼び出し方を指します。

function example1() {
    console.log(this);// thisの中身を出力する
}
example1();

関数を関数名つきで定義し、その関数名によって呼び出すことを「関数を関数として呼び出す」と表現しています。

そして、このコードを実行させてコンソールを確認するとWindowと表示されるはずです。これはwindowオブジェクト(グローバル・コンテキスト)のことです。

つまり、関数を関数として呼び出したとき、その関数内でのthisはwindowオブジェクトを指すということになります。
(windowオブジェクトに関してはこちらに詳しく書いてあります → 「window」)

また、これは

function example1(){
    function inner(){
        console.log(this);
    }
    inner();
}
example1();

のように階層を深くしても同じくwindowオブジェクトを出力します。

2.オブジェクトのメソッドとして関数を呼び出す

var example2 = {};
example2.func = function(){
    console.log(this);
}
example2.func();

冒頭に書いた通り、JavaScriptにおいて関数はオブジェクトの一種なので、上記のようにまずexampleというオブジェクトを用意し、そのメソッドとして関数を用意することが出来ます。
またメソッドによる参照を介して関数を呼び出すことも勿論可能です(この為、上記のコードにおいて関数に関数名は必要ではありません)。

上記のコードを実行すると、コンソールには{func: f}と表示されます。
更にコードに手を加えて

var example2 = {};
example2.x = 2;
example2.func = function(){
    console.log(this);
}
example2.func();

というようにexample2というオブジェクトに別のintというプロパティを追加すると、コンソールには{x: 2, func: f}と表示されます。

つまり、オブジェクトのメソッドとして呼び出された関数の中身のthisには、そのthisを含むオブジェクトが入るということです。
上の例で言えば、this.xで同一オブジェクト内のintプロパティを呼び出すことが出来ます。

3.コンストラクタとして関数を呼び出す

今までの2つの呼び出し方に比べるとなかなか見掛けないのですが、JavaScriptでは関数をコンストラクタとして呼び出すことができます。

function Example3(){
    console.log(this);
}
new Example3();

というように関数呼び出しの前にnewをつけるとコンストラクタとして呼び出すことができます。コンストラクタ呼び出しでは新しいオブジェクトが生成されます。

なので、上記のコードを実行させるとコンソールにはexample {}と新しく生成された空のオブジェクトが入ります。

ちなみに本題には関係ないのですが、JavaScriptにおけるコンストラクタには
・呼び出されると新しい空のオブジェクトを生成する
・関数に明示的な返り値がなければ、その新しいオブジェクトがコンストラクタの値として返される
という特徴があり、普通の関数とは明確に違う使われ方をします。
なので、普通の関数の関数名は一般的に小文字から始まりますが、コンストラクタを生成するための関数は大文字から始まることが命名規約として定められています。例示用のコード内での関数名が急に大文字から始まるようになったのはそのためです。

4.apply()、もしくはcall()による関数の呼び出し

先に説明してしまうと、apply()やcall()は好きなオブジェクトを呼び出す関数のthisに指定することができるメソッドです。
どういうことかというと、

function example4(){
    console.log(this);
}

var element = {
    int: 4;
}

example4.apply(element);

というようなコードを書いた時、これを実行させるとコンソールに{int: 4}と表示されます。
これはapplyがcallだったとしても同じです。引数を二つ以上渡したいときには渡し方に違いが生じるのですが、例のように引数が一つだけの場合はどちらも挙動は変わりません。

最後にもう一度

関数を関数名で呼び出した場合、関数内のthiswindowオブジェクトになる。
関数をオブジェクトのメソッドとして呼び出した場合、関数内のthisそのthisを含んでいるオブジェクトになる。
関数をコンストラクタで呼び出した場合、関数内のthis空のオブジェクトになる。
関数をapply()もしくはcall()で呼び出した場合、関数内のthis自分がapply()もしくはcall()の引数に入れたオブジェクトになる。

2017.12.18 追記

関数内のthisの値を固定させてしまうbindという関数もあります。

function example(){
    console.log(this + 4);
}
var eight = example.bind(8);
eight();

上記のコードを実行すると12と表示されます。つまり、bindで関数exampleの中のthisを8で固定したということです。
bindでは引数も固定できたりします。
詳しくは → 「Function.prototype.bind()