みなさん、こんにちは!
タカハシ(@ntakahashi0505)です。
「初心者でもわかるGoogle Apps Scriptのクラス」をテーマにシリーズ連載をお送りしてまいりました。
前回の記事はこちらです。
GASでオブジェクトに直接追加しているプロパティについてのみループをする方法をお伝えしました。
それで、そのプロパティについてのみループしてログ出力するという機能もよく使いそうなので、メソッドとしてクラスに追加しちゃおうと思っています。
ですが、ちょっと簡単にはいかないことがありまして…
今回はそのへんを進めていきますよ。
GASでselfを使ってスコープの中からインスタンスを表すthisを使えるようにする方法です。
ナンノコッチャですよね…?
でも、行ってみましょう!
前回のおさらい
まずは、これまでのおさらいからです・
以下のようなスプレッドシートがあります。
この一行ずつのデータを表すクラスを作成しました。
こちらのクラスPersonです。
(function(global){ var Person = function(record){ var _id = record[0]; this.name = record[1]; this.gender = record[2]; this.birthday = record[3]; Object.defineProperties(this, { id: { get: function(){ return _id; } } }); }; Person.prototype.greet = function(){ Browser.msgBox(this.name + "です、こんにちは!"); }; Person.prototype.log = function(){ Logger.log('%s|%s|%s|%s|', this.id, this.name, this.gender, this.birthday); }; global.Person = Person; })(this);
それで、各行から生成したインスタンスを集合で扱うためのクラスを作りました。
それがこちらのクラスPersonsです。
(function(global){ var Persons = function() { var values = SpreadsheetApp.getActiveSheet().getDataRange().getValues(); values.shift(); for(var i = 0; i < values.length; i++){ var p = new Person(values[i]); this[p.id] = p; } }; Persons.prototype.add = function(record){ var p = new Person(record); this[p.id] = p; return this[p.id]; }; Persons.prototype.remove = function(id){ delete this[id]; }; global.Persons = Persons; })(this);
それで、前回。
このPersonsクラスのインスタンスから、それに含まれるPersonオブジェクトのデータをログ出力で確認できるように、Object.keysメソッドを使って以下のような処理を作成しました。
function myFunction() { var persons = new Persons(); Object.keys(persons).forEach( function(id) { persons[id].log(); } ); }
今回のお題:「var self = this」とは?
それで、このPersonsオブジェクトの内容をログ出力するメソッドなのですが、よく使いそうなので、Personsクラスに追加しちゃおう!
というのが今回やりたいことです。
ただし、ちょっと引っかかるポイントがあります。
何が引っかかるポイントなのか、またその解決方法が「var self = this」であり、それはなぜかということについてお伝えしていきます。
「undefinedのメソッドを呼び出せません」
では、ひとまずストレートにメソッドを追加してみましょう。
上記のmyFunctionの5~9行目を、コンストラクタPersonsのprotorypeプロパティにlogメソッドとして追加していきます。
コンストラクタPersonsでいうと、24行目あたりに以下のようなコードを挿入すれば良さそうに思えます。
Persons.prototype.log = function() { Object.keys(this).forEach( function(id) { this[id].log(); } ); }
では、myFunctionを以下のように変更して、実行してみましょう。
function myFunction() { var persons = new Persons(); persons.log(); }
おや…?
「undefinedのメソッド「log」を呼び出せません。」というエラーが出てしまいます。
thisは場所によって参照するものが異なる
ここでいう「this」はPersonsクラスのインスタンスを表していてほしくて、ブラケット(角括弧)の中にidを指定しているので、これでPresonオブジェクトを取り出しているはず…
そして、Personオブジェクトにはlogメソッドがあるので、それを呼び出せるはず…
と思っていたのですが、ちょっと勝手が違うようです。
このエラー、原因を知るのは難しいのですが、forEachメソッドの引数として指定されている関数が原因です。
その中に「this」があるのですが、実はこの関数内のthisはPersonsオブジェクトではなく、グローバルオブジェクトを表します。
thisの場所と参照先の対応
thisは場所によって参照するものが異なるのです。
表にすると以下になります。
thisの場所 | thisが参照するもの |
---|---|
グローバル | グローバルオブジェクト |
コンストラクタ | 生成したインスタンス |
メソッド | 呼び出し元のオブジェクト |
関数 | グローバルオブジェクト |
つまり、コンストラクタ直下もしくはメソッド直下であれば、そのインスタンスを表すわけですが、その内部に含まれる関数の中に入ってしまうと、thisはグローバルオブジェクトになっちゃうというわけです。
なので、そもそもグローバルオブジェクトには「a01」とか「a02」といったプロパティが存在していないので、未定義つまりundefinedとなってしまうというわけです。
thisがグローバルオブジェクトを指していることを確認
では、forEachメソッドの関数内のthisがグローバルオブジェクトを指していることを確認してみましょう。
グローバル領域に以下のような、ステートメントを書いておきます。
this.a01 = 'hoge'; this.a02 = 'fuga'; this.a03 = 'foo';
これで、グローバルオブジェクトのa01やa02はundefinedではなくなります。
続いて、コンストラクタPersonsのlogメソッドですが、以下のようにして実行してみましょ。
Persons.prototype.log = function() { Object.keys(this).forEach( function(id) { Logger.log(this[id]); } ); }
確かに、forEachメソッドの関数内のthisはグローバルオブジェクトでしたね。
selfを使ってインスタンスを関数内に持ち込む
では、forEachメソッドの関数内でインスタンスを参照したいときはどうすれば良いでしょうか?
実はそんなに難しいことではなくて、logメソッド内でインスタンスを指しているときのthisを任意の変数に代入しておいて、関数内からそれを参照すればOKです。
その任意の変数として、よくselfという変数名が使われるというわけです。
つまり、こういうことです。
Persons.prototype.log = function() { var self = this; Object.keys(this).forEach( function(id) { self[id].log(); } ); }
logメソッド内では、thisは呼び出したオブジェクトを指します。つまり、Personsオブジェクトのインスタンスです。
それを、変数selfに代入しておきます。
forEachメソッド内ではthisはグローバルオブジェクトになってしまいますが、変数selfは変わらずPersonsオブジェクトのインスタンスを参照しているというわけです。
myFunctionを実行すると、無事にすべてのデータについてログ出力されていることを確認できます。
まとめ
以上、GASでselfを使って関数の中からインスタンスを表すthisを使えるようにする方法をお伝えしました。
スコープとthis…ものすごいJavaScriptの特徴を味わえる内容だったと思います。
けっこうハマりやすいポイントだと思いますので、覚えておくとよいですね。
では、次回はいよいよPersonsオブジェクトの内容をスプレッドシートに反映させるメソッドを作ります。
どうぞお楽しみに!