クモのようにコツコツと

フロントエンドエンジニア イイダリョウのブログ。略称「クモコツ」

【JS】アロー関数のthisの挙動(プロトタイプとクラスの違い)

アロー関数の続きです。前回はアロー関数の書式の違いについてでした。今回は従来の関数とアロー関数の挙動の違いについて。特に影響が大きそうなのはthisの挙動。この違いを認識しないまま単純に従来の関数をアロー関数に書き換えていくと動かない事態もあり得ます(thisをディスるつもりはないけども…)。それでは行きましょう!

【目次】

※参考:前回記事
【JS】アロー関数の複数の書き方を試してみた - クモのようにコツコツと

※参考:ネイティブJSでいろいろやってみたシリーズ qiita.com

従来のthisは呼び出し元が基準

従来の関数でのthisはこのような特徴を持っていた。

thisの4種類のパターン

1:メソッド呼び出しパターン
2:関数呼び出しパターン
3:コンストラクタ呼び出しパターン
4:apply,call呼び出しパターン
ここで重要なのは「呼び出し元」をみることです。
なぜなら「呼び出し元」に「this」は左右されるからです。

※参考:JavaScriptの「this」は「4種類」?? - Qiita

thisを設定されている場所(関数定義)よりも、thisを呼び出している場所(関数実行)から見た位置関係が重視されて、呼び出した場所によってthisの対象が変わる。

アロー関数のthisは定義した場所が基準

それがアロー関数ではthisの挙動が変わるらしい。「アロー関数 this」で調べるとたくさん情報が出てくる。

MDNの記事は詳しいがちょっと説明が分かりにくく感じた。

2 つの理由から、アロー関数が導入されました。1 つ目の理由は関数を短く書きたいということで、2 つ目の理由は this を束縛したくない、ということです。

「束縛したくない」?これまでは束縛していた?そもそも「thisの挙動が変わる」というよりも意図的に「変えた」んだな。変わるんじゃない、変えたんだ!

this を束縛しない

アロー関数が登場するまでは、関数ごとに自身の this の値を定義していました(コンストラクタでは新しいオブジェクト、strict モード の関数呼び出しでは undefined、「オブジェクトのメソッド」として呼び出された関数ではそのときのオブジェクト、など)。これは、オブジェクト指向プログラミングをする上で煩わしいということが分かりました。

うーんと…、これまでは関数の呼び出し元によってthisの対象が変わる、それがすなわちthisを束縛した状態ということだろうか。thisを引っ張り回してるようなイメージかな。買い物に付き合わせている、みたいなw

アロー関数自身は this を持ちません。レキシカルスコープの this 値を使います。つまり、アロー関数内の this 値は通常の変数検索ルールに従います(スコープに this 値がない場合、その一つ外側のスコープで this 値を探します)。

そのため、次のコードで setInterval に渡される関数の this の値は、外部関数の this と同じ値になります:

アロー関数の場合は通常の変数のルールにと同じ。スコープにthisの値がない場合、一つ外側のスコープでthisの値を探す、と。。

※参考:アロー関数 - JavaScript | MDN

レキシカルスコープとは

ところで「レキシカルスコープ」ってなんだろう?歴史狩る?まわわからない単語が出てきたぞ。

調べたらこちらの記事が分かりやすい。

JavaScriptのスコープにはグローバルなものとローカルなものがあるのだと学びました。

しかし、細かいことを気にして行くと、ローカル変数の有効範囲は関数の定義時に決まるものなのか、他の場所で呼び出した時にも更新されるものなのか、という疑問が発生しませんか?(私はしませんでした。笑)

これについては、実は各プログラミングごとに決まっていて、それぞれ「 レキシカルスコープ(静的スコープ)」と「 ダイナミックスコープ(動的スコープ)」と呼ばれています。

※参考:【JavaScriptの基礎】レキシカルスコープとクロージャを理解する | WEMO

ふむふむ。JS、Ruby、Java、pythonなどはレキシカル(静的)、Perlなどはダイナミック(動的)とのこと。

もう少し読み進めると…

レキシカルスコープは 関数を定義した時点でスコープが決まります。

ダイナミックスコープは、 関数を実行した時点でスコープが決まります。

※参考:【JavaScriptの基礎】レキシカルスコープとクロージャを理解する | WEMO

は!先ほどのthisの束縛の話!通常の変数は「レキシカル(静的)」だけどthisは「ダイナミック(動的)」だ!

これを無くしたい、という意図でアロー関数は作られたわけだ。

プロトタイプとアロー関数

従来の関数で書いたプロトタイプ

thisは以前書いた「newとプロトタイプとクラス」の記事でも多用している。

※参考:【JS】newとプロトタイプとクラス - クモのようにコツコツと

こんなやつ

See the Pen JS's new 05 by イイダリョウ (@i_ryo) on CodePen.

