JavaScriptカーネルシリーズ第7章クローズ


第七章閉パッケージ
閉パッケージは、JavaScriptプログラマーを含むプログラマーに神秘的で深い感覚を与えてきたが、実際には、閉パッケージの概念は関数式プログラミング言語では理解しにくい知識ではない.役割ドメイン,関数が独立したオブジェクトであるという基本概念をよく理解すれば,閉パケットの概念を理解し,実際のプログラミング実践に応用するのに水が流れるような感じがする.
DOMのイベント処理では、多くのプログラマーが閉パッケージを使用していることさえ知らない.この場合、ブラウザに埋め込まれたJavaScriptエンジンのバグがメモリ漏れを引き起こす可能性があるという問題はともかく、プログラマーが自分でデバッグしても霧が立ち込めることが多い.
JavaScriptの閉パッケージの概念を簡単な文で説明します.JavaScriptでは、関数がオブジェクトであり、オブジェクトが属性の集合であり、属性の値がオブジェクトであってもよいため、関数内で関数を定義するのは当然です.関数funcの内部で関数innerを宣言し、関数の外部でinnerを呼び出すと、このプロセスは閉パッケージを生成します.
7.1閉パッケージの特性
まず、JavaScriptの特性を理解していないと、原因を見つけるのは難しい例を見てみましょう.
 
var outter = [];
function clouseTest () {
    var array = ["one", "two", "three", "four"];
    for(var i = 0; i < array.length;i++){
       var x = {};
       x.no = i;
       x.text = array[i];
       x.invoke = function(){
           print(i);
       }
       outter.push(x);
    }
}
 
//      
clouseTest();
 
print(outter[0].invoke());
print(outter[1].invoke());
print(outter[2].invoke());
print(outter[3].invoke());
 
運転の結果はどうなりますか?多くの初心者はこのような答えを出すかもしれません.
0
1
2
3
 
しかし、このプログラムを実行すると、次の結果が得られます.
4
4
4
4
 
実は、反復するたびに、このような文x.invoke=function(){print(i);}実行されていませんが、関数体は「print(i);」として構築されています.の関数オブジェクト、それだけです.一方、i=4の場合、反復が停止する外部関数が戻り、outter[0]が呼び出される.invoke()の場合、iの値は依然として4であるため、outter配列の各要素のinvokeはiの値:4を返します.
 
どうやってこの問題を解決しますか?匿名関数を宣言し、すぐに実行できます.
 
var outter = [];
 
function clouseTest2(){
    var array = ["one", "two", "three", "four"];
    for(var i = 0; i < array.length;i++){
       var x = {};
       x.no = i;
       x.text = array[i];
       x.invoke = function(no){
           return function(){
              print(no);
           }
       }(i);
       outter.push(x);
    }  
}
 
clouseTest2();
 
この例では、x.invokeに値を割り当てるときに、関数を返す関数を実行してからすぐに実行します.これにより、x.invokeの反復器のたびに、このような文を実行することに相当します.
 
//x == 0
x.invoke = function(){print(0);}
//x == 1
x.invoke = function(){print(1);}
//x == 2
x.invoke = function(){print(2);}
//x == 3
x.invoke = function(){print(3);}

 
 
これで正しい結果が得られます.閉パッケージを使用すると、外部関数に存在する変数を参照できます.ただし、この変数を使用して作成された値ではなく、外部関数の変数の最後の値を使用します.
7.2閉パッケージの用途
今では、閉パッケージの概念が明らかになりました.閉パッケージの用途を見てみましょう.実際、閉パッケージを使用することで、私たちは多くのことをすることができます.たとえば、オブジェクト向けのコードスタイルをシミュレートします.より優雅で、より簡潔にコードを表現します.いくつかの面でコードの実行効率を向上させる.
7.2.1匿名自己実行関数
前節の例では、実際には閉パッケージの1つの用途であり、前述した内容から分かるように、すべての変数にvarキーワードを付けなければ、デフォルトではグローバルオブジェクトの属性に追加され、このような一時変数がグローバルオブジェクトに追加されるのは多くのデメリットがあります.例えば、他の関数がこれらの変数を誤用する可能性があります.グローバルオブジェクトが膨大すぎてアクセス速度に影響します(変数の値はプロトタイプチェーンから遍歴する必要があるため).変数を使用するたびにvarキーワードを使用する場合を除き、UIの初期化などの内部変数を維持する必要がなく、閉パッケージを使用することができます.
 
var datamodel = {
    table : [],
    tree : {}
};
 
(function(dm){
    for(var i = 0; i < dm.table.rows; i++){
       var row = dm.table.rows[i];
       for(var j = 0; j < row.cells; i++){
           drawCell(i, j);
       }
    }
   
    //build dm.tree  
})(datamodel);
 
匿名の関数を作成し、すぐに実行します.外部は内部の変数を参照できないため、実行が完了するとすぐに解放されます.このメカニズムがグローバルオブジェクトを汚染しないことが重要です.
7.2.2キャッシュ
もう1つの例では、処理に時間がかかる関数オブジェクトがあり、呼び出すたびに時間がかかると想定します.計算した値を格納する必要があります.この関数を呼び出すときは、まずキャッシュで検索し、見つからない場合は計算し、キャッシュを更新して値を返します.見つかったら、検索した値を直接返します.閉パッケージは、外部の参照が解放されないため、関数内部の値が保持されるため、これを行うことができます.
 
