わからないんだな、これが。
呼び出されたタイミングで何が値として入るか決まるという JavaScript の不思議ちゃん this
。使う機会はそこそこあるのですが私は理解できないまま使っているので想定外の動作に対応できません。
いつまでも逃げ続けるわけにはいかないので渋々調べて & 検証してまとめていこうと思います。this
と仲良くなろう〜
この記事で検証すること
8つの使い方を試し、 this
の値がどのように出力されるかを確かめます。
- グローバルスコープ
- 関数内
- アロー関数内
- オブジェクトのメソッド内
- オブジェクトのプロトタイプチェーン
- クラス・派生クラス内
- 関数コンストラクター内
- DOMイベントハンドラー内
-
this
を固定するbind
apply
call
メソッド
グローバルスコープ
Window オブジェクトを参照します。
関数内
グローバルオブジェクト(ブラウザであれば Window オブジェクト)を参照します。
ただしStrict モードのときは undefined
となります。
アロー関数内
ES2015 では、自身では this の結び付けを行わないアロー関数が導入されました (これは包含する構文上のコンテキストの this の値を保持します)。
this - JavaScript | MDN
厄介なのがアロー関数です。
簡単に関数を作れて便利ですが、this
の働きが関数宣言された関数とは異なります(なぜそんな仕様になったのか背景は残念ながら知りません)。
アロー関数内の this
はアロー関数を包括しているものの this
となります。
例えばグローバルスコープで宣言していれば Window オブジェクトです。
const arrow = () => {
console.log(this); // この関数を包括しているのはグローバルスコープなので、グローバルスコープの this である Window オブジェクト
}
また例えばオブジェクトのメソッド内でアロー関数を使っていれば、オブジェクトのメソッドがオブジェクト自体を this
とするので、内部のアロー関数が持っている this
もオブジェクト自体になります。
const kinako = {
name: "kinako",
spices: "cat",
age: 2,
greet: function () {
console.log(this); // kinako オブジェクト
const inner = () => {
console.log(this); // kinako オブジェクト
}
inner();
return "meow";
},
}
//
オブジェクトのメソッドについては次の項でも解説します。
オブジェクトのメソッド内
オブジェクト自体になります。getter, setter も同様です。
const kinako = {
name: "kinako",
spices: "cat",
age: 2,
greet: function () {
console.log(this); // kinako オブジェクト
const inner = () => {
console.log(this); // kinako オブジェクト
}
inner();
return "meow";
},
get ageInHumanYear() { // 人間年齢を返す
console.log("getter:");
console.log(this); // kinako オブジェクト
if(this.age > 3) {
return 24 + (this.age - 2) * 4;
} else if(this.age === 2) {
return 24;
} else if(this.age === 1) {
return 18;
} else if(this.age === 0) {
return 0;
} else {
return "Age is not valid";
}
},
set currentAge(currentAge) {
console.log("setter:");
console.log(this); // kinako オブジェクト
this.age = currentAge;
return this.age;
},
}
オブジェクトのプロトタイプチェーン
インスタンス側から this
を使った場合、インスタンスのオブジェクトとなります。
メソッドが元のオブジェクトを参照している場合でも this
はインスタンスで指定されたものになるのでなかなか取り扱いに注意が必要です。
const kinako = { /* さっきと同じオブジェクトなので省略 */ }
const kinakoCrone = Object.create(kinako);
kinakoCrone.name = "kinako-2";
kinakoCrone.spices = "cat-clone";
kinakoCrone.age = 0;
console.log(kinakoCrone.greet());
// -> {name: 'kinako-2', spices: 'cat-clone', age: 0}
// -> meow
// kinako.greet を参照しているが、 `this` は kinakoCrone のもの
console.log(kinakoCrone.ageInHumanYear); // -> 0
console.log(kinakoCrone.currentAge = 1); // -> 1
console.log(kinakoCrone.ageInHumanYear); // -> 18
クラス・派生クラス内
クラス自体を this
とします。
extend で生成された派生クラスの場合、this
は派生クラスとなります。
//// -- ベースとなるクラスの作成 -- ////
class CoffeeMaker {
constructor() {
this.size = "Venti©";
this.quantity = 0;
}
brew () {
this.quantity++
return "brewing...";
}
serve () {
if(!this.quantity) {
return "There is no coffee"
}
this.quantity--
return "Here you are!";
}
}
const myCoffeeMaker = new CoffeeMaker();
/*
myCoffeeMaker の動作チェック
console.log(myCoffeeMaker.quantity);
console.log(myCoffeeMaker.serve());
console.log(myCoffeeMaker.brew());
console.log(myCoffeeMaker.quantity);
*/
//// -- 他のクラスを作成してメソッドを渡してみる -- ////
class CoffeePot {
constructor() {
this.quantity = 10;
}
get size() {
return "2L";
}
}
const myCoffeePot = new CoffeePot();
myCoffeePot.serve = myCoffeeMaker.serve;
console.log(myCoffeePot.serve());
// -> "Here you are!"
// pot の方を参照しているので serve された
//// -- 派生クラス -- ////
class CaféAuLaitMaker extends CoffeeMaker {
constructor() {
super();
this.milk = "hotted";
}
}
const caféAuLait = new CaféAuLaitMaker();
console.log(myCoffeeMaker.brew()); // 普通のコーヒーを入れる
console.log(myCoffeeMaker.brew()); // 普通のコーヒーを入れる
console.log(caféAuLait.serve());
// -> "There is no coffee"
// this は継承元の CoffeePot ではなく CaféAuLaitMaker となっている
ちょっと長いコードですが、手元で実行してみてください。
関数コンストラクター内
コンストラクターで定義されているオブジェクトがそのまま this
となります。ただしオブジェクトが返されている場合、返り値のオブジェクトが this
となります。返り値がオブジェクトでない場合は単純に返り値となり this
にはなりません。
function Coffee() {
this.size = "Venti©";
this.milk = null;
}
const coffee = new Coffee();
console.log(coffee.size); // "Venti©",
function CaféAuLait() {
this.size = "Tall"
this.milk = "heated"
return {
size: "Venti©",
milk: "heated almond milk",
}
}
const caféAuLait = new CaféAuLait();
console.log(caféAuLait.size); // "Venti©",
console.log(caféAuLait.milk); // "heated almond milk",
function Cappuccino() {
this.size = "Tall"
this.milk = ["formed", "steamed"]
return "tasty!";
}
const cappuccino = new Cappuccino();
console.log(cappuccino.size); // "Tall
console.log(cappuccino.milk); // ["formed", "steamed"]
DOMイベントハンドラー内
Event.currentTarget
、つまり現在イベントで処理されている対象を指します。
// リスナーとして呼び出された場合は、関連づけられた要素を青にする
function bluify(e) {
// 常に true
console.log("this === e.currentTarget:", this === e.currentTarget);
// currentTarget と target が同じオブジェクトであれば true
console.log("this === e.target:", this === e.target);
this.style.backgroundColor = '#A5D9F3';
}
// 文書内の各要素の一覧を取得
var elements = document.getElementsByTagName('*');
// クリックリスナーとして bluify を追加することで、
// 要素をクリックすると青くなるようになる
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', bluify, false);
}
// 引用: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this#%E6%B4%BE%E7%94%9F%E3%82%AF%E3%83%A9%E3%82%B9
this
を固定する bind
apply
call
メソッド
呼び出した場所によって内容が変わってしまう this
ですが、固定することも可能です。使えるメソッドは3つあります。
-
bind
: 宣言時にthis
を固定 -
apply
: 呼び出し時に配列を引数として指定してthis
を固定 -
call
: 呼び出し時に文字列を引数として指定してthis
を固定
今回は bind
を試してみます。
上で使った class 「CoffeeMaker」を少しアレンジして、コーヒーメーカーを洗う「wash」というメソッドを追加してみましょう。
class CoffeeMaker {
constructor() {
this.size = "Venti©";
this.quantity = 0;
this.wash = this.wash.bind(this); // wash の this を bind で固定
}
brew () {
this.quantity++
return "brewing...";
}
serve () {
if(!this.quantity) {
return "There is no coffee"
}
this.quantity--
return "here you are!";
}
wash() {
if(this.quantity) {
return "First, you'll need to transfer the coffee to a pot."
}
return "washing..."
}
}
bind
メソッドを使って宣言時に this
を固定してみました。
以下のような動作になるはずです。
const myCoffeeMaker = new CoffeeMaker();
class CoffeePot {
constructor() {
this.quantity = 10;
}
get size() {
return "2L";
}
}
const myCoffeePot = new CoffeePot();
myCoffeePot.serve = myCoffeeMaker.serve;
myCoffeePot.wash = myCoffeeMaker.wash;
console.log(myCoffeePot.serve()); // -> "here you are!"
// マシンにはコーヒーがないが、pot の方を参照しているので1杯 serve された
console.log(myCoffeePot.wash()); // -> "washing..."
// pot には9杯分のコーヒーが残っているが、this は maker の方を見ているので「マシンにコーヒーは残っていない」と認識され、wash された
まとめ
- グローバルスコープ: Window オブジェクト
- 関数内: Global オブジェクト、Strict モードなら undefined
- アロー関数内: アロー関数を包括しているものの
this
- オブジェクトのメソッド内: オブジェクト自体
- オブジェクトのプロトタイプチェーン: インスタンス
- クラス・派生クラス内: クラス自体、派生クラスの場合は派生クラス自体
- 関数コンストラクター内: 定義されている通り、または返り値がオブジェクトであれば返り値
- DOMイベントハンドラー内:
Event.currentTarget
-
this
を固定するにはbind
apply
call
メソッドを使う
this
という名前だけあって自身を指していることが多かったですね。ちょっとだけ仲良くなれた気がします。