ボタンを押すとアラートが3つ立ち上がる。「○○は○○」という形式。

JSコード

//DOM取得
var btn = document.getElementById('btn');

btn.addEventListener('click', function(e){

//コンストラクタ関数
function Ms (name, spec) {
  this.name = name;
  this.spec = spec;
}
//プロトタイプ拡張 
Ms.prototype.say = function() {
    alert(this.name + "は" + this.spec);
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say(); 
gufu.say();

}, false);
  • 変数btnにボタンのDOMを取得
  • btnにクリックイベントを設定
  • コンストラクタ関数Msで引数namespecを設定
  • プロトタイプ拡張say()で「namespecです」というアラートを設定
  • 変数zakushargufuでコンストラクタMSからインスタンスを作り、引数も設定
  • zakushargufusay()メソッドを実行

zakuなどのインスタンスを実行するとき、引数の「ザク」「通常の一倍」などがthisに読み込まれている。

プロトタイプをアロー関数にする

これをアロー関数に書き直すとどうなるか。こちら。

ボタンを押してみると…

See the Pen JS's new 05_arrow1 by イイダリョウ (@i_ryo) on CodePen.

…シーーン。返事がない。ただのしかばねのようだ。

JSコード

var btn = document.getElementById('btn');
btn.addEventListener('click', (e) => {

//コンストラクタ関数
var Ms = (name, spec) => {
  this.name = name;
  this.spec = spec;
}
//プロトタイプ拡張 
Ms.prototype.say = () => {
    alert(this.name + "は" + this.spec);
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say(); 
gufu.say();

}, false);

先程のコードのfunctionの部分を全てアロー=>にしている。

ボタンを押すとデベロッパーツールではこんなエラーになる。

TypeError: undefined is not an object (evaluating 'Ms.prototype.say = () => {
    alert(this.name + "は" + this.spec);
}')

タイプエラー:undefinedはオブジェクトではありません( 'Ms.prototype.say =()=> { alert(this.name + "は" + this.spec); } ')

Ms.prototype.sayはオブジェクトではありませんよと。

こちらの記事で同じ事象が説明されていた。

※参考:【JavaScript】アロー関数式を学ぶついでにthisも復習する話 - Qiita

アロー関数はthisにすると呼び出し時ではなく宣言時に確定する。ただし、prototypeは呼び出し時にthisを確定させたい目的なのでアロー関数を使うと怒られる。

prototypeで怒られない書き方(ただし目的は未遂)

色々試した結果、この書き方なら怒られなかった。

See the Pen JS's new 05_arrow2 by イイダリョウ (@i_ryo) on CodePen.

ボタンを押してもエラーにならない。ただしアラートのテキストは全て「CodePen Preview for JS's new 05_arrow2はundefined」となる。zaku.say()などの実行時に「ザク」、「通常の一倍」などの引数を読んでくれていない。

どう言うコードか。

var btn = document.getElementById('btn');

btn.addEventListener('click', (e) => {
  
//コンストラクタ関数
var Ms = function (name, spec) {
  this.name = name;
  this.spec = spec;
}
//プロトタイプ拡張 
Ms.prototype.say = () => {
    alert(this.name + "は" + this.spec);
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say(); 
gufu.say();

}, false);
  • 変数Msのみfunctionにする。

一つ一つのアローを=>からfunctionに戻してみたが他は=>のままでも問題ない。コンストラクタ関数を定義しているここだけはどうしてもエラーになる。(先程の記事の例でもここはfunctionにしている)

※参考:【JavaScript】アロー関数式を学ぶついでにthisも復習する話 - Qiita

MDNのアロー関数のページにも言及されている。

アロー関数式は、メソッドでない関数に最適で、コンストラクタとして使うことはできません。

※参考:アロー関数 - JavaScript | MDN

メソッドは宣言時ではなく呼び出し時にthisを確定させる(引数をthisにする)のでアロー関数に向かない。

では、アラートのテキストが「CodePen Preview for JS's new 05_arrow2はundefined」になったのは何を読んでいるのか。

アロー関数の中に記述した this は、実行時には window になります。より正確に言うと、この記述部分が実行されたときのコンテキスト(オブジェクト)が this に束縛されます。たいていは windowsオブジェクトだろうということです。

※参考:【JavaScript】無名関数とアロー関数とイベントリスナーのthis | ラボラジアン

thisが定義されていない場合はwindowオブジェクトを意味するようだ。一つ目はwindowオブジェクトのnameプロパティ。二つ目はwindowオブジェクトのspecプロパティで、そんなプロパティはありませんよ(undefined)ということか。なるほど。

thisの対象をグローバル変数で設定してみる

試しにthisの対象(namespec)をグローバルな場所で設定してみる。

See the Pen JS's new 05_arrow3 by イイダリョウ (@i_ryo) on CodePen.

ボタンを押すと今度は「富野御大は監督」を連呼するw

JSコード

var name = "富野御大";
var spec = "監督";  

var btn = document.getElementById('btn');

btn.addEventListener('click', (e) => {
  
//コンストラクタ関数
var Ms = function (name, spec) {
  this.name = name;
  this.spec = spec;
}
//プロトタイプ拡張 
Ms.prototype.say = () => {
    alert(this.name + "は" + this.spec);
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say(); 
gufu.say();

}, false);
  • グローバル変数nameに「富野御大」
  • グローバル変数specに「監督」

Ms()メソッドの中でどんな引数(「ザク」「通常の一倍」など)を設定しようが、thisは一番外側で設定している「富野御大」「監督」だけを読み込み続ける。一途なやつだ。

クラスとアロー関数

クラスにはfunctionがないっ!

次にクラスのアロー関数化を検証する。

以前の「newとプロトタイプとクラス」の記事ではプロトタイプからクラスに書き換えを行なっている。

※参考:【JS】newとプロトタイプとクラス - クモのようにコツコツと

See the Pen JS's new 08 by イイダリョウ (@i_ryo) on CodePen.

ボタンを押した挙動は先程のプロトタイプと一緒。

JSコード

var btn = document.getElementById('btn');
btn.addEventListener('click', function(e){

class Ms {
  //コンストラクタ
   constructor(name, spec) {
    this.name = name;
    this.spec = spec;
  }
  //メソッド 
  say() {
    alert(this.name + "は" + this.spec);
  }
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say();
gufu.say();

}, false);
  • 全体をclassで包む。クラス名はMS
  • プロトタイプの時のコンストラクタ関数だったところはconstructor()メソッドになる。
  • その下のプロトタイプ拡張Ms.prototype.sayだったところはsay()メソッドになる。

なんと、先程エラーになったコンストラクタ関数部分がconstructor()メソッドになっててfunctionがない!

functionをアロー関数に書き換え

念のため先ほどの事例のfunctionをアロー関数に書き換えてみる。

See the Pen JS's new 08_arrow by イイダリョウ (@i_ryo) on CodePen.

おお!アラートのテキストが変わらない!「ザクは通常の一倍」とか引数を読んでくれてる!

JSコード

var btn = document.getElementById('btn');
btn.addEventListener('click', (e) => {

class Ms {
  //コンストラクタ
   constructor(name, spec) {
    this.name = name;
    this.spec = spec;
  }
  //メソッド 
  say() {
    alert(this.name + "は" + this.spec);
  }
}

var zaku = new Ms("ザク", "通常の一倍");
 
var shar = new Ms("シャアザク", "通常の三倍");
  
var gufu = new Ms("グフ", "ザクとは違う");

//下に移動
zaku.say();
shar.say();
gufu.say();

}, false);
  • functionからアロー関数=>に書き換えた箇所はイベントリスナの一か所のみ

書き換えた部分はクリックイベントだから、クラス構文の外側にある。

クラス構文は全てメソッドのみで設定ができてfunctionやアロー関数=>を使わない。thisを読み込み時に固定したい場合はクラス構文で書いた方が良さそうだ!

また、調べるてみるとやはりクラス構文の中ではメソッドしか使えないようだ。

JavaScriptのclass構文では、メソッドしか作れないのです(MDN)。アロー関数に限らず、構文内で直接プロパティを定義することができません。

※参考:JavaScript - プロトタイプにアロー関数は使えないのでしょうか|teratail

MDNのクラスのページにもconstructor()は「特別なメソッド」とある。

コンストラクター は、class によって生成されるオブジェクトの生成・初期化を行う特別なメソッドです。"constructor" という名前のメソッドは、クラスに 1つしか定義できません。2 回以上定義されている場合は、SyntaxError がスローされます。

※参考:クラス - JavaScript | MDN

アロー関数とクラスはどちらもES5で追加された機能なので、下記のような役割の明確化が意図されたのかもしれない。

  • thisを宣言時にグローバルに固定させたい時はアロー関数
  • thisを 読み込み時に変更したい場合はクラス構文

最後に

ということで、今回はアロー関数の挙動についてもう少し掘り下げてみました。

  • 単純にfunctionをアロー=>に書き換えればいいというわけではない
  • アロー関数のthisは読み込み時ではなく宣言時に固定されるので注意
  • thisを読み込み時に変更したい場合はクラス構文のconstructor()メソッドで定義する

クラスも単にプロトタイプを書き換えるだけなのか(どちらで書くかはご自由に)と思ってたけど、thisの挙動に違いがあるんですな。勉強になりました。

ES6〜のJSの書き方は「カイゼン」を目的とした変更がありそうなのでこれからも積極的に使ってい期待です。そして、Babelなどでコンパイルすることで、記述と実装の分離を行いたく思います。それではまた!


※参考:ネイティブJSでいろいろやってみたシリーズ qiita.com