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
の値)はベースオブジェクトとなります。
ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを言います。 ベースオブジェクトがない場合のthis
はundefined
となります。
たとえば、fn()
のように関数を呼び出したとき、このfn
関数呼び出しのベースオブジェクトはないため、this
はundefined
となります。 一方、obj.method()
のようにメソッドを呼び出したとき、このobj.method
メソッド呼び出しのベースオブジェクトはobj
オブジェクトとなり、this
はobj
となります。
// `fn`関数はメソッドではないのでベースオブジェクトはない
fn();
// `obj.method`メソッドのベースオブジェクトは`obj`
obj.method();
// `obj1.obj2.method`メソッドのベースオブジェクトは`obj2`
// ドット演算子、ブラケット演算子どちらも結果は同じ
obj1.obj2.method();
obj1["obj2"]["method"]();
関数宣言や関数式におけるthis
まずは、関数宣言や関数式の場合を見ていきます。
次の例では、関数宣言で関数fn1
と関数式で関数fn2
を定義し、それぞれの関数内でthis
を返します。 定義したそれぞれの関数をfn1()
とfn2()
のようにただの関数として呼び出しています。 このとき、ベースオブジェクトはないため、this
はundefined
となります。
"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ではない状況でthis
がundefined
の場合は、this
がグローバルオブジェクトを参照するように変換される問題があるためです。
strict modeは、このような意図しにくい動作を防止するために導入されています。 しかしながら、strict modeのメソッド以外の関数におけるthis
はundefined
となるため使い道がありません。 そのため、メソッド以外でthis
を使う必要はありません。
ソッド呼び出しにおけるthis
次に、メソッドの場合を見ていきます。 メソッドの場合は、そのメソッドが何かしらのオブジェクトに所属しています。 なぜなら、JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼ぶためです。
次の例ではmethod1
とmethod2
はそれぞれメソッドとして呼び出されています。 このとき、それぞれのベースオブジェクトはobj
となり、this
はobj
となります。
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
メソッドを参照)のベースオブジェクトはありません。 そのため、this
はundefined
となり、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
(関数オブジェクト)にはcall
、apply
、bind
といった明示的にthis
を指定して関数を実行するメソッドが用意されています。
call
メソッドは第一引数にthis
としたい値を指定し、残りの引数には呼び出す関数の引数を指定します。 暗黙的に渡されるthis
の値を明示的に渡せるメソッドと言えます。
関数.call(thisの値, ...関数の引数);
次の例ではthis
にperson
オブジェクトを指定した状態で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]);
次の例ではthis
にperson
オブジェクトを指定した状態で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された関数
次の例ではthis
をperson
オブジェクトに束縛した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
メソッドをただの関数で表現すると次のように書けます。 bind
はthis
や引数を束縛した関数を作るメソッドだということがわかります。
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!"
このようにcall
、apply
、bind
メソッドを使うことでthis
を明示的に指定した状態で関数を呼び出せます。 しかし、毎回関数を呼び出すたびにこれらのメソッドを使うのは、関数を呼び出すための関数が必要になってしまい手間がかかります。 そのため、基本的には「メソッドとして定義されている関数はメソッドとして呼ぶこと」でこの問題を回避するほうがよいでしょう。 その中で、どうしてもthis
を固定したい場合にはcall
、apply
、bind
メソッドを利用します。
Arrow Functionとthis
Arrow Functionで定義された関数やメソッドにおけるthis
がどの値を参照するかは関数の定義時(静的)に決まります。 一方、Arrow Functionではない関数においては、this
は呼び出し元に依存するため関数の実行時(動的)に決まります。
Arrow Functionとそれ以外の関数で大きく違うことは、Arrow Functionはthis
を暗黙的な引数として受けつけないということです。 そのため、Arrow Function内にはthis
が定義されていません。このときのthis
は外側のスコープ(関数)のthis
を参照します。
これは、変数におけるスコープチェーンの仕組みと同様で、そのスコープにthis
が定義されていない場合には外側のスコープを探索します。 そのため、Arrow Function内のthis
の参照で、常に外側のスコープ(関数)へとthis
の定義を探索しに行きます(詳細はスコープチェーンを参照)。 また、this
はECMAScriptのキーワードであるため、ユーザーはthis
という変数を定義できません。
次の例では、関数式で定義したArrow Functionの中のthis
をコンソールに出力しています。 このとき、fn
の外側には関数がないため、「自身より外側のスコープに定義されたもっとも近い関数」の条件にあてはまるものはありません。 このときのthis
はトップレベルに書かれたthis
と同じ値になります。
// Arrow Functionで定義した関数
const fn = () => {
// この関数の外側には関数は存在しない
// トップレベルの`this`と同じ値
return this;
};
console.log(fn() === this); // => true