記事の始まりに先立ち、まずは問題を解いてみましょう:
window.name = "window";
function User(name) {
this.name = name;
this.greet1 = function() {
console.log(this.name);
};
this.greet2 = function() {
return function() {
console.log(this.name);
};
};
this.greet3 = function() {
return () => console.log(this.name);
};
}
const UserA = new User("UserA");
const UserB = new User("UserB");
UserA.greet1();
UserA.greet1.call(UserB);
UserA.greet2()();
UserA.greet2.call(UserB)();
UserA.greet3()();
UserA.greet3.call(UserB)();
自分で答えを書いてみてもいいですし、答えを確認したい場合はそのまま記事の最後に移動してください。最後に解析も行います。
この記事では、以下の五つのルールから JavaScript
における this を紹介します:
- 暗黙のバインディング
- 明示的なバインディング
- new バインディング
- レキシカルバインディング
- デフォルトバインディング
暗黙のバインディング#
まずはコードを見てみましょう:
const user = {
name: 'Jeremy',
greet () {
console.log(`私の名前は ${this.name} です`)
}
}
user
オブジェクトの greet
メソッドを呼び出してみましょう:
user.greet(); // 私の名前は Jeremy です
user
オブジェクトを通じて greet
メソッドを呼び出すと、greet
内の this
は user
オブジェクトを指していることがわかります。これが暗黙のバインディングのポイントです: 関数がコンテキストオブジェクトを持つ場合、暗黙のバインディングは関数呼び出しの this
をそのコンテキストオブジェクトにバインドします。したがって、ここでの this.name
は user.name
と同じです。
少し拡張してみましょう:
const user = {
name: 'Jeremy',
greet () {
console.log(`私の名前は ${this.name} です`)
},
son: {
name: 'lap',
greet () {
console.log(`私の名前は ${this.name} です`)
}
}
}
user.son.greet()
を呼び出した結果はあなたの期待通りですか?
では、コードを書き換えてみましょう:
function greet () {
console.log(`私の名前は ${this.name} です`)
}
const user = {
name: 'Jeremy'
}
greet
を独立した関数に分けました。では、greet
内の this
を user
オブジェクトに指すようにするにはどうすればよいでしょうか?
明示的なバインディング#
JavaScript では、すべての関数にはこの機能を実現するためのメソッドがあります(つまり、this
の指し示す先を変更することができます)。それが call
です:
call()
メソッドは、指定されたthis
値を持つ関数を呼び出し、提供された引数(引数のリスト)をそれぞれ渡します。
したがって、次のように呼び出すことができます:
greet.call(user)
これが 明示的なバインディング の意味です。私たちは明示的に(.call
を使用して)this
の指し示す先を指定しました。
もし greet
にいくつかの引数を渡したい場合、call
メソッドの残りの引数を使用する必要があります:
function greet (l1, l2, l3) {
console.log(`私の名前は ${this.name} で、${l1}、${l2}、${l3} を知っています`)
}
const user = {
name: 'Jeremy'
}
const languages = ['JavaScript', 'Java', 'PHP']
greet.call(user, languages[0], languages[1], languages[2]) // 私の名前は Jeremy で、JavaScript、Java、PHP を知っています
これらのコードを実際に実行すると、languages
配列の要素を一つずつ渡すのが面倒だと気づくでしょう。この場合、より良い選択肢があります。それが .apply
です:
apply()
メソッドは、指定されたthis
値を持つ関数を呼び出し、配列(または [配列のようなオブジェクト](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Indexed_collections#Working_with_array-like_objects))として提供された引数を渡します。
.apply
と .call
の唯一の違いは、引数の渡し方です。したがって、次のように呼び出すことができます:
greet.apply(user, languages) // 私の名前は Jeremy で、JavaScript、Java、PHP を知っています
最後に紹介するのは .bind
です:
bind()
メソッドは、新しい関数を作成し、呼び出すときにthis
キーワードを提供された値に設定します。そして、新しい関数を呼び出すときに、指定された引数リストを元の関数の引数の先頭に追加します。
.bind
は .call
と似ていますが、違いは .call
は即座に呼び出されるのに対し、.bind
は新しい関数を返し、後で呼び出すことができます:
const newFn = greet.bind(user, languages[0], languages[1], languages[2])
newFn() // 私の名前は Jeremy で、JavaScript、Java、PHP を知っています
new バインディング#
新しいコードを見てみましょう:
function User (name) {
this.name = name
}
const me = new User('Jeremy')
console.log(me.name) // Jeremy
new
を使って User
を呼び出すと、新しいオブジェクトが構築され、そのオブジェクトが User
呼び出しの this
にバインドされます。
JavaScript
のnew
の使い方は、従来のクラスベースの言語と似ていますが、内部メカニズムは全く異なります。new
を使って関数を呼び出すと、次の操作が実行されます:
- 新しいオブジェクトを作成
- このオブジェクトをプロトタイプにリンク
- このオブジェクトを関数呼び出しの
this
にバインド- もしその関数が他のオブジェクトを返さなければ、この新しいオブジェクトを返す
レキシカルバインディング#
上記で紹介した四つのルールは、すべての通常の関数をカバーしていますが、ES6
では特別な関数が導入されました。それがアロー関数です。
アロー関数式 の構文は関数式よりも短く、独自の this、arguments、super または new.target を持ちません。これらの関数式は、本来匿名関数が必要な場所に適しており、コンストラクタ関数として使用することはできません。
アロー関数は独自の this
を持たず、外側の(関数またはグローバル)スコープに基づいて this
を決定します。
では、コードを書き換えてみましょう:
const user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
return function () {
console.log(this.name);
}
}
}
greet
メソッド内で関数を返しましたが、user.greet()()
を呼び出すと、返されるのは undefined
です。
この理由は、返された関数を呼び出すときにバインドされたコンテキストオブジェクトがないため(デフォルトで window
になります)です。したがって、直接的な解決策として 明示的なバインディング を使用することを考え、コードを次のように変更します:
const user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
return function () {
console.log(this.name);
}.bind(this)
}
}
user.greet()() // Jeremy
では、アロー関数 を使って書き換えた場合はどうなるでしょうか?
const user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
return () => {
console.log(this.name);
}
}
}
user.greet()() // Jeremy
アロー関数の this
の探索ルールは、実際には 変数探索 に似ています。ES6
以前から、ほぼ同等のパターンを使用していました:
var user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
var self = this;
return function () {
console.log(self.name);
}
}
}
user.greet()() // Jeremy
同じ関数やプログラム内で、これら二つのスタイルを混在させない方が良いです。そうしないと、コードがより難しくなり、メンテナンスも大変になります。
デフォルトバインディング#
最後に、このコードを再度見てみましょう:
function greet () {
console.log(`私の名前は ${this.name} です`)
}
const user = {
name: 'Jeremy'
}
もし greet
を直接呼び出したら、何が起こるでしょうか?
greet() // 私の名前は undefined です
これが最後のルールを引き起こします。もし 暗黙のバインディング(オブジェクト呼び出し)も 明示的なバインディング(.call
、.apply
、.bind
)もなく、new バインディングもない場合、JavaScript
はデフォルトで this
を window
オブジェクトに指し示します(したがって、デフォルトバインディングは ウィンドウバインディング とも呼ばれます):
window.name = 'window'
function greet () {
console.log(`私の名前は ${this.name} です`)
}
const user = {
name: 'Jeremy'
}
greet() // window
ES5 では、厳密モードを有効にすると、
JavaScript
はthis
をundefined
に保ちます。
まとめ#
this
の指し示す先を判断するためのプロセスをまとめてみましょう:
- まず、関数がどこで呼び出されているかを確認します。
- 関数はオブジェクトを通じて呼び出されていますか(. の左側がオブジェクト)?もしそうなら、
this
はそのオブジェクトを指します。そうでなければ、次に進みます。 - 関数は
.call
、.apply
、または.bind
を通じて呼び出されていますか?もしそうなら、this
は指定されたコンテキストオブジェクトを指します。そうでなければ、次に進みます。 - 関数は
new
キーワードを使って呼び出されていますか?もしそうなら、this
は新しく作成されたオブジェクトを指します。そうでなければ、次に進みます。 - 関数はアロー関数ですか?もしそうなら、
this
はアロー関数の外側の最初の非アロー関数を指します。そうでなければ、次に進みます。 - 実行環境は厳密モードですか?もしそうなら、
this
はundefined
です。そうでなければ、次に進みます。 this
はwindow
オブジェクトを指します。
最後に、記事の冒頭の問題に戻り、実行結果を示します:
window.name = "window";
function User(name) {
this.name = name;
this.greet1 = function() {
console.log(this.name);
};
this.greet2 = function() {
return function() {
console.log(this.name);
};
};
this.greet3 = function() {
return () => console.log(this.name);
};
}
const UserA = new User("UserA");
const UserB = new User("UserB");
UserA.greet1(); // UserA
UserA.greet1.call(UserB); // UserB
UserA.greet2()(); // window
UserA.greet2.call(UserB)(); // window
UserA.greet3()(); // UserA
UserA.greet3.call(UserB)(); // UserB
UserA
と UserB
はそれぞれ new
で構築されているため、対応する name
はそれぞれ UserA
と UserB
です。
UserA.greet1()
: 最初にgreet1
がUserA
によって呼び出されるため、greet1
内のthis
はUserA
を指し、したがって出力はUserA
です。UserA.greet1.call(UserB)
:greet1
が.call
を通じて呼び出され、指定されたオブジェクトはUserB
なので、出力はUserB
です。UserA.greet2()()
:最初にgreet2
がUserA
によって呼び出され、バインドされたコンテキストオブジェクトのない関数を返すため、出力はwindow
です。User.greet2.call(UserB)()
:ここでgree2
が.call
を通じてUserB
によって呼び出されますが、同様にバインドされたコンテキストオブジェクトのない関数を返すため、出力は依然としてwindow
です。UserA.greet3()()
:ここで返されるのはレキシカルバインディングのアロー関数で、バインドされたコンテキストオブジェクトはUserA
なので、出力はUserA
です。UserA.gree3.call(UserB)()
:ここでもアロー関数が返され、バインドされたコンテキストオブジェクトは.call
で指定されたUserB
なので、出力はUserB
です。
参考リンク: