TL;DR
- アロー関数とそうでない関数(※以下、「通常関数」と記述)は、単に記述方法が異なるだけではなく、その違いの一つとして、thisの指す値が挙げられる
- アロー関数では関数定義時に、通常関数では実行時にthisの値が決まる
- 通常関数においてthisを決定する要因は実行モード、対象のスクリプトの種別、実行コンテキスト、呼び出し方、関数定義方法など複数あり、これを常に意識するのはつらみがある
- つまり、基本的にはアロー関数使いましょう
- アロー関数はthisを持たないため、そのアロー関数自身のより外側のスコープチェーンをたどった結果もっとも近いthisがアロー関数のコンテキスト内で参照できるthisとなる
thisは何を指すのか
thisの初期値
- thisの値は状況により変動するので、一概に説明できないが初期値が存在する
- 以下のとおり、strictモードか否かでthisの初期値が変わってくる
非strictモード | strictモード |
---|---|
windowオブジェクト | undefined |
※本記事では、原則としてstrictモードの前提でコードを例示する
thisの前提知識
- thisは読み取り専用のグローバル変数のため、上書き不可
- プロパティの追加や値の変更は可能
- 実際の開発におけるthisのユースケースのほとんどは、クラス内のthisである(以下のようなjQueryのthisもあるとは思うが・・・)
document.getElementById("submit-button").onclick = function() {
// thisはbutton要素
console.log(this);
};
- globalThis
- thisと同様にグローバルオブジェクトを指すが、thisとは異なり、実行環境(ブラウザ・サーバ)に依存しない
- グローバルオブジェクトを参照したい場合、ブラウザ側ではwindowを、Node.js側ではglobalという別々のキーワードを使用する必要があったが、ES2020で実行環境にとらわれずグローバルオブジェクトを参照するためのキーワードとして導入された
thisはどのように決まるのか
- 通常関数におけるthisの値は、関数が呼び出された際に動的に決定される(アロー関数はこの限りでない)
- そのうえで、
this
は大きく以下の要因によってその値が変化する
項目 | 内容 |
---|---|
実行モード | strictモードか否か |
対象のスクリプトの種別 | ScriptかModuleか |
実行コンテキスト | グローバルコンテキストと関数コンテキスト |
呼び出し方 | call, apply |
関数定義方法 | bindメソッド, アロー関数か否か |
実行モード
- 前述の通り、strictモードではthisの値はundefinedとなり、非strictモードであればwindowオブジェクトとなる(明示的にthisが指定されていなければ)
対象のスクリプトの種別
- スクリプトとして実行されている場合で非strictモードであればwindowオブジェクト、strictモードの場合はundefinedとなる
<script>
console.log(this); //window(非strict) or undefined
</script>
- モジュールとして実行されている場合は、strictモード扱いとなるため、undefinedとなる
<script type="module">
console.log(this); // undefined
</script>
実行コンテキスト
- どのような状況(文脈)でコードが実行されているかという情報
グローバルコンテキスト
- トップレベル(関数の外側)を指す
console.log(this); // undefined
```
function ...() {
///
};
関数コンテキスト
- 関数が実行される直前に生成される、関数内の環境({ }の中)を指す
- 通常関数では、thisは関数定義時でなく実行時に決まる
- 関数コンテキストは、さらにいくつかの種類に分けられ、その内容ごとに
this
の値が変わってくる - thisの値は
オブジェクト.func()
、オブジェクト["func"]()
のようなベースとなるオブジェクトがある場合はそのオブジェクトを、ない場合はundefinedとなる
const ...
function hoge() {
// 関数コンテキスト({ }の中)
console.log(this) // 原則undefinedだが、呼び出し方により変動する
};
constructor
this
はnew演算子でインスタンス化されたオブジェクトを指す
関数コンテキスト(入れ子)
- 入れ子関数の場合、少し複雑になる(以下のコード例では、説明の便宜上、入れ子関数をpersonというオブジェクトに持たせている)
- 入れ子関数の外側のthis(以下の①)personオブジェクトを、入れ子関数の内側のthis(以下の②)はundefinedを指す
- ②はpersonオブジェクトから呼び出されているわけではないため(ベースオブジェクトがなく、通常関数のコンテキストと同じ要領)
- なお、入れ子になる関数がいくつ増えても同じ動作となる
const person = {
printName: function() {
console.log(this) //personを指す・・・①
const inner = function() {
console.log(this) //undefinedを指す・・・②
}()
}
};
person.printName()
メソッド
this
はnew演算子でインスタンス化されたオブジェクトを指す
class Person {
constructor(name) {
this.name = name;
}
printName() {
console.log(this.name); //Person のname("tanaka")を指す
}
}
const person = new Person("tanaka")
person.printName() // "tanaka"
- ネストしたオブジェクトでも考え方は同じでベースオブジェクトに着目すればよい
const person = {
name: {
fullName: {
print() {
return this;
},
},
},
};
console.log(person.name.fullName.print() === person.name.fullName); //true
- なお、以下のようにメソッドを関数の外でいったん別の変数に代入して実行すると、thisは
undefined
を指すようになる(通常関数コンテキストと同じ要領)
const person = {
printName: function() {
console.log(this) //personを指す
}
};
// ↓
const printName = person.printName
printName() // undefinedを指す
以下のように,演算子
を使用してもおなじ
const person = {
printName: function () {
console.log(this); //personを指す
},
};
//↓
(0, person.printName)(); // undefinedを指す
グローバルオブジェクトがもつメソッド内
- グローバルオブジェクトがもつメソッドとは、例えば
setTimeout
などのこと - これらのメソッドの中でthisを呼び出す場合は、
this
はundefined
を指す - ※ 後述の通り、setTimeoutに渡す関数をアロー関数にするとこの限りではない
class Person {
constructor(name) {
this.name = name;
}
printName() {
console.log(this); //Person {name: "tanaka"}を指す
setTimeout(function () {
console.log(this); // undefined
}, 1000);
}
}
const person = new Person("tanaka")
person.printName()
コールバック関数
- コールバック関数内で
- mapに渡したコールバック関数は、配列の各要素に対して適用され、その際の呼び出され方は単に
callbackFn()
イメージで、ベースオブジェクトがないため、thisはundefinedとなる - 以下では、mapの引数に渡しているコールバック関数内でthisを参照しているthisがundefinedとなることを知らず自身のオブジェクトを指すと思いnameプロパティを呼び出してエラーになっているケース
class Person {
name;
constructor(name) {
this.name = name;
}
isContain(names) {
names.map(function (name) {
console.log(this.name === name); //TypeError: Cannot read properties of undefined (reading 'name')
});
}
}
new Person('keito').isContain(['keito']);
呼び出し方
call
,apply
を使用すると、thisの値を指定して関数を呼び出すことが可能- 以下の①では、通常通りpersonオブジェクトのメソッドを呼び出しているので、thisはpersonを指している
- 以下の②では、
call
を使用してメソッドを呼び出しているので、thisはundefined
を指している
const person = {
printName: function() {
console.log(this)
}
};
person.printName() //personを指す・・・①
person.printName.call() //undefinedを指す・・・②
call,applyの違いは第二引数(=関数の引数)の指定方法が、配列かそうでないかの違いのみ
function printProfile(hobby, myfavoriteFood) { //本来、関数コンテキストのthisはundefinedだが、call,applyでthisを指定しているのでpersonを指す
console.log(this.age); //20
console.log(hobby); //釣り
console.log(myfavoriteFood); //ハンバーガー
}
const person = {
name: 'tanaka',
age: 20,
};
const hobby = '釣り';
const myfavoriteFood = 'ハンバーガー';
printProfile.call(person, hobby, myfavoriteFood);
printProfile.apply(person, [hobby, myfavoriteFood]);
上記で説明済みの関数コンテキスト(入れ子)の場合も以下の通り、inner関数をcallを使ってthisを指定してあげると、personを指すようになる
const person = {
printName: function () {
console.log(this); //personを指す
const inner = function () {
console.log(this); //personを指す
};
inner.call(this);
},
};
person.printName();
関数定義方法
bind
bind
メソッドを使用することで、関数定義時にthisを固定することが可能- 以下のようにすることで、setTimeout内のthisを
undefined
からpersonオブジェクトに変更(固定)することが可能 - bindの返り値は、オブジェクト束縛後の生成された新しい関数が返る
class Person {
constructor(name) {
this.name = name;
}
printName() {
console.log(this); //Person {name: "tanaka"}
setTimeout(function () {
console.log(this); //Person {name: "tanaka"} こちらもPersonを指すようになる
}.bind(this), 1000);
}
}
const person = new Person("tanaka")
person.printName()
なお、以下のようアロー関数の形式でsetTimeOutに関数を渡すと、thisが自身のpersonオブジェクトを指すようになる(※アロー関数についての詳細は下部で説明)
class Person {
constructor(name) {
this.name = name;
}
printName() {
setTimeout(() => {
console.log(this); //Person {name: "tanaka"}を指すようになる
}, 1000);
}
}
const person = new Person('tanaka');
person.printName();
アロー関数
- thisは実行時ではなく、宣言時に静的に決まる(=レキシカルスコープ)
- thisはそのアロー関数自身の外側のスコープチェーンをたどった結果としてもっとも近いthisを指す(常にアロー関数の外側の関数のthisをそのまま利用)
class Person {
name;
constructor(name) {
this.name = name;
}
isContain(names) {
names.map((name) => {
console.log(this.name === name); //true
});
}
}
new Person('keito').isContain(['keito']);
- イベントハンドラー内のthisは、イベントの発生元(要素)を表す
- 以下のようなイベントリスナーの登録時にアロー関数を使うかそうでないかで、thisの値が変わってくる
(=なんでもかんでもアロー関数を使用すればよいということではない)
document.getElementById("submit-button").onclick = function() {
// thisはbutton要素
console.log(this);
};
document.getElementById("submit-button").onclick = () => {
// thisはundefined
console.log(this);
};
- アロー関数の外側の関数のthisをアロー関数内で参照できるのみで、アロー関数自体はthisを保有していない
- アロー関数に対してcall、apply、bindを使ったthisの指定はできない(指定しても無視される)
const arrow = () => {
return this;
};
console.log(arrow()); // undefined
// callを使用しても、thisは変わらない
console.log(arrow.call({ name: 'hoge' })); // undefined
まとめ
いかがでしたでしょうか。本記事では、JavaScriptのthisについて最低限押さえておくべきことについてサンプルコードを提示しながら説明しました。特に、通常関数とアロー関数におけるthisの差異や、thisを固定する方法については重要性が高いので、ぜひ押さえてください。