30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

this, call(), apply(), bind()

JavaScriptの言語仕様を勉強していくことにした。

いい技術書に巡り合ったこともあり、それなりに理解できるようにはなったが、まだまだ身についてはいない。
あくまでも、技術書の説明を読めば理解できる、というレベルに過ぎない。
これでは実際のコーディングに役立てることは出来ないし、開発中に詰まる度に、調べ直さなきゃいけない。

「読めば分かる」と「理解している」は、かなり距離がある。この距離を埋めていく。

ES5に準拠した内容を学んでいく。

本当はES2015(ES6)を学んだほうがいいのかもしれないが、ES6を体系的にまとめた入門書はまだ見当たらない。
それに、ES2015についての様々な情報は、ES5の内容を理解していることを前提にしているものが多い。
基礎を疎かにしないためにも、背伸びせずES5から学ぶことにした。
そのほうが、スムーズにES2015に移行でき、結果的に早いと思う。歯抜けのまま、理解が曖昧なまま進んでいくのは、よくない。

今回は、thisの基礎について。

2016.9.1追記
thisについてはこちらも参照。
ES2015の関数(アロー関数、this、残余引数など)

this

thisは、その関数を呼び出しているオブジェクトを示す。
グローバルスコープで呼び出した場合は、グローバルオブジェクトを示す。

function sayNameForAll(){
    console.log(this.name);
};

var person1 = {
    name: 'Tom',
    sayName: sayNameForAll,
};
var person2 = {
    name: 'John',
    sayName: sayNameForAll,
};

var name = 'Hoge';

person1.sayName();  // Tom
person2.sayName();  // John
sayNameForAll();    // ブラウザではHoge, Node.jsだとundefined

sayNameForAll()の中で、thisを使っている。
この関数をperson1のメソッドとして呼び出した場合、thisperson1を指す。
同様に、person2のメソッドとして呼び出した場合は、thisperson2を指す。
そのため、それぞれのオブジェクトのnameが表示されることになる。

また、値がHogeであるグローバル変数nameを宣言している。
この場合、sayNameForAll()をグローバルスコープで呼び出した際、「グローバルオブジェクトのnameプロパティ」であるHogeが表示される。
しかし、これはブラウザの場合のみ。Node.jsだとundefinedとなってしまった。

そこで、グローバルオブジェクトそのものを表示してみると、以下のような結果になった。

console.log(this);  // ブラウザではWindowオブジェクト, Node.jsだと空のオブジェクト

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

myFunc();       // ブラウザではWindowオブジェクト, Node.jsだと謎のオブジェクト

ブラウザにおける挙動は予想通りだが、Node.jsの挙動はよく分からない。いずれ別の機会に調べる。

call()

JavaScriptの関数はオブジェクトであるため、関数自身がメソッドを持っている。
その中には、thisを操作できるメソッドもいくつかある。
これらは関数のみが持つメソッドなので、関数以外のオブジェクトが実行しようとすると当然エラーになる。

まずはcall()

第一引数に指定したオブジェクトを、thisとして、関数を実行する。第二引数は、そのまま関数の引数となる。

function sayNameForAll(label){
    console.log(label + 'の名前は、' + this.name);
};

var person1 = {
    name: 'Tom'
};
var person2 = {
    name: 'John'
};

var name = 'Hoge';

sayNameForAll.call(this, 'global'); // globalの名前は、Hoge (Node.jsの場合はHogeではなくundefinedになってしまう)
sayNameForAll.call(person1, 'person1');    // person1の名前は、Tom
sayNameForAll.call(person2, 'person2');    // person2の名前は、John

sayNameForAll.call(name, 'name');  // nameの名前は、undefined
sayNameForAll.call('文字列', 'name');  // nameの名前は、undefined

// undefinedの名前は、Hoge ブラウザの場合
// undefinedの名前は、undefined Node.jsの場合
sayNameForAll.call();

引数を指定しなかった場合、グローバルオブジェクトがthisになるようだ。
その際のブラウザとNode.jsの挙動の差は、先程と同じ。

また、第一引数にプリミティブ型を与えても、エラーにはならない。
上記の例だと文字列が格納されているnameや、文字列そのものを与えているが、エラーにはならず動作する。恐らく、ラッパーオブジェクトが生成されるのだろう。
だが当然、nameプロパティは持っていないので、undefinedとなる。

call()を使えば、関数をそれぞれのオブジェクトにメソッドとして登録する必要がない。
必要なときに、call()を使って呼び出せばいいからだ。

apply()

apply()は、call()とまったく同じ。
違いは、第二引数が配列になり、この配列に必要な引数を全て格納するということだけ。
以下のコードは、call()のときと全く同じ結果になる。

function sayNameForAll(label){
    console.log(label + 'の名前は、' + this.name);
};

var person1 = {
    name: 'Tom'
};
var person2 = {
    name: 'John'
};

var name = 'Hoge';

sayNameForAll.apply(this, ['global']);    // globalの名前は、Hoge (Node.jsの場合はHogeではなくundefinedになってしまう)
sayNameForAll.apply(person1, ['person1']);   // person1の名前は、Tom
sayNameForAll.apply(person2, ['person2']);   // person2の名前は、John
sayNameForAll.apply(name, ['name']); // nameの名前は、undefined
sayNameForAll.apply('文字列', ['name']); // nameの名前は、undefined

// undefinedの名前は、Hoge ブラウザの場合
// undefinedの名前は、undefined Node.jsの場合
sayNameForAll.apply();

bind()

ES5で追加された機能。

bind()は、thisの値を最初に固定して関数を作る。
第一引数に指定したものが、thisとなる。
この方法で作られた関数は、今後どのような文脈で呼ばれても、thisの値は最初に設定したもののまま、変わることがない。

function sayNameForAll(label){
    console.log(label + 'の名前は、' + this.name);
};

var japanese = {
    name: 'Ichiro'
};
var american = {
    name: 'Bob'
};

var sayNameForJ = sayNameForAll.bind(japanese);  // sayNameForJにおいては、thisはjapaneseを指すようになる
sayNameForJ('日本人');  // 日本人の名前は、Ichiro

var sayNameForA = sayNameForAll.bind(american);  // sayNameForAにおいては、thisはamericanを指すようになる
sayNameForA('アメリカ人');    // アメリカ人の名前は、Bob

american.sayName = sayNameForJ; // americanのメソッドになっても、sayNameForJのthisはjapaneseのまま
american.sayName('アメリカ人');   // アメリカ人の名前は、Ichiro

また、第二引数以降の引数は、作成される関数の引数として固定される。
新しく作成した関数を使用する際に与えられた引数は、argumentsの末尾に追加されていく。

function sayNameForAll(label){
    console.log(label + 'の名前は、' + this.name);
    console.log(arguments);
};

var japanese = {
    name: 'Ichiro'
};
var american = {
    name: 'Bob'
};

var sayNameForJ = sayNameForAll.bind(japanese, '日本人');
sayNameForJ();
// 日本人の名前は、Ichiro
// ["日本人"]

var sayNameForA = sayNameForAll.bind(american, 'アメリカ人', '中国人');
sayNameForA('ロシア人');
// アメリカ人の名前は、Bob
// ["アメリカ人", "中国人", "ロシア人"]