4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ひとりJavaScriptAdvent Calendar 2022

Day 11

JavaScript の this がいつまでたってもわからない

Last updated at Posted at 2022-12-11

わからないんだな、これが。

呼び出されたタイミングで何が値として入るか決まるという JavaScript の不思議ちゃん this。使う機会はそこそこあるのですが私は理解できないまま使っているので想定外の動作に対応できません。

いつまでも逃げ続けるわけにはいかないので渋々調べて & 検証してまとめていこうと思います。this と仲良くなろう〜

この記事で検証すること

8つの使い方を試し、 this の値がどのように出力されるかを確かめます。

  1. グローバルスコープ
  2. 関数内
  3. アロー関数内
  4. オブジェクトのメソッド内
  5. オブジェクトのプロトタイプチェーン
  6. クラス・派生クラス内
  7. 関数コンストラクター内
  8. DOMイベントハンドラー内
  9. 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つあります。

  1. bind : 宣言時に this を固定
  2. apply : 呼び出し時に配列を引数として指定して this を固定
  3. 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 された

まとめ

  1. グローバルスコープ: Window オブジェクト
  2. 関数内: Global オブジェクト、Strict モードなら undefined
  3. アロー関数内: アロー関数を包括しているものの this
  4. オブジェクトのメソッド内: オブジェクト自体
  5. オブジェクトのプロトタイプチェーン: インスタンス
  6. クラス・派生クラス内: クラス自体、派生クラスの場合は派生クラス自体
  7. 関数コンストラクター内: 定義されている通り、または返り値がオブジェクトであれば返り値
  8. DOMイベントハンドラー内: Event.currentTarget
  9. this を固定するには bind apply call メソッドを使う

this という名前だけあって自身を指していることが多かったですね。ちょっとだけ仲良くなれた気がします。

参考資料

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?