英文学生の日常

業務日誌.tomo

業務で感じたことをひたすら綴る

JavaScript thisのまとめ

メソッド

JavaScriptではオブジェクトのプロパティが関数である場合にそれをメソッドと呼びます。 一般的にはメソッドも含めたものを関数と言い、関数宣言などとプロパティである関数を区別する場合にメソッドと呼びます。

メソッドを定義する場合には、オブジェクトのプロパティに関数式を定義するだけです。

const obj = { // `function`キーワードを使ったメソッド method1: function() { }, // Arrow Functionを使ったメソッド method2: () => { } };

 

this

thisは基本的にはメソッドの中で利用しますが、thisは読み取り専用のグローバル変数のようなものでどこにでも書けます。 加えて、thisの参照先(評価結果)は条件によって異なります。

thisの参照先は主に次の条件によって変化します。

  • 実行コンテキストにおけるthis
  • コンストラクタにおけるthis
  • 関数とメソッドにおけるthis
  • Arrow Functionにおけるthis

 

Arrow Function以外の関数(メソッドも含む)におけるthisは、実行時に決まる値となります。 言い方を変えるとthisは関数に渡される暗黙的な引数のようなもので、その渡される値は関数を実行するときに決まります。

 

関数におけるthisの基本的な参照先(暗黙的に関数に渡すthisの値)はベースオブジェクトとなります。

ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを言います。 ベースオブジェクトがない場合のthisundefinedとなります。

たとえば、fn()のように関数を呼び出したとき、このfn関数呼び出しのベースオブジェクトはないため、thisundefinedとなります。 一方、obj.method()のようにメソッドを呼び出したとき、このobj.methodメソッド呼び出しのベースオブジェクトはobjオブジェクトとなり、thisobjとなります。

// `fn`関数はメソッドではないのでベースオブジェクトはない
fn();
// `obj.method`メソッドのベースオブジェクトは`obj`
obj.method();
// `obj1.obj2.method`メソッドのベースオブジェクトは`obj2`
// ドット演算子、ブラケット演算子どちらも結果は同じ
obj1.obj2.method();
obj1["obj2"]["method"]();

関数宣言や関数式におけるthis

まずは、関数宣言や関数式の場合を見ていきます。

次の例では、関数宣言で関数fn1と関数式で関数fn2を定義し、それぞれの関数内でthisを返します。 定義したそれぞれの関数をfn1()fn2()のようにただの関数として呼び出しています。 このとき、ベースオブジェクトはないため、thisundefinedとなります。

 

"use strict";
function fn1() {
    return this;
}
const fn2 = function() {
    return this;
};
// 関数の中の`this`が参照する値は呼び出し方によって決まる
// `fn1`と`fn2`どちらもただの関数として呼び出している
// メソッドとして呼び出していないためベースオブジェクトはない
// ベースオブジェクトがない場合、`this`は`undefined`となる
console.log(fn1()); // => undefined
console.log(fn2()); // => undefined

これは、関数の中に関数を定義して呼び出す場合も同じです。

 

"use strict";
function outer() {
    console.log(this); // => undefined
    function inner() {
        console.log(this); // => undefined
    }
    // `inner`関数呼び出しのベースオブジェクトはない
    inner();
}
// `outer`関数呼び出しのベースオブジェクトはない
outer();

この書籍では注釈がないコードはstrict modeとして扱いますが、コード例に"use strict";と改めてstrict modeを明示しています。 なぜなら、strict modeではない状況でthisundefinedの場合は、thisがグローバルオブジェクトを参照するように変換される問題があるためです。

strict modeは、このような意図しにくい動作を防止するために導入されています。 しかしながら、strict modeのメソッド以外の関数におけるthisundefinedとなるため使い道がありません。 そのため、メソッド以外でthisを使う必要はありません。

 

ソッド呼び出しにおけるthis

次に、メソッドの場合を見ていきます。 メソッドの場合は、そのメソッドが何かしらのオブジェクトに所属しています。 なぜなら、JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼ぶためです。

次の例ではmethod1method2はそれぞれメソッドとして呼び出されています。 このとき、それぞれのベースオブジェクトはobjとなり、thisobjとなります。

 

const obj = {
    // 関数式をプロパティの値にしたメソッド
    method1: function() {
        return this;
    },
    // 短縮記法で定義したメソッド
    method2() {
        return this;
    }
};
// メソッド呼び出しの場合、それぞれの`this`はベースオブジェクト(`obj`)を参照する
// メソッド呼び出しの`.`の左にあるオブジェクトがベースオブジェクト
console.log(obj.method1()); // => obj
console.log(obj.method2()); // => obj

これを利用すれば、メソッドの中から同じオブジェクトに所属する別のプロパティをthisで参照できます。

 

const person = {
    fullName: "Brendan Eich",
    sayName: function() {
        // `person.fullName`と書いているのと同じ
        return this.fullName;
    }
};
// `person.fullName`を出力する
console.log(person.sayName()); // => "Brendan Eich"

このようにメソッドが所属するオブジェクトのプロパティを、オブジェクト名.プロパティ名の代わりにthis.プロパティ名で参照できます。

オブジェクトは何重にもネストできますが、thisはベースオブジェクトを参照するというルールは同じです。

次のコードを見てみると、ネストしたオブジェクトにおいてメソッド内のthisがベースオブジェクトであるobj3を参照していることがわかります。 このときのベースオブジェクトはドットでつないだ一番左のobj1ではなく、メソッドから見てひとつ左のobj3となります。

 

const obj1 = {
    obj2: {
        obj3: {
            method() {
                return this;
            }
        }
    }
};
// `obj1.obj2.obj3.method`メソッドの`this`は`obj3`を参照
console.log(obj1.obj2.obj3.method() === obj1.obj2.obj3); // => true

 

問題: thisを含むメソッドを変数に代入した場合

JavaScriptではメソッドとして定義したものが、後からただの関数として呼び出されることがあります。 なぜなら、メソッドは関数を値に持つプロパティのことで、プロパティは変数に代入し直すことができるためです。

そのため、メソッドとして定義した関数も、別の変数に代入してただの関数として呼び出されることがあります。 この場合には、メソッドとして定義した関数であっても、実行時にはただの関数であるためベースオブジェクトが変わっています。 これはthisが定義した時点ではなく実行したときに決まるという性質そのものです。

具体的に、thisが実行時に変わる例を見ていきます。 次の例では、person.sayNameメソッドを変数sayに代入してから実行しています。 このときのsay関数(sayNameメソッドを参照)のベースオブジェクトはありません。 そのため、thisundefinedとなり、undefined.fullNameは参照できずに例外を投げます。

 

"use strict";
const person = {
    fullName: "Brendan Eich",
    sayName: function() {
        // `this`は呼び出し元によって異なる
        return this.fullName;
    }
};
// `sayName`メソッドは`person`オブジェクトに所属する
// `this`は`person`オブジェクトとなる
console.log(person.sayName()); // => "Brendan Eich"
// `person.sayName`を`say`変数に代入する
const say = person.sayName;
// 代入したメソッドを関数として呼ぶ
// この`say`関数はどのオブジェクトにも所属していない
// `this`はundefinedとなるため例外を投げる
say(); // => TypeError: Cannot read property 'fullName' of undefined

結果的には、次のようなコードが実行されているのと同じです。 次のコードでは、undefined.fullNameを参照しようとして例外が発生しています。

 

"use strict";
// const say = person.sayName; は次のようなイメージ
const say = function() {
    return this.fullName;
};
// `this`は`undefined`となるため例外を投げる
say(); // => TypeError: Cannot read property 'fullName' of undefined

このように、Arrow Function以外の関数において、thisは定義したときではなく実行したときに決定されます。 そのため、関数にthisを含んでいる場合、その関数は意図した呼ばれ方がされないと間違った結果が発生するという問題があります。

この問題の対処法としては大きく分けて2つあります。

1つはメソッドとして定義されている関数はメソッドとして呼ぶということです。 メソッドをわざわざただの関数として呼ばなければそもそもこの問題は発生しません。

もう1つは、thisの値を指定して関数を呼べるメソッドで関数を実行する方法です。

 

処法: call、apply、bindメソッド

関数やメソッドのthisを明示的に指定して関数を実行する方法もあります。 Function(関数オブジェクト)にはcallapplybindといった明示的にthisを指定して関数を実行するメソッドが用意されています。

callメソッドは第一引数にthisとしたい値を指定し、残りの引数には呼び出す関数の引数を指定します。 暗黙的に渡されるthisの値を明示的に渡せるメソッドと言えます。

関数.call(thisの値, ...関数の引数);

次の例ではthispersonオブジェクトを指定した状態でsay関数を呼び出しています。 callメソッドの第二引数で指定した値が、say関数の仮引数messageに入ります。

 

"use strict";
function say(message) {
    return `${message} ${this.fullName}!`;
}
const person = {
    fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
console.log(say.call(person, "こんにちは")); // => "こんにちは Brendan Eich!"
// `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生
say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined

applyメソッドは第一引数にthisとする値を指定し、第二引数に関数の引数を配列として渡します。

関数.apply(thisの値, [関数の引数1, 関数の引数2]);

次の例ではthispersonオブジェクトを指定した状態でsay関数を呼び出しています。 applyメソッドの第二引数で指定した配列は、自動的に展開されてsay関数の仮引数messageに入ります。

 

"use strict";
function say(message) {
    return `${message} ${this.fullName}!`;
}
const person = {
    fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
// callとは異なり引数を配列として渡す
console.log(say.apply(person, ["こんにちは"])); // => "こんにちは Brendan Eich!"
// `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生
say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined

callメソッドとapplyメソッドの違いは、関数の引数への値の渡し方が異なるだけです。 また、どちらのメソッドもthisの値が不要な場合はnullを渡すのが一般的です。

 

function add(x, y) {
    return x + y;
}
// `this`が不要な場合は、nullを渡す
console.log(add.call(null, 1, 2)); // => 3
console.log(add.apply(null, [1, 2])); // => 3

最後にbindメソッドについてです。 名前のとおりthisの値を束縛(bind)した新しい関数を作成します。

関数.bind(thisの値, ...関数の引数); // => thisや引数がbindされた関数

次の例ではthispersonオブジェクトに束縛したsay関数をラップした関数を作っています。 bindメソッドの第二引数以降に値を渡すことで、束縛した関数の引数も束縛できます。

 

function say(message) {
    return `${message} ${this.fullName}!`;
}
const person = {
    fullName: "Brendan Eich"
};
// `this`を`person`に束縛した`say`関数をラップした関数を作る
const sayPerson = say.bind(person, "こんにちは");
console.log(sayPerson()); // => "こんにちは Brendan Eich!"

このbindメソッドをただの関数で表現すると次のように書けます。 bindthisや引数を束縛した関数を作るメソッドだということがわかります。

 

function say(message) {
    return `${message} ${this.fullName}!`;
}
const person = {
    fullName: "Brendan Eich"
};
// `this`を`person`に束縛した`say`関数をラップした関数を作る
//  say.bind(person, "こんにちは"); は次のようなラップ関数を作る
const sayPerson = () => {
    return say.call(person, "こんにちは");
};
console.log(sayPerson()); // => "こんにちは Brendan Eich!"

このようにcallapplybindメソッドを使うことでthisを明示的に指定した状態で関数を呼び出せます。 しかし、毎回関数を呼び出すたびにこれらのメソッドを使うのは、関数を呼び出すための関数が必要になってしまい手間がかかります。 そのため、基本的には「メソッドとして定義されている関数はメソッドとして呼ぶこと」でこの問題を回避するほうがよいでしょう。 その中で、どうしてもthisを固定したい場合にはcallapplybindメソッドを利用します。

 

Arrow Functionとthis

Arrow Functionで定義された関数やメソッドにおけるthisがどの値を参照するかは関数の定義時(静的)に決まります。 一方、Arrow Functionではない関数においては、thisは呼び出し元に依存するため関数の実行時(動的)に決まります。

Arrow Functionとそれ以外の関数で大きく違うことは、Arrow Functionはthisを暗黙的な引数として受けつけないということです。 そのため、Arrow Function内にはthisが定義されていません。このときのthisは外側のスコープ(関数)のthisを参照します。

これは、変数におけるスコープチェーンの仕組みと同様で、そのスコープにthisが定義されていない場合には外側のスコープを探索します。 そのため、Arrow Function内のthisの参照で、常に外側のスコープ(関数)へとthisの定義を探索しに行きます(詳細はスコープチェーンを参照)。 また、thisECMAScriptのキーワードであるため、ユーザーはthisという変数を定義できません。

 

次の例では、関数式で定義したArrow Functionの中のthisをコンソールに出力しています。 このとき、fnの外側には関数がないため、「自身より外側のスコープに定義されたもっとも近い関数」の条件にあてはまるものはありません。 このときのthisはトップレベルに書かれたthisと同じ値になります。

 

// Arrow Functionで定義した関数
const fn = () => {
    // この関数の外側には関数は存在しない
    // トップレベルの`this`と同じ値
    return this;
};
console.log(fn() === this); // => true