var CachedSearchBox = (function(){
    var cache = {},
       count = [];
    return {
       attachSearchBox : function(dsid){
           if(dsid in cache){//        
              return cache[dsid];//          
           }
           var fsb = new uikit.webctrl.SearchBox(dsid);//  
           cache[dsid] = fsb;//    
           if(count.length > 100){//       <=100
              delete cache[count.shift()];
           }
           return fsb;      
       },
 
       clearSearchBox : function(dsid){
           if(dsid in cache){
              cache[dsid].clearSelection();  
           }
       }
    };
})();
 
CachedSearchBox.attachSearchBox("input1");
 
 
CachedSearchBoxを2回目に呼び出すとattachSerachBox(「input 1」)では、新しいsearchboxオブジェクトを作成することなく、キャッシュからオブジェクトを取り出すことができます.
7.2.3実装パッケージ
まず、パッケージに関する例を見てみましょう.person以外の場所では内部の変数にアクセスできず、閉パッケージの形式でアクセスできます.
 
var person = function(){
    //          ,      
    var name = "default";   
   
    return {
       getName : function(){
           return name;
       },
       setName : function(newName){
           name = newName;
       }
    }
}();
 
print(person.name);//    ,   undefined
print(person.getName());
person.setName("abruzzi");
print(person.getName());
 
結果は次のとおりです.
 
undefined
default
abruzzi
 
閉包のもう一つの重要な用途は、オブジェクト向けのオブジェクトを実現することであり、従来のオブジェクト言語はクラスのテンプレートメカニズムを提供しており、このように異なるオブジェクト(クラスのインスタンス)は独立したメンバーと状態を持ち、互いに干渉しない.JavaScriptにはクラスのようなメカニズムはありませんが、閉パッケージを使用することで、このようなメカニズムをシミュレートすることができます.以上の例で説明します.
 
function Person(){
    var name = "default";   
   
    return {
       getName : function(){
           return name;
       },
       setName : function(newName){
           name = newName;
       }
    }
};
 
 
var john = Person();
print(john.getName());
john.setName("john");
print(john.getName());
 
var jack = Person();
print(jack.getName());
jack.setName("jack");
print(jack.getName());
 
実行結果は次のとおりです.
 
defaultjohndefaultjack
 
このコードから、johnとjackはPersonというクラスのインスタンスと呼ぶことができる.この2つのインスタンスはnameというメンバーへのアクセスが独立しており、互いに影響しないからだ.
実際,関数式のプログラム設計では,閉パケットが大量に用いられるが,8章では関数式プログラミングについて議論し,そこで閉パケットの役割を再び検討する.
7.3注意すべき問題
7.3.1メモリリーク
異なるJavaScript解釈器の実装では、解釈器自体の欠陥により、閉パッケージを使用するとメモリが漏れる可能性があり、メモリの漏洩は比較的深刻な問題であり、ブラウザの応答速度に深刻な影響を及ぼし、ユーザー体験を低下させ、ブラウザの応答がないなどの現象をもたらす可能性がある.
JavaScriptの解釈器はすべてごみの回収のメカニズムを備えて、一般的に引用のカウントの形式を採用して、1つのオブジェクトの引用のカウントがゼロならば、ごみの回収のメカニズムはそれを回収して、この過程は自動的です.しかし、閉パケットの概念があると、このプロセスは複雑になり、閉パケットでは、局所的な変数が将来のある時点で使用される必要がある可能性があるため、ゴミ回収メカニズムは、外部に参照された局所的な変数を処理することはなく、ループ参照、すなわちオブジェクトAがBを参照し、BがCを参照し、CがAを参照する場合、このような状況により、ゴミ回収メカニズムは、参照カウントがゼロでないと結論し、メモリ漏れをもたらす.
7.3.2コンテキストの参照
thisについては、呼び出しオブジェクトへの参照を表す前に議論しましたが、閉パッケージで最もエラーが発生しやすいのはthisを誤用したことです.フロントエンドJavaScriptの開発では、一般的なエラーの1つは、thisクラスを他の外部ローカル変数に比較することです.
 
$(function(){
    var con = $("div#panel");
    this.id = "content";
    con.click(function(){
       alert(this.id);//panel
    });
});

 
ここのalert(this.id)はいったいどんな値を引用しているのだろうか.多くの開発者は、閉パッケージの概念に基づいて、誤った判断を下す可能性があります.
content
 
 
理由は、this.idはcontentとして表示するが、clickコールバックでは形成された閉パケットがthisに参照される.idのため、戻り値はcontentです.しかし、実際には、このalertが「panel」をポップアップするのは、ここでのthisであり、閉パケットは局所変数を参照することができるが、thisに関連する場合、閉パケットが呼び出された場合(このpanelのclickイベントが発生した場合)、ここでのthisはconというjQueryオブジェクトを参照するように、呼び出しオブジェクトの存在が微妙であるためである.匿名関数のthis.id=「content」は匿名関数自体に対する操作である.2つのthisが参照しているのは同じオブジェクトではありません.
イベント処理関数でこの値にアクセスするには、いくつかの変更が必要です.
 
$(function(){
    var con = $("div#panel");
    this.id = "content";
    var self = this;
    con.click(function(){
       alert(self.id);//content
    });
});

 
 
このように,イベント処理関数に保存されるのは,thisではなく外部の局所変数selfの参照である.このテクニックは実際の応用に多く応用されており,後の章で詳細に議論する.閉パッケージの詳細については、他のコマンド言語での「閉パッケージ」、実際のプロジェクトでの閉パッケージの応用など、9章で詳しく説明します.
 
附:作者自身のレベルが限られているため、文の中で間違いなどを漏らすのは避けられない、あるいは言語自身に不適切なところがあるので、直ちに指摘して、提案を出して、討論に参加して、ありがとうございます!