#はじめに
今回はJavaScriptのthisの挙動について改めてまとめていきたいと思います。
このthisの挙動については多くのエンジニアが頭を悩ませていると思います。
何を隠そう、私もその一人です。
多くの時間を捧げたおかげで、ある程度thisの挙動について理解できた気がするので記事にまとめていきたいと思います。
「完全に理解した」と書いてありますが、自分の勘違いである可能性も十分にあリます。コメント欄で優しく指摘して頂ければ幸いです。
thisの挙動を頑張って理解していきましょう。頑張りましょう。
#thisとは
JavaScriptにおけるthisを一言で説明すると、関数の実行コンテキストのオブジェクト自身への参照が格納された暗黙の引数
であるということができます。
以下の記事が参考になりました。
JavaScript の this を理解する多分一番分かりやすい説明
PythonやRubyなどの一般の言語においては、メソッドの実行においてその実行コンテキストのオブジェクトがthis
やself
として渡されています。
##Pythonの場合
Pythonの場合、実行コンテキストのオブジェクトへの参照をself
という引数で明示的に受け取ります。
class UnReact:
def __init__(self, name):
self.name = name
def some_function(self):
print(self.name)
organization = UnReact('unreact')
print(organization.name)
Pythonではクラスからインスタンスを作成した場合、そのインスタンス自身への参照をクラス内のメソッドの第一引数に受け取ります。
変数名は何でも構いませんが、一般的にはself
という変数名で受け取ります。
organization.name
という形でインスタンスの属性へアクセスした際にunreact
という文字列にアクセスできるということは、コンストラクタ関数内の処理self.name = name
のselfがorganaization自身
を表しているということを示しています。
このように、Pythonではメソッドの実行においてその実行コンテキストのオブジェクトをself
という形で明示的に受け取ります。
実行コンテキストのオブジェクトがメソッドの第一引数に格納されるという性質を確かめるため、以下のコードを実行してください。
class UnReact:
def __init__(self, name):
self.name = name
def some_function(this):
print(this.name)
organization = UnReact('unreact')
some_function(organization) # unreact
UnReact.some_function = some_function
organization.some_function() # unreact
unreact
unreact
some_function(organization)
の例では、インスタンスであるorganization
がそのままthisとして使用されています。
また、UnReact.some_function = some_function
の後にorganization.some_function()
をした例では、引数にインスタンスを設定していないにも関わらずインスンタンスであるorganaization
がsome_function
の第一引数this
に渡されていることが確認されます。
つまり、Pythonのメソッドの実行においては実行コンテキストのオブジェクト、今回の例では.
の前のorganization自身
がthisとして渡されていることが確認できます。
Pythonはその設計哲学に「Explicit is better than implicit」
を掲げているため、他の言語(RubyやJava)では暗黙の引数として渡されるthis
をクラスのメソッドの第一引数として明示的に記述することが求められます。
JavaScriptでは、このthisが暗黙の内に渡されているだけです。
このことを頭に入れて、実際の具体例に進んでいきましょう。
#thisのパターン4種類
それでは具体的にthisの4つのパターンについて見ていきましょう。
##[パターン1] new演算子をつけて呼び出したとき
JavaScriptにおいてクラスの生成に用いられるnew演算子ですが、このnew演算子を用いたときは、new演算子により新規生成されるオブジェクトがthisとして渡されることになります。
というかJavaScriptにおいて、厳密な意味でのクラス定義は存在せず、ただのオブジェクトを生成するシンタックスシュガーです。
とりあえずその話は気にせずに、new演算子が使用されたときのthisの挙動について実際のコードを用いて解説していきます。
const unreact = function () {
console.log("Hello UnReact, this is", this);
};
const obj = new unreact();
Hello UnReact, this is unreact {}
クラス定義に使用されるnew演算子ですが、実は任意の関数に実行することができます。
new演算子の挙動ですが、これは非常に難解なので以下の記事に目を通すことをおすすめします。
さすがにそろそろ JavaScript の new (あと継承も)について理解したいと思っているあなたに。
new演算子の挙動をざっくり説明すると、先にnew unreact
が実行されて、new演算子によりunreact
のプロトタイプオブジェクト、この場合はfunction
のプロトタイプのため空のオブジェクト{}
がコピーされて新規にオブジェクトが作られます。
その後にnew unreact()
の()
の部分が実行されて、空のオブジェクト{}
を実行コンテキストにしてfunctionの中身が実行されることになります。
今回は関数宣言をunreact
に代入しているので、thisにはunreact {}
が格納されており、new unreact()
の()
により実行されるために結果が出力されています。
つまり、new演算子をつけて呼び出したときには、this
にはその新規生成されるオブジェクトがかくのう されることになります。
もう少し分かりやすく説明するために、以下のようにコードを書き換えました。
const unreact = function (name) {
console.log("Hello UnReact, this is", this); //Hello UnReact, this is unreact {}
this.name = name;
console.log(this.name); //UnReact
console.log(this); //unreact { name: 'UnReact' }
};
const obj = new unreact("UnReact");
functionの中のコードはnew unreact("UnReact")
の("UnReact")
が実行されて段階で実行されます。
new unreact
の部分でfunctionのプロトタイプオブジェクトである{}:空のオブジェクト
が新規作成されて渡されることになります。
その空のオブジェクトを実行コンテキストにして、functionの中身が実行されます。つまり、this.name = name
の部分は{}.name = 'UnReact'
というコードが実行されることになるため、オブジェクトの中身が{ name: 'UnReact' }
になります。
ここまでで、new演算子を用いた際のthisの挙動についての解説は終わりです。
##[パターン2] メソッドとして実行したとき
これは最もシンプルなパターンですが、JavaScriptにおいてthisがメソッド内で使用された場合は、そのthisはメソッドが所属するオブジェクト
になります。
そもそもJavaScriptにおいてメソッドとはオブジェクトのプロパティに存在する関数のことであり、それ以上でもそれ以下でもありません。
そのメソッド内でthisを使用した際に、そのthisにメソッドが所属するオブジェクト自体が格納されるというのは、他の言語と共通する当然の挙動であるといえるでしょう。
const unreact = {
name: "UnReact",
greet: function () {
console.log("Hello", this);
},
};
unreact.greet();
unreact.greet
の部分で、オブジェクトのプロパティであるfunctionにアクセスして、()
を用いることでその関数を実行しています。
メソッド内におけるthisはそのメソッドが所属するオブジェクト、つまりはunreactオブジェクト自体を表すため、上記のような挙動になります。
Hello { name: 'UnReact', greet: [Function: greet] }
##[パターン3] 1,2以外の関数
thisの4つもパターンと書いたんですが、このパターン3に至っては2つに場合分けされます。「いやもうそれ4つじゃないやん!」と言いたくなる気持ちも分かりますが、ぐっとこらえて読んで下さい。
というのもES5から追加されたStrictモード
により、グローバル環境へのthisへのアクセスの挙動が分岐するからです。
Strictモード
はJavaScriptの古来から続くヤバい挙動を防ぐためにES5から追加されました。そのStrictモード
によりグローバル変数this
へのアクセスが禁止されてしまったのです。
冷静に考えて、グローバル変数thisへ簡単にアクセスできてしまう初期の実装が悪いのですけれども、このStrictモード
により余計にthisが分かりにくくなってしまったのは事実だと思います。
ここでは一旦Strictモードが無い場合と有る場合に分けてthisの挙動を確認していきましょう。
###1,2以外の関数のthisの挙動 [非Strictモード]
それでは最初に非Strictモードにおける挙動をみていきましょう。
こちらの方はJavaScriptの基本設計に沿った挙動をするため、今までの話の延長線上にあります。
というかこの基本設計がヤバかったため、Strictモードが導入されて禁止されることになったんですが。
メソッド内のthisや、new演算子を用いた際のthis以外の関数内で使われるthisは実行コンテキストがグローバルオブジェクトになります。これは処理系によっても異なっていて、Node.jsならglobal
オブジェクトであり、ブラウザであればWindow
オブジェクトになります。
thisは実行環境のオブジェクトである
という基本原則が守られており、分かりやすいですよね。
以下のコードで具体例をみていきましょう。
const unreact = function (name) {
this.name = name;
};
unreact("UnReact");
console.log(name);
UnReact
この仕様は設計方針的には正しいですが、こんなに簡単に汚染が起きてしまうのは良くないですよね。
ちなみに以下のようにすればグローバル環境の汚染が起きてしまったことが確認できます。
const unreact = function (name) {
this.name = name;
};
unreact("UnReact");
console.log(name);
console.log(global);
...
setTimeout: [Function],
console: [Getter],
name: 'UnReact' }
このように、メソッドやnew演算子が無い関数の中で呼び出されるthisはグローバルオブジェクトになります。
###1,2以外の関数のthisの挙動 [Strictモード]
それでは次にStrictモードにおける挙動を見てみましょう。
Strictモードとは、ES5から追加されたJavaScriptの危険な挙動を回避するためのものでしたね。
Strictモードにおいて、関数定義の中で呼ばれるthisはグローバルオブジェクトではなくundefinedになります。
const unreact = function () {
console.log(this)
};
unreact();
undefined
この挙動は安全ではありますが、thisは実行コンテキストのオブジェクトである
という基本原則から外れてしまっていますね。
この辺りはStrictモードによる例外としてしっかりと抑えておきましょう。
##apply, callによる呼び出し
以下の記事を参考にしました。
callやapplyはthisを呼び出し側から任意のオブジェクトに指定して関数を実行する方法です。
第一引数に渡したオブジェクトをthis
に強制的に束縛して、callやapplyを実行した関数を実行します。
言葉で説明しても何がなんだか分からないと思うので、実際にコードで解説していきます。
const unreact = {
name: "UnReact",
say: function () {
console.log(this.name);
},
};
const react = {
name: "React",
};
unreact.say.call(react); //React
unreact.say.apply(react); //React
React
React
上記の例では、unreactオブジェクトのsayメソッドを、reactオブジェクトを主体として呼び出しています。
callメソッドとapplyメソッドは、第一引数に実行の主体となるオブジェクトを渡し、第2引数にメソッドの引数を渡します。
この例ではunreact.say
メソッドを実行する際に、主体をreactオブジェクトに切り替えて実行しています。
unreactオブジェクトのsayメソッドの中で用いられるthisは、実行コンテキストがreactオブジェクトに切り替えられているので、この場合はreactオブジェクト自身が格納されることになります。
もう少し分かりやすく説明するために、引数にオブジェクトを直接渡してみましょう。
const unreact = {
say: function (organization) {
this.organization = organization;
console.log(this);
},
};
unreact.say.call({ name: "React" }, "UnReact");
unreact.say.apply({ name: "React" }, ["UnReact"]);
{ name: 'React', organization: 'UnReact' }
{ name: 'React', organization: 'UnReact' }
上記の例では、{ name: "React" }
を実行コンテキストとして、unreactオブジェクトのsayメソッドを呼び出しています。
callやapplyの第2引数は、sayメソッドの引数organaization
に代入されます。それによりthis.organization = organization;
の部分が{ name: "React" }.organaization = "UnReact"
になるため、上記のような結果になります。
callとapplyの違いは、第2引数のとり方です。applyは第2引数に配列をとり、その配列の中身が引数として渡されますが、callは第2引数以降がそのまま引数として渡されます。
#thisの問題点を回避しよう
ここまでで、4つのパターンのthisの挙動が理解できたと思います。
それでは次に、このthisの挙動が引き起こす問題とその回避方法についてまとめていきます。
よくあるthisの挙動により起こる問題は、クラスのメソッドの中で新たに関数を定義した際のthisの挙動でしょうか。
##クラスのメソッドで関数を定義した場合
結論から書きますと、クラスのメソッドで関数を定義して、その中でthisを使おうとした場合、そのthisはグローバルオブジェクトになります。
Strictモードにおいてはグローバルオブジェクトのthisへのアクセスは禁止されているので、undefinedが返ってくることになります。
下記のコードです。
class Organization {
constructor(name) {
this.name = name;
}
say() {
const hello = function () {
console.log(`Hello ${this.name}`);
};
hello();
}
}
const Company = new Organization("UnReact");
Company.say();
TypeError: Cannot read property 'name' of undefined
上記の例において、sayメソッド内で新たに定義したhelloという関数はただの関数であり、オブジェクトの実行コンテキストにありません。そのため、非Strictモードではthisはグローバルオブジェクトになるのですが、今回はStrictモードで実行しているためthisにundefinedが格納されています。
そのundefinedに対してプロパティアクセスを実行しているため、TypeErrorが発生しています。
この問題を回避するためにいくつかの方法が考えられます。
- callやapplyを使ってthisを強制的に指定する
- bindを使って関数にthisを束縛する
- thisの値を一時変数に避難させる
- アロー関数式を使用する
各々の方法について解説していきます。
###callやapplyを使用してthisを強制的に指定する
callやapplyを使ってthisを指定してみましょう。
以下のコードです。
class Organization {
constructor(name) {
this.name = name;
}
say() {
const hello = function () {
console.log(`Hello ${this.name}`);
};
hello.call(this);
}
}
const Company = new Organization("UnReact");
Company.say();
hello()
で実行していた部分をhello.call(this)
とすることで、thisを主体としてhello関数を実行しています。
この場合のthisはメソッドの中のthisなので、実行コンテキストがオブジェクト自身、この場合はCompany
インスタンス自身になります。
applyを使用しても同様の挙動になります。
###bindを使って関数にthisを束縛する
それでは次にbindを使って関数にthisを束縛する方法をみていきましょう。
class Organization {
constructor(name) {
this.name = name;
}
say() {
const hello = function () {
console.log(`Hello ${this.name}`);
};
const bindHello = hello.bind(this);
bindHello();
}
}
const Company = new Organization("UnReact");
Company.say();
Hello UnReact
const bindHello = hello.bind(this)
を使用することでthisをhello関数に束縛しています。このthisはメソッドの中のthisであるため、実行コンテキストがオブジェクト自身、つまりCompany
インスタンスになっています。
そのため、Company.say()
を実行するとthisがCompany自身
となり、this.nameにUnReactが格納されています。
###thisの値を一時変数に避難させる
次はthisの値を一時変数に避難させる方法です。
メソッド内でのthisは実行コンテキストがオブジェクトになっているため、このthisを一時変数_this
に避難させて、この_thisをメソッド内で定義した関数内で使用します。
class Organization {
constructor(name) {
this.name = name;
}
say() {
const _this = this;
const hello = function () {
console.log(`Hello ${_this.name}`);
};
hello();
}
}
const Company = new Organization("UnReact");
Company.say();
Hello UnReact
const _this = this
の部分で、実行コンテキストがオブジェクト自身であるthisを一時変数_thisに格納しています。
この_thisをhello関数の中で使用することで、thisの挙動を制御しています。
###アロー関数式を使用する
最後はアロー関数式です。
メソッド内で定義する関数のみをアロー関数式で定義する場合と、メソッド自身もアロー関数式で定義する場合があります。
この2つを場合分けして解説します。
####メソッド内で定義する関数のみをアロー関数式で定義する
アロー関数式で関数を定義すると、functionで関数を宣言した場合とは異なる挙動をします。
以下のコードです。
class Organization {
constructor(name) {
this.name = name;
}
say() {
const hello = () => {
console.log(`Hello ${this.name}`);
};
hello();
}
}
const Company = new Organization("UnReact");
Company.say();
Hello UnReact
上記のコードでは、hello関数をアロー関数式で定義しています。
アロー関数式はES2015から追加された書き方です。そのため、一般的なオブジェクト指向言語、PythonやRubyなどに慣れ親しんだ開発者向けに配慮されています。
具体的には、アロー関数式を使用した場合は暗黙の引数としてのthisを持ちません。
つまり、上記の例ではhello関数は暗黙の引数thisを持ちません。その代わり、外のスコープのthis、この場合はメソッド内のthisが使用されることになります。
そのため、上記のようにhello関数の中のthisはCompany自体となっています。
基本的には、このアロー関数式を使った方法でthisの問題を回避することになります。
####メソッド自身もアロー関数式で定義する場合
最後はメソッド自身もアロー関数式で定義する場合です。
以下のコードになります。
class Organization {
constructor(name) {
this.name = name;
}
say = () => {
const hello = () => {
console.log(`Hello ${this.name}`);
};
hello();
};
}
const Company = new Organization("UnReact");
Company.say();
Hello UnReact
メソッド自身もアロー関数式で定義したパターンです。
本来なら、say関数内のthisは実行コンテキストがグローバルになるので、Strictモードでundefinedになるはずです。
しかし、クラスのインスタンスのメソッドとしてアロー関数式が定義された場合には、例外的にthisの一時変数による移し替えが発生します。
それにより、メソッド自身もアロー関数式で定義した場合も、メソッドをfunctionで定義した場合と同様の挙動をします。
#終わりに
今回の記事はここまでになります。
ここまで読んで頂きありがとうございました。