JavaScript
関数
クロージャ

関数のお尻についてるf() ←この丸カッコについて考えてみた

普段、関数を書く時、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() は処理結果であり、関数そのものではない!

という簡単だけど重要な事実をしっかりと意識することで、

変数の値の管理やメモリに配慮した設計に役立つかもしれない

・・・ということです。


本エントリは、独自の実験に基づいており、誤認識等あるかも知れませんので、

コメント等でご指摘いただけると幸いです。