クロージャの定義をいくつか見てみましょう:
クロージャとは、別の関数のスコープ内の変数にアクセスする権利を持つ関数を指します - 『JS高度なプログラミング 第3版』p178
関数オブジェクトスコープチェーンを介して関連付けることができ、関数本体内の変数を関数スコープに格納できます。この機能は「クロージャー」と呼ばれます。 -- 『決定版 JS ガイド』 p183
内部関数は、それが定義されている外部関数のパラメータと変数にアクセスできます (this
と arguments
を除く) )。 -- 『JS言語の本質』p36this
和arguments
)。 --《JS语言精粹》 p36
来个定义总结
可以访问外部函数作用域中变量的函数
被内部函数访问的外部函数的变量可以保存在外部函数作用域内而不被回收---这是核心,后面我们遇到闭包都要想到,我们要重点关注被闭包引用的这个变量。
来创建个简单的闭包
var sayName = function(){var name = 'jozo';return function(){ alert(name); } };var say = sayName(); say();
来解读后面两个语句:
var say = sayName()
:返回了一个匿名的内部函数保存在变量say中,并且引用了外部函数的变量name,由于垃圾回收机制,sayName函数执行完毕后,变量name并没有被销毁。
say()
:执行返回的内部函数,依然能访问变量name,输出 'jozo' .
理解作用域链对理解闭包也很有帮助。
变量在作用域中的查找方式应该都很熟悉了,其实这就是顺着作用域链往上查找的。
当函数被调用时:
先创建一个执行环境(execution context),及相应的作用域链;
将arguments和其他命名参数的值添加到函数的活动对象(activation object)
作用域链:当前函数的活动对象优先级最高,外部函数的活动对象次之,外部函数的外部函数的活动对象依次递减,直至作用域链的末端--全局作用域。优先级就是变量查找的先后顺序;
先来看个普通的作用域链:
function sayName(name){return name; }var say = sayName('jozo');
这段代码包含两个作用域:a.
全局作用域;b.
定義をまとめてみましょう
外部関数のスコープ内の変数にアクセスできる関数
は、内部関数 外部関数の変数は、再利用せずに外部関数のスコープに格納できます。これが核心です。後でクロージャに遭遇したときに、このことを考えるでしょう。クロージャによって参照される変数に注目する必要があります。 。 は単純なクロージャを作成します。
function sayName(name){return function(){return name; } }var say = sayName('jozo');
は次の 2 つのステートメントを解釈します: varsay =sayName() </ code>: 変数sayに格納された匿名内部関数を返し、外部関数の変数名を参照します。ガベージコレクション機構により、変数名はsayName関数の実行後に破棄されません。 </p><p></p><p><code>say()
: 返された内部関数を実行します。変数名にアクセスでき、「jozo」を出力します。
スコープ チェーンを理解することは、クロージャを理解するのにも役立ちます。
実際、スコープ内で変数が検索される方法についてはよく知っているはずです。関数が呼び出されるとき:
🎜🎜最初に実行コンテキストと対応するスコープチェーンを作成します。🎜🎜🎜🎜関数のアクティブオブジェクト(アクティベーションオブジェクト)に引数と他の名前付きパラメータの値を追加します。 )🎜🎜🎜スコープ チェーン: 現在の関数のアクティブ オブジェクトが最も高い優先度を持ち、次に外部関数のアクティブ オブジェクトが続き、外部関数の外部関数のアクティブ オブジェクトは、スコープ チェーンの終端 -- グローバル スコープ。優先順位は変数が検索される順序です。🎜🎜🎜まず共通のスコープ チェーンを見てみましょう。このコードには 2 つのスコープが含まれています。a.
b.< /code>sayName 関数のスコープ。つまり、変数オブジェクトは 2 つだけです。対応する実行環境に対して実行されると、変数オブジェクトはアクティブ オブジェクトになり、実行環境スコープ チェーンのフロントエンドにプッシュされます。つまり、最も優先度が高いものです。 絵を見ながら話しましょう: 🎜🎜🎜🎜🎜🎜🎜この絵はJS上級プログラミングの本にも載っていて、また描きました。 🎜🎜sayName() 関数を作成すると、変数オブジェクトを事前に含むスコープ チェーンが作成されます。これは、図のインデックス 1 を持つスコープ チェーンであり、呼び出されたときに内部の [[Scope]] 属性に保存されます。 SayName() 関数を呼び出すと、実行環境が作成され、関数の [[Scope]] 属性内のオブジェクトをコピーすることによってアクティブなドメイン チェーンが構築されます。その後、別のアクティブなオブジェクト (図のインデックス 0)。 ) が作成され、実行環境スコープ チェーンの先頭にプッシュされます。 🎜🎜一般に、関数が実行されると、ローカルのアクティブなオブジェクトは破棄され、グローバル スコープのみがメモリに保存されます。ただし、クロージャの状況は異なります: 🎜🎜🎜 クロージャのスコープ チェーンを見てみましょう: 🎜🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="hljs oxygene">// 方式1var a = 0;var add = function(){
a++;
console.log(a)
}add();add();//方式2 :闭包var add = (function(){
var a = 0;
return function(){
a++;
console.log(a);
}
})();
console.log(a); //undefinedadd();add();
相比之下方式2更加优雅,也减少全局变量,将变量私有化</pre><div class="contentsignin">ログイン後にコピー</div></div>🎜 このクロージャ インスタンスには、前の例よりも匿名関数のスコープが 1 つ増えています: 🎜🎜🎜🎜 🎜🎜🎜After匿名関数がsayName()関数から返され、そのスコープチェーンがアクティブオブジェクトとsayName()関数を含むグローバル変数オブジェクトに初期化されます。このようにして、匿名関数は、sayName() で定義されたすべての変数とパラメータにアクセスできます。さらに重要なのは、sayName() 関数の実行後、そのアクティブなオブジェクトは、このアクティブな関数のスコープ チェーンにより破棄されません。つまり、sayName() 関数が実行された後、その実行環境のスコープ チェーンは破棄されますが、そのアクティブなオブジェクトは匿名関数が破棄されるまでメモリ内に残ります。これは、後で説明するメモリ リークの問題でもあります。 🎜🎜スコープチェーンの問題についてはあまり書きませんし、本に書くのも大変なのでo(╯□╰)o🎜🎜3. クロージャの例🎜🎜🎜例1: 蓄積の実装🎜🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="hljs javascript"> var oli = document.getElementsByTagName(&#39;li&#39;); var i; for(i = 0;i < 5;i++){
oli[i].onclick = function(){
alert(i);
}
} console.log(i); // 5 //执行匿名函数
(function(){
alert(i); //5
}());</pre><div class="contentsignin">ログイン後にコピー</div></div> 🎜🎜例 2: 各 li にクリック イベントを追加します🎜🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="hljs javascript"> var oli = document.getElementsByTagName(&#39;li&#39;); var i; for(i = 0;i < 5;i++){
oli[i].onclick = (function(num){ var a = num; // 为了说明问题 return function(){
alert(a);
}
})(i)
} console.log(i); // 5</pre><div class="contentsignin">ログイン後にコピー</div></div><div class="contentsignin">ログイン後にコピー</div></div>🎜 上記は典型的な例です。実行結果が 5 つポップアップすることは誰もが知っており、この問題を解決するためにクロージャーを使用できることも知っています。なぜ毎回ポップアップが表示されるのか、なぜクロージャーでこの問題が解決できるのか理解できませんでした。少し検索した後、最終的にそれを理解しました: 🎜<p><strong>a.</strong> 先来分析没用闭包前的情况:for循环中,我们给每个li点击事件绑定了一个匿名函数,匿名函数中返回了变量i的值,当循环结束后,变量i的值变为5,此时我们再去点击每个li,也就是执行相应的匿名函数(看上面的代码),这是变量i已经是5了,所以每个点击弹出5. 因为这里返回的每个匿名函数都是引用了同一个变量i,如果我们新建一个变量保存循环执行时当前的i的值,然后再让匿名函数应用这个变量,最后再返回这个匿名函数,这样就可以达到我们的目的了,这就是运用闭包来实现的!</p><p><strong>b.</strong> 再来分析下运用闭包时的情况:</p><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="hljs javascript"> var oli = document.getElementsByTagName(&#39;li&#39;); var i; for(i = 0;i < 5;i++){
oli[i].onclick = (function(num){ var a = num; // 为了说明问题 return function(){
alert(a);
}
})(i)
} console.log(i); // 5</pre><div class="contentsignin">ログイン後にコピー</div></div><div class="contentsignin">ログイン後にコピー</div></div><p>这里for循环执行时,给点击事件绑定的匿名函数传递i后立即执行返回一个内部的匿名函数,因为参数是按值传递的,所以此时形参num保存的就是当前i的值,然后赋值给局部变量 a,然后这个内部的匿名函数一直保存着a的引用,也就是一直保存着当前i的值。 所以循环执行完毕后点击每个li,返回的匿名函数执行弹出各自保存的 a 的引用的值。</p><h2>4. 闭包的运用</h2><p>我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。</p><p><strong>1. 匿名自执行函数</strong></p><p>我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:</p><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="hljs javascript">//将全部li字体变为红色
(function(){ var els = document.getElementsByTagName(&#39;li&#39;);for(var i = 0,lng = els.length;i < lng;i++){
els[i].style.color = &#39;red&#39;;
}
})();</pre><div class="contentsignin">ログイン後にコピー</div></div><p>我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,<br/>因此els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!<br/>关键是这种机制不会污染全局对象。</p><p><strong>2. 实现封装/模块化代码</strong></p><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="hljs javascript">var person= function(){ //变量作用域为函数内部,外部无法访问 var name = "default"; return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}();console.log(person.name);//直接访问,结果为undefined
console.log(person.getName()); //default
person.setName("jozo");
console.log(person.getName()); //jozo</pre><div class="contentsignin">ログイン後にコピー</div></div><p><strong>3. 实现面向对象中的对象</strong><br/>这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,<br/>我们可以模拟出这样的机制。还是以上边的例子来讲:</p><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="hljs php">function Person(){ var name = "default"; return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var person1= Person();
print(person1.getName());
john.setName("person1");
print(person1.getName()); // person1
var person2= Person();
print(person2.getName());
jack.setName("erson2");
print(erson2.getName()); //person2</pre><div class="contentsignin">ログイン後にコピー</div></div><p>Person的两个实例person1 和 person2 互不干扰!因为这两个实例对name这个成员的访问是独立的 。</p><h2>5. 内存泄露及解决方案</h2><p><strong>垃圾回收机制</strong></p><p>说到内存管理,自然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收:标记清除 和 引用计数;</p><p><code>标记清除:
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;引用计数:
跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。
这个方式存在一个比较大的问题就是循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0 。早期的IE版本里(ie4-ie6)采用是计数的垃圾回收机制,闭包导致内存泄露的一个原因就是这个算法的一个缺陷。
我们知道,IE中有一部分对象并不是原生额javascript对象,例如,BOM和DOM中的对象就是以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数。因此,虽然IE的javascript引擎采用的是标记清除策略,但是访问COM对象依然是基于引用计数的,因此只要在IE中设计COM对象就会存在循环引用的问题!
举个栗子:
window.onload = function(){var el = document.getElementById("id"); el.onclick = function(){ alert(el.id); } }
这段代码为什么会造成内存泄露?
el.onclick= function () { alert(el.id); };
执行这段代码的时候,将匿名函数对象赋值给el的onclick属性;然后匿名函数内部又引用了el对象,存在循环引用,所以不能被回收;
解决方法:
window.onload = function(){var el = document.getElementById("id");var id = el.id; //解除循环引用 el.onclick = function(){ alert(id); } el = null; // 将闭包引用的外部函数中活动对象清除 }
优点:
可以让一个变量常驻内存 (如果用的多了就成了缺点
避免全局变量的污染
私有化变量
缺点
クロージャはそれを含む関数のスコープを運ぶため、他の関数よりも多くのメモリを占有します
メモリリークを引き起こします
以上がjs のクロージャの詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。