実装から理解するクロージャ
実装から理解するクロージャ
About Me
- κeen
- @blackenedgold
- Github: KeenS
- 渋谷のエンジニア
- Lisp, ML, Shell Scriptあたりを書きます
クロージャとは?
- 日本語にすると(関数)閉包
- 関数が外側のローカル変数を補足する
- 補足されたローカル変数は無限の生存期間を持つ
- ローカル変数は本来スコープを抜けると生存期間が終わる
- 言い換えるとグローバル変数みたいになる
- でもあくまでスコープはローカル
コード例
function genpower(n){
var x = 1;
return function(){
x *= n;
return x;
};
}
var p = genpower(2);
コード例
p() // => 2
p() // => 4
p() // => 8
x * 2 // x is not defined
コード例
p
がn
とx
を補足しているので関数を抜けた後もx
とn
は使える。- 関数の仮引数もローカル変数。
- でもローカル変数なので外からは見えない。
+-----------------------+
| function genpower(n){ |
| var x = 1; |
| ... ^ |
| } | |
+-----------------------+
|
+------+
|
+-------------+
||function(){ |
|+-- x *= n; |
| return x; |
| }; |
+-------------+
コード例2
function incdec(){
var x = 0;
return [function(){ return ++x;},
function(){ return --x;}];
}
var fs = incdec();
var inc = fs[0];
var dec = fs[1];
コード例2
inc() // => 1
inc() // => 2
dec() // => 1
inc() // => 2
コード例2
- 同じタイミングで作られたクロージャ群は捕捉変数を共有する
+--------------------+
| function incdec(){ |
| var x = 0; |
| ... ^ |
| } | |
+--------------------+
+--------------+----+
| |
+----------------------|---+|
| function(){ return ++x;} ||
+--------------------------+|
+----+
+----------------------|---+
| function(){ return --x;} |
+--------------------------+
コード例3
function genpower(n){
var x = 1;
return function(){
x *= n;
return x;
};
}
var p1 = genpower(2);
var p2 = genpower(2);
コード例3
p1() // => 2
p1() // => 4
p2() // => 2
p2() // => 4
コード例3
- 逆に、同じ関数から生まれても違うタイミングなら共有しない。
+-----------------------+ +-----------------------+
| function genpower(n){ | | function genpower(n){ |
| var x = 1; | | var x = 1; |
| ... ^ | | ... ^ |
| } | | | } | |
+-----------------------+ +-----------------------+
| |
+------+ +------+
| |
+-------------+ +-------------+
||function(){ | ||function(){ |
|+-- x *= n; | |+-- x *= n; |
| return x; | | return x; |
| }; | | }; |
+-------------+ +-------------+
実装方法
- ここでは複数ある実装方法のうちの1つを紹介する。
- 言語はVM型のインタプリタ(大抵のインタプリタの実装に同じ)を仮定する
用語整理
outer
から見たらx
は捕捉(Captured)変数inner
から捕捉されてるから
inner
から見たらx
は自由(Free)変数inner
からしたらx
は知らない子だから
function outer(x) {
function inner(y){
return x * y;
}
}
実装概要
- クロージャとは捕捉変数の集まり
- つまり、捕捉した側ではなくされた側が作る
- 捕捉した側は作られたものを参照するだけ
変数の話
- グローバル変数はヒープ領域に置かれる
- グローバル DB(大抵巨大なハッシュテーブル)に登録される
- ローカル変数はコールスタックに置かれる
- 配列が作られ、インデックスでアクセスされる感じ。
- ローカル変数の数は関数定義時に決定するので配列で管理出来る
- 関数の実引数も同じように置かれる
捕捉変数の話
- 捕捉変数はヒープ領域に置かれる
- 簡単には小さなハッシュテーブルに登録される
- つまり、グローバル変数と同じ
- 捕捉変数も関数定義時に決定するので配列でも管理出来る
- ハッシュテーブル/配列はクロージャ毎に作られる
- 簡単には小さなハッシュテーブルに登録される
※ 本気出した実装だとコールスタックでどうにかすることもある
var g = 1;
function sample(a) {
var l = 2;
var c = 3;
return function(){ return c;};
}
| .... |
|---------------|
| args[0] = _ | a
|---------------|
| locals[0] = 1 | var l
|---------------|--コールスタック↑--
| .... |
| .... |
|---------------|--ヒープ領域↓--
| caps[c] = 3 | var c = 3
|---------------|
| global[g] = 1 | var g = 1
| global[_] = _ |
| .... |
| .... |
function incdec(){
var x = 0;
return [function(){ return ++x;},
function(){ return --x;}];
}
var fs = incdec();
var inc = fs[0];
var dec = fs[1];
| .... |
|---------------|--ヒープ領域↓--
| caps[x] = 0 <-++------------------------+
|---------------|| +----------------------|---+
| global[_] = _ || | function(){ return ++x;} |
| .... || +--------------------------+
| .... |+------------------------+
| .... | +----------------------|---+
| .... | | function(){ return --x;} |
| .... | +--------------------------+
| .... |
| .... |
| .... |
function genpower(n){
var x = 1;
return function(){
x *= n;
return x;
};
}
var p1 = genpower(2);
var p2 = genpower(2);
AA略
| .... |
|---------------|--ヒープ領域↓--
| caps[x] = 0 <-+-|p1|
|---------------|
| caps[x] = 0 <-+-|p2|
|---------------|
| global[_] = _ |
| .... |
| .... |
捕捉変数の実装
- クロージャ毎にcapturedが作られる
- capturedとlocalはソース上の見た目は似ているが実装は大きく異なる
- 多分この所為で分かりづらい
- グローバルアクセス出来ないだけでグローバル変数に似ている
まとまってないけどまとめ
- クロージャについて説明した
- クロージャの正体は捕捉変数の集まり
- 捕捉変数はヒープ領域に置かれるローカルスコープな変数
※あくまで実装の1例です
実装から理解するクロージャ