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 氏に感謝いたします。