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
で呼び出した場合、this
の値は bind
で変更できません。
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 に感謝します。