アロー関数の続きです。前回はアロー関数の書式の違いについてでした。今回は従来の関数とアロー関数の挙動の違いについて。特に影響が大きそうなのは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のスコープにはグローバルなものとローカルなものがあるのだと学びました。
しかし、細かいことを気にして行くと、ローカル変数の有効範囲は関数の定義時に決まるものなのか、他の場所で呼び出した時にも更新されるものなのか、という疑問が発生しませんか?(私はしませんでした。笑)
これについては、実は各プログラミングごとに決まっていて、それぞれ「 レキシカルスコープ(静的スコープ)」と「 ダイナミックスコープ(動的スコープ)」と呼ばれています。
※参考:【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
で引数name
とspec
を設定 - プロトタイプ拡張
say()
で「name
はspec
です」というアラートを設定 - 変数
zaku
、shar
、gufu
でコンストラクタMS
からインスタンスを作り、引数も設定 zaku
、shar
、gufu
でsay()
メソッドを実行
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のアロー関数のページにも言及されている。
アロー関数式は、メソッドでない関数に最適で、コンストラクタとして使うことはできません。
メソッドは宣言時ではなく呼び出し時に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
の対象(name
とspec
)をグローバルな場所で設定してみる。
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 がスローされます。
アロー関数とクラスはどちらもES5で追加された機能なので、下記のような役割の明確化が意図されたのかもしれない。
this
を宣言時にグローバルに固定させたい時はアロー関数this
を 読み込み時に変更したい場合はクラス構文
最後に
ということで、今回はアロー関数の挙動についてもう少し掘り下げてみました。
- 単純に
function
をアロー=>
に書き換えればいいというわけではない - アロー関数の
this
は読み込み時ではなく宣言時に固定されるので注意 this
を読み込み時に変更したい場合はクラス構文のconstructor()
メソッドで定義する
クラスも単にプロトタイプを書き換えるだけなのか(どちらで書くかはご自由に)と思ってたけど、this
の挙動に違いがあるんですな。勉強になりました。
ES6〜のJSの書き方は「カイゼン」を目的とした変更がありそうなのでこれからも積極的に使ってい期待です。そして、Babelなどでコンパイルすることで、記述と実装の分離を行いたく思います。それではまた!
※参考:ネイティブJSでいろいろやってみたシリーズ qiita.com