クロージャとは、自身の関数定義を取り囲む構文スコープに存在する変数などの値を利用できる関数のことだ。JavaScriptでは、これを利用して、情報隠ぺいが実現されている。以下に例を示す。
function getInc(init) {
var start = init;
return function() { // 「return () => start++;」と等価
return start++;
}
}
var inc = getInc(0);
console.log(inc());
console.log(inc());
この例では、getInc関数は関数を返すが、return文の前でローカル変数startを定義している。そして、return文で返送している関数はこのローカル変数startの値にアクセスできる。getInc関数が終了しても、このローカル変数は破棄されずに生存を続けるため、その後で関数が呼び出されてもこの値を利用できる。変数startの初期値はインクリメントを行う際の初期値となり、返送された関数が呼び出されるたびにその値が1ずつ増えていく。
また、変数startには、getInc関数とその戻り値である匿名関数以外からはアクセスする手段もない。このため、外部からアクセスしてほしくない情報をこのような形で保持することで情報も隠ぺいできる。
このように関数外部の構文スコープで定義されている情報を補足(キャプチャー)して、利用するような関数のことをクロージャと呼ぶ。JavaScriptではこのようなクロージャが活用されている。上記のコード自体はECMAScript 2015でもTypeScriptでも変わらないので、これらのコードは割愛しよう。
そしてもちろん、C#でもクロージャは実現可能だ。以下に同じことをC#で行う例を示す。
namespace cs
{
class Program
{
static Func<int> getInc(int init)
{
var start = init;
return () => start++;
}
public static void Main()
{
var inc = getInc(0);
Console.WriteLine(inc());
Console.WriteLine(inc());
Console.ReadKey();
}
}
}
上のJavaScriptコードでは関数式を使用していたが(JavaScriptコードのコメントにもある通り、「return () => start++;」でもよい。このような場合は「return start++;」から「return」の記述を省略して単に「start++;」だけを書けばよいのである)、C#バージョンではラムダ式を使っている。また、getInc関数の戻り値型である「Func<int>」は引数を取らずに、int型の戻り値を返すことを意味するデリゲートである。両者を見ると、戻り値型の指定以外はほぼ同様に記述できることが分かる。
だが、両者の間には大きな違いもある。
その大きな違いを検証するために、上のコードを以下のように書き換えてみた(今度はクラスBarを定義し、そのインスタンスごとにインクリメントの初期値を共有する。あるインスタンスから得たインクリメント関数は、実行するたびにthis.start(インスタンスのメンバー「start」)の値を1増加させる。かなり作為的なコードだ)。
namespace cs
{
class Bar
{
private int start;
public Bar(int init)
{
start = init;
}
public Func<int> getInc()
{
return () => this.start++;
}
}
class Program
{
public static void Main()
{
var b = new Bar(0);
var inc = b.getInc();
Console.WriteLine(inc()); // 0
Console.WriteLine(inc()); // 1
var inc2 = b.getInc(); // 新たなインクリメント関数を取得
Console.WriteLine(inc2()); // 2: カウントは以前の値を引き継ぐ
Console.WriteLine(inc2()); // 3
Console.ReadKey();
}
}
}
インクリメント関数を取得するたびに、初期値からカウントが始まってほしいと思うのが普通だが、これはサンプルなのでそういうものだと思ってほしい。
これをJavaScriptとECMAScript 2015で書き直したものが以下だ。ここではアロー関数を使わずに匿名関数を使っている。
var Bar = (function() {
function Bar(init) {
this.start = init;
}
Bar.prototype.getInc = function() {
return function() {
return this.start++;
}
}
return Bar;
})();
class Baz {
constructor(init) {
this.start = init;
}
getInc() {
return function() {
return this.start++;
}
}
}
var b1 = new Bar(0);
var b2 = new Baz(0);
var inc1 = b1.getInc();
var inc2 = b2.getInc();
console.log(inc1());
console.log(inc2());
これを実行してみると、「Cannot read property 'start' of undefined」と怒られてしまう。JavaScriptでは、メソッドの呼び出しに使われたオブジェクトが「this」としてそのメソッドに渡される。そのため、取得したインクリメント関数(inc1関数/inc2関数)を単独で実行しても何らかのオブジェクトがthisとしてインクリメント関数に渡されることはない(興味ある方は「Lambdas and using 'this'」ページ(英語)などを参照されたい)。要するに、上記の書き方ではBarクラスのインスタンスをクロージャの中で利用できないのである。そして、アロー関数ではこれが可能になっている。
class Baz {
constructor(init) {
this.start = init;
}
getInc() {
return () => this.start++;
}
}
var b = new Baz(0);
var inc1 = b.getInc();
console.log(inc1()); // 0
console.log(inc1()); // 1
var inc2 = b.getInc();
console.log(inc2()); // 2
console.log(inc2()); // 3
このように、匿名関数とアロー関数ではthisに関連する挙動が異なるので、注意が必要だ。最後にTypeScript版のコードも示しておこう。メンバー変数の宣言や型注釈以外は上のコードと同様なので説明は省略する。
class Baz {
private start: number;
constructor(init: number) {
this.start = init;
}
getInc(): Function {
return () => this.start++;
}
}
var b = new Baz(0);
var inc1 = b.getInc();
alert(inc1());
alert(inc1());
var inc2 = b.getInc();
alert(inc2());
alert(inc2());
なお、匿名関数でも上記と同様な処理を行いたい場合には、一度、this変数の値をローカル変数にコピーするとよい(これにより、その値が構文スコープに存在するようになるため、クロージャから利用できるようになる)。
class Baz {
constructor(init) {
this.start = init;
}
getInc() {
var _this = this;
return function() {
return _this.start++;
}
}
}
今回は関数定義に関連するコードをC#/JavaScript 5/ECMAScript 2015/TypeScriptを用いて書きながら、C#とJavaScriptの差異を見た。ラムダ式/アロー関数による関数記述がC#とECMAScript 2015/TypeScriptではほぼ同一になるなど、C#プログラマーには便利な面もある一方で、JavaScriptならではのクセ、thisの挙動がC#プログラマーの直感とは異なるなど、注意すべき点もある。
次回はモジュールの扱いなどについて見ていく予定だ。
Copyright© Digital Advantage Corp. All Rights Reserved.