普段、関数を書く時、f()
←こういう丸カッコを何気なく使うと思いますが、
丸カッコの意味について、再考させられる機会があったので書き留めておきます。
0 結論
いきなり結論ですが、例えば次のような関数f
があった場合、
function f(){
return "hello!";
}
f → 関数の実体(entity)
f() → 関数の返り値(return value)
であるということです。実際、
console.log(f); //--> [Function: f]
console.log(f()); //--> hello!
となります・・・?何を当たり前のことを、と思われるかもしれませんが、
この事実をもう少しだけ、掘り下げてみます。
1 検証その1
わかりやすい事例として、よく見かけるクロージャの例を挙げます。
function greet() {
var x = '';
return (function(){
x += 'hello!';
return x;
});
}
var g = greet();
console.log(g()); //--> hello!
console.log(g()); //--> hello!hello!
console.log(g()); //--> hello!hello!hello!
これは、関数greet
の返り値である無名関数 function(){...}
がグローバル変数g
から参照されることによって、function(){...}
内の変数x
もグローバル変数g
から参照され、変数x
の値がグローバル空間に保持されるという現象です。
※グローバルオブジェクトから参照をたどって行き着くことのできる値は、 ガベージコレクション(メモリの掃除のような機能)の対象から外れるので、グローバル空間に値が保持される、ということだったかと思います。
変数の値を保持するために有効な方法ですが、メモリリークの温床ともなるので無用な多用は控えましょう・・・とよく諭されます。
では、上記の処理を次のように書きかえるとどうなるでしょうか?
2 検証その2
function greet() {
var x = '';
return (function(){
x += 'hello!';
return x;
});
}
var g = greet;
console.log(g()());
console.log(g()());
console.log(g()()); //--> ?
前の例との違いは、グローバル変数g
に代入されているのが返り値greet()
でなく関数の実体greet
というところです。ちなみに、g()()
と()が2つ繋がっているのは、greet
の返り値である無名関数 function(){...}
の返り値を取り出すためです。
これもクロージャと似ていますが、前の例とはグローバル空間に保持される範囲が異なります。
ここでは関数の実体greet
をそのまま保持しているため、返り値である無名関数 function(){...}
だけでなくvar x=""
の処理部分も一緒に保持されます。したがって、出力は次のようになります。
var g = greet;
console.log(g()()); //--> hello!
console.log(g()()); //--> hello!
console.log(g()()); //--> hello!
Javascriptでは関数型の変数への代入は参照渡しとなるため、メモリへの影響はほとんどないと思われますが、関数を毎回実行するため処理量は増えます。
もう1つ例を挙げます。次の場合はどうなるでしょうか?
3 検証その3
function greet(){
var x="";
return (function(){
x += "hello!";
return ()=>x; //g()で結果を取得したいので、返り値はxを返り値とする関数にする
})(); //<--末尾に()をつけるのがポイント
}
var g = greet();
console.log(g());
console.log(g());
console.log(g()); //--> ?
検証その1と同様、グローバル変数g
への代入部分は、関数greet
の返り値greet()
を用いています。
一方、関数greet
の定義部分を見ると、返り値には、無名関数の代わりに、無名関数の末尾に()をつけて即時関数としたものが指定されています。
つまり、検証その1との違いは、グローバル変数g
に代入されるのが無名関数そのものでなく、無名関数の実行結果であるということです。
したがって、出力は次のようになります。
var g = greet();
console.log(g()); //--> hello!
console.log(g()); //--> hello!
console.log(g()); //--> hello!
ここで、グローバル変数g
から参照可能なのは無名関数 function(){...}
実行結果のみで、無名関数 function(){...}
内の変数x
へアクセスすることはできません。つまり、これはクロージャではありません。
g
に保持されるのは()=>'hello!'
の部分のみなので、上記3つの例の中では、メモリ消費量が比較的少なく、かつ処理量も少なくなります。
4 もっと実用的な例
実際にJavascriptを使って何か作る場合は、以下のように疑似クラス等を定義して使用することが多いかと思います。
function Greet(){
var x = 'hello!';
Greet.prototype.goodbye = function(){
x += 'goodbye!';
return x;
};
Greet.prototype.hello = function(){
return x;
};
}
本来はGreet.prototype
を関数Greet
の外に出す方が処理的にはベターなのですが、変数x
をプライベートメンバとして扱いたいので、便宜的に中に入れています。
検証1のクロージャと全く同じで、インスタンスg
にメンバ変数x
の値が保持されるため、次のような出力になります。
var g = new Greet();
console.log(g.goodbye()); //--> hello!goodbye!
console.log(g.hello()); //--> hello!goodbye!
ここで、Greet.prototype.hello
をメンバ変数x
の初期値を返すだけの関数にしたい場合は、どうしたらいいでしょうか?
var g = new Greet();
console.log(g.goodbye()); //--> hello!goodbye!
console.log(g.hello()); //--> hello! <--こうしたい
検証1~3の結果を用いて、メンバ変数x
の値が変わらないようにする方法を考えてみます。
5 もっと実用的な例(メンバ変数の値が変わらないようにする)
function Greet(){
var x = 'hello!';
Greet.prototype.goodbye = (function(){
x += 'goodbye!';
return ()=>x;
})();
Greet.prototype.hello = function(){
return x;
};
}
検証その3と同様、Greet.prototype.goodbye
に即時関数を割り当てているので、x
の値が保持されないように見えますが、これを実行すると
var g = new Greet();
console.log(g.goodbye()); //--> hello!goodbye!
console.log(g.hello()); //--> hello!goodbye!
・・・? 期待通りになりません。
おそらくですが、検証その3では、関数greet
は無名関数の返り値を返していたのに対し、上の例では関数Greet
が、new演算子の仕様により、暗黙的に関数Greet
の実体を返しているためだと思われます。関数Greet
のスコープ全体がグローバル変数g
に割り当てられることにより、変数x
が保持されているのだと思われます。
これを回避するためには、例えば次のような案が考えられます。
function Greet(){
var x = 'hello!';
Greet.prototype.goodbye = (function(y){
y += 'goodbye!';
return ()=>y;
})(x);
Greet.prototype.hello = function(){
return x;
};
}
こうすると、相変わらず変数x
の値は保持されているものの、Greet.prototype.goodbye
中では仮引数y
のみが使用されているため、変数x
が侵襲されることがありません。
var g = new Greet();
console.log(g.goodbye()); //--> hello!goodbye!
console.log(g.hello()); //--> hello!
6 つまり・・・どういうことだってばよ?
結局のところ、何が言いたいのかよくわからないエントリになってしまいましたが、
要するに、f()
←この丸カッコの正体は、
() の直前の関数を実行し、処理結果を得る演算子
と言い換えることができ、
f() は処理結果であり、関数そのものではない!
という簡単だけど重要な事実をしっかりと意識することで、
変数の値の管理やメモリに配慮した設計に役立つかもしれない
・・・ということです。
本エントリは、独自の実験に基づいており、誤認識等あるかも知れませんので、
コメント等でご指摘いただけると幸いです。