JavaScript で this の値を把握するのは難しい場合があります。次に、その方法を示します。
JavaScript の this は、多くのジョークの対象となっています。これは、かなり複雑なためです。ただし、この this を扱わないように、デベロッパーがドメイン固有の複雑な処理を行うことも見受けられます。this について不明な点がある場合は、以下の情報がお役に立てば幸いです。これは私の this ガイドです。
最も具体的な状況から、最も具体的でない状況へと説明していきます。この記事は大きな if (…) … else if () … else if (…) … のようなものです。確認しているコードに一致する最初のセクションに直接移動できます。
- 関数が矢印関数として定義されている場合
- それ以外の場合、関数またはクラスが
newで呼び出された - それ以外の場合、関数に「バインドされた」
this値がある場合 - それ以外の場合、
thisが呼び出し時に設定されている場合 - それ以外の場合、関数が親オブジェクト(
parent.func())を介して呼び出された場合 - それ以外の場合、関数または親スコープが厳格モードの場合
- それ以外の場合
関数がアロー関数として定義されている場合:
const arrowFunction = () => {
console.log(this);
};
この場合、this の値は親スコープの this と常に同じです。
const outerThis = this;
const arrowFunction = () => {
// Always logs `true`:
console.log(this === outerThis);
};
矢印関数は、this の内部値を変更できないため、外側の this と常に同じであるという点で優れています。
その他の例
矢印関数では、bind を使用して this の値を変更できません。
// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();
矢印関数を使用する場合、call または apply で this の値を変更できません。
// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});
アロー関数を使用すると、別のオブジェクトのメンバーとして関数を呼び出して this の値を変更できません。
const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();
アロー関数では、コンストラクタとして関数を呼び出して this の値を変更することはできません。
// TypeError: arrowFunction is not a constructor
new arrowFunction();
「バインドされた」インスタンス メソッド
インスタンス メソッドで、this が常にクラス インスタンスを参照するようにするには、矢印関数とクラス フィールドを使用するのが最善の方法です。
class Whatever {
someMethod = () => {
// Always the instance of Whatever:
console.log(this);
};
}
このパターンは、コンポーネント(React コンポーネントやウェブ コンポーネントなど)のイベント リスナーとしてインスタンス メソッドを使用する場合に非常に便利です。
上記は、「this は親スコープの this と同じになる」というルールに違反しているように見えるかもしれませんが、クラス フィールドをコンストラクタで設定するための糖衣構文と考えると、意味がわかりやすくなります。
class Whatever {
someMethod = (() => {
const outerThis = this;
return () => {
// Always logs `true`:
console.log(this === outerThis);
};
})();
}
// …is roughly equivalent to:
class Whatever {
constructor() {
const outerThis = this;
this.someMethod = () => {
// Always logs `true`:
console.log(this === outerThis);
};
}
}
別のパターンでは、コンストラクタで既存の関数をバインドするか、コンストラクタで関数を割り当てます。なんらかの理由でクラス フィールドを使用できない場合は、コンストラクタで関数を割り当てることをおすすめします。
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
それ以外の場合、関数またはクラスが new で呼び出されると、次のように処理されます。
new Whatever();
上記では、this が Object.create(Whatever.prototype) の結果に設定された状態で Whatever(クラスの場合はそのコンストラクタ関数)が呼び出されます。
class MyClass {
constructor() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
}
// Logs `true`:
new MyClass();
古いスタイルのコンストラクタについても同様です。
function MyClass() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
// Logs `true`:
new MyClass();
その他の例
new で呼び出された場合は、bind で this の値を変更できません。
const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();
new で呼び出された関数を別のオブジェクトのメンバーとして呼び出して、this の値を変更することはできません。
const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();
それ以外の場合、関数に「バインドされた」this 値がある場合:
function someFunction() {
return this;
}
const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);
boundFunction が呼び出されるたびに、その this 値が bind(boundObject)に渡されるオブジェクトになります。
// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);
その他の例
バインドされた関数を呼び出すときに、call または apply で this の値を変更することはできません。
// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
バインドされた関数を呼び出すときに、別のオブジェクトのメンバーとして関数を呼び出して this の値を変更することはできません。
const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);
それ以外の場合は、this が呼び出し時に設定されている場合:
function someFunction() {
return this;
}
const someObject = {hello: 'world'};
// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);
this の値は、call/apply に渡されるオブジェクトです。
残念ながら、this は DOM イベント リスナーなどによって別の値に設定されるため、使用するとコードがわかりにくくなる可能性があります。
element.addEventListener('click', function (event) { // Logs `element`, since the DOM spec sets `this` to // the element the handler is attached to. console.log(this); });
上記のようなケースでは this を使用しないで、代わりに次のようにします。
element.addEventListener('click', (event) => { // Ideally, grab it from a parent scope: console.log(element); // But if you can't do that, get it from the event object: console.log(event.currentTarget); });
それ以外の場合、関数が親オブジェクト(parent.func())を介して呼び出された場合は、次のようにします。
const obj = {
someMethod() {
return this;
},
};
// Logs `true`:
console.log(obj.someMethod() === obj);
この場合、関数は obj のメンバーとして呼び出されるため、this は obj になります。これは呼び出し時に行われるため、親オブジェクトなしで関数を呼び出した場合や、異なる親オブジェクトで呼び出した場合、リンクは切断されます。
const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);
const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);
someMethod は obj のメンバーとして呼び出されないため、someMethod() === obj は false です。次のようなことを試したときに、この問題が発生した可能性があります。
const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');
これは、querySelector の実装が独自の this 値を調べ、それが DOM ノードであることを想定しているため、上記の処理によって接続が切断されるためです。上記を正しく実現するには:
const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);
豆知識: すべての API が内部で this を使用するわけではありません。console.log などのコンソール メソッドが変更され、this への参照が回避されるため、log を console にバインドする必要がなくなりました。
それ以外の場合、関数または親スコープが厳格モードの場合:
function someFunction() {
'use strict';
return this;
}
// Logs `true`:
console.log(someFunction() === undefined);
この場合、this の値は未定義です。親スコープが厳格モード(およびすべてのモジュールが厳格モード)の場合、関数で 'use strict' は必要ありません。
それ以外の場合は以下のとおりです。
function someFunction() {
return this;
}
// Logs `true`:
console.log(someFunction() === globalThis);
この場合、this の値は globalThis と同じです。
さて、
これで完了です。this について確認できた情報は以上です。ご不明な点がございましたら、見落としはございませんか?ツイートでお気軽にお問い合わせください。
レビューを実施した Mathias Bynens 氏、Ingvar Stepanyan 氏、Thomas Steiner 氏に感謝いたします。