こういう日記を書いています。なんでこんなめんどくさいことするか、ということを説明していきます。
function diarySource() {
this.daily('1998/05/19', function () {
this.paragraph(
'やと ねつ ひいた も とてもかゆい',
'今日 はらへったの、いぬ のエサ くう');
});
this.daily('1998/05/21', function () {
this.paragraph(
'かゆい',
'うま');
});
JavaScriptのthisについておさらい
JavaScriptのFunctionオブジェクトの中には、明示的に宣言せずとも this
というローカル変数が存在します。このことは、以下のコードが動くこと、つまりReferenceErrorなどが出ないことからわかります。
(function() {
console.log(this);
})();
では、この this
は何を指しているのか、というと、「Functionオブジェクトが属しているオブジェクト」を指しています(それでは最初の例が指した this
はなんだったのか、という話になりますが、面倒くさいので避けます。)。以下のコードを実行すると、このことを確認できます。
var obj = {
f: function() {
console.log(this);
},
};
obj.f();
この性質により(幸か不幸か)、JavaScriptのFunctionオブジェクトは、静的な関数と、インスタンスメソッドの両方の役割を兼ね備えることができるのです。
またFunctionオブジェクトは、言い換えれば第1級関数なので、関数の中に関数を置くことができます。以下の入れ子の例を見てみましょう。
var obj = {
f: function() {
console.log(this);
(function() {
console.log(this);
})();
},
};
obj.f();
入れ子の内外の this
が、それぞれ指すものが異なる、ということが読み取れればそれで十分です。JavaScriptのFunctionオブジェクトは、 this
というローカル変数を暗黙に備えていて、しかも入れ子にできる、この2つが重なったときに、こういう振る舞いになる、ということです。
これが困ることがままあって、JavaScriptにおいて多用されるコールバック関数を渡すときに、内側と外側の this
がそれぞれ異なったものを指してしまいます。今回の例を手っ取り早く直すには、次のようにすると良いでしょう。
var obj = {
f: function() {
console.log(this);
(function() {
console.log(this);
}).call(this);
},
};
obj.f();
callとは、「引数を this
で読み替えて、Functionオブジェクトを評価する」関数です。callの引数の this
は、最初に出てきた this
と同じスコープにいるので、2つのconsole.logの結果が一致します。というわけで、扱いづらかった this
を、実は操作できるということがわかりました。
補足程度に、newやコンストラクタについて。
JavaScriptのFunctionオブジェクトは、幸か不幸か2つの役割を与えられていると書きましたが、実はさらにもうひとつの役割があり、それがコンストラクタです。
コンストラクタとは、new演算子と共に呼び出されることを期待する関数です。以下のようなコードが代表的です。
function Hoge() {
}
var hoge = new Hoge();
こうすると何が嬉しいのか、というと、prototypeからプロパティを引き継げるから、なのですが、さすがにprototypeの説明は割愛いたします。
callとthisのパターン (1) ローカル変数名の汚染を減らす
似たオブジェクトをいくつか作って、でもすこしずつ違いがある場合。簡単Builderパターンみたいな感じか。
var objectA = {};
objectA.name = 'alice';
objectA.isAbsent = true;
var objectB = {};
objectB.name = 'bob';
objectB.isAbsent = false;
いくつか作っていくと、例えばgrepする場合にローカル変数名が散らばっていて、nameやisAbsentをいじくる時に、コピペで再利用できません。なので、callとthisを組み合わせてこうする。
var objectA = (function() {
this.name = 'alice';
this.isAbsent = true;
return this;
}).call({});
var objectB = (function() {
this.name = 'bob';
this.isAbsent = false;
return this;
}).call({});
ひとまずこれで喫緊の課題は解決しましたが、外側の this
に触れたくなる場合は困るので、そういう場合は、
var objectA = (function(object) {
object.name = 'alice';
object.isAbsent = true;
return object;
}).call(this, {});
こういう感じならよいかと。
ちなみに、callとそっくりなメソッドとしてapplyというものがあります。callとapplyの使い分けは単純で、argumentsを渡さない場合はcall、渡す場合はapply、という基準が良いと思います。何を行っているかわからない人はcallを使うことにしましょう。
callとthisのパターン (2) オブジェクトの中に無理やり入り込む
パターン(1)と本質的には違いがないのですが、いちおう紹介します。Rubyでいうところのinstance_evalです。以下のような普通の状態を持つオブジェクトがある場合に。
var counter = {
count: 0,
countUp: function() {
return ++this.count;
},
};
counter.countUp();
counter.countUp();
counter.countUp();
この後に以下のような、意地悪を。
(function() {
this.count = -255;
}).call(counter);
counter.countUp();
counter.countUp();
counter.countUp();
private/publicの区別がないJavaScriptにおいて、この程度のことは外側から counter.count = 0;
してやれば済みますが、「インスタンスメソッドと同じ空間で無名関数を実行できる」という感触を得てもらいたいです。
callとthisのパターン (3) ポータブルなDSLと、その処理系
まずはこれを見てください。
function diarySource() {
this.date('2018/11/12');
this.at('東京');
}
function logDiary(srcFunction) {
srcFunction.call({
date: function(string) {
console.log('日付:', string);
},
at: function(string) {
console.log('場所:', string);
},
});
}
function mdDiary(srcFunction) {
var temp = '';
srcFunction.call({
date: function(string) {
temp += '# ' + string + '\n';
},
at: function(string) {
temp += '今日は、' + string + 'にいた。'
}
});
return temp;
}
logDiary(diarySource);
mdDiary(diarySource);
diarySourceは、thisが多用されているけれども、外側の変数や関数に依存しておらず、高度に抽象化されているので、サーバーサイド・クライアントサイド問わず動くし、とりあえずdiarySourceで書いておいて、他の形式の出力が必要になったら、 logDiaryやmdDiaryのようなものを増やしていけばよい、という点が優れていると思います。
現代的なJavaScriptであれば、class構文とかextendsとかTemplate Methodパターンとかでできるんでしょう。
Google Apps ScriptでDSL
そういうわけで、とりあえず何にも依存しないfunctionの入れ子で日記を書いておくと、それをcallする時の引数に応じて振る舞いを変えることができるので、便利です。なんらかのデザインパターンを利用して同じ目的を達成できるでしょうが、古くからのJavaScriptでも再現する、つまりポータビリティの点で有利です。私はこのjsコードを、Google Apps Scriptで書いていて、まぁあとからスプレッドシートなりドキュメントなりの形式でほしくなれば、そういう「callする側」の機能を書けば済む、という方針でいます。抽象的に記述していく指針があれば、生成物の形式を考えるのを後回しにできるので、便利だな、と思いました。(力尽きた)