來看一些關於閉包的定義:
#閉包是指有權存取另一個函數作用域中變數的函數- -《JS高階程式設計第三版》 p178
函數物件可以透過作用域鏈相關聯起來,函數體內部的變數都可以保存在函數作用域內,這種特性稱為'閉包' 。 --《JS權威指南》 p183
內部函數可以存取定義它們的外部函數的參數和變數(除了this
和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.
sayName函數的作用域,也就是只有兩個變數對象,當執行到對應的執行環境時,變數物件會變成活動對象,並被推入到執行環境作用域鏈的前端,也就是成為優先順序最高的那個。 看圖片說話:
這圖在JS高階程式設計書上也有,我重新繪了遍。
在創建sayName()函數時,會建立一個預先包含變數物件的作用域鏈,也就是圖中索引為1的作用域鏈,並且被儲存到內部的[[Scope]]屬性中,當呼叫sayName()函數的時候,會建立一個執行環境,然後透過複製函數的[[Scope]]屬性中的物件建立起作用域鏈,此後,又有一個活動物件(圖中索引為0 )被創建,並被推入執行環境作用域鏈的前端。
一般來說,當函數執行完畢後,局部活動物件就會被銷毀,記憶體中只保存全域作用域。但是,閉包的情況又有所不同:
再來看看看閉包的作用域鏈:
function sayName(name){return function(){return name; } }var say = sayName('jozo');
這個閉包實例比上一個例子多了一個匿名函數的作用域:
在匿名函式從sayName()函式中被傳回後,它的作用域鏈被初始化為包含sayName()函數的活動物件和全域變數物件。這樣,匿名函數就可以存取在sayName()中定義的所有變數和參數,更為重要的是,sayName()函數在執行完畢後,其活動物件也不會被銷毀,因為匿名函數的作用域鏈依然在引用這個活動對象,換句話說,sayName()函數執行完後,其執行環境的作用域鏈會被銷毀,但他的活動對象會留在記憶體中,知道匿名函數會銷毀。這個也是後面要講到的記憶體外洩的問題。
作用域鏈問題不寫那麼多了,寫書上的東西也很累o(╯□╰)o
實例1:實現累加
// 方式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更加优雅,也减少全局变量,将变量私有化
實例2 :給每個li添加點擊事件
var oli = document.getElementsByTagName('li'); var i; for(i = 0;i < 5;i++){ oli[i].onclick = function(){ alert(i); } } console.log(i); // 5 //执行匿名函数 (function(){ alert(i); //5 }());
上面是一個經典的例子,我們都知道執行結果是都彈出5,也知道可以用閉包解決這個問題,但是我剛開始始終不能明白為什麼每次彈出都是5,為什麼閉包可以解決這個問題。後來捋一捋還是把它弄清晰了:
a. 先来分析没用闭包前的情况:for循环中,我们给每个li点击事件绑定了一个匿名函数,匿名函数中返回了变量i的值,当循环结束后,变量i的值变为5,此时我们再去点击每个li,也就是执行相应的匿名函数(看上面的代码),这是变量i已经是5了,所以每个点击弹出5. 因为这里返回的每个匿名函数都是引用了同一个变量i,如果我们新建一个变量保存循环执行时当前的i的值,然后再让匿名函数应用这个变量,最后再返回这个匿名函数,这样就可以达到我们的目的了,这就是运用闭包来实现的!
b. 再来分析下运用闭包时的情况:
var oli = document.getElementsByTagName('li'); 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
这里for循环执行时,给点击事件绑定的匿名函数传递i后立即执行返回一个内部的匿名函数,因为参数是按值传递的,所以此时形参num保存的就是当前i的值,然后赋值给局部变量 a,然后这个内部的匿名函数一直保存着a的引用,也就是一直保存着当前i的值。 所以循环执行完毕后点击每个li,返回的匿名函数执行弹出各自保存的 a 的引用的值。
我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。
1. 匿名自执行函数
我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:
//将全部li字体变为红色 (function(){ var els = document.getElementsByTagName('li');for(var i = 0,lng = els.length;i < lng;i++){ els[i].style.color = 'red'; } })();
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,
因此els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!
关键是这种机制不会污染全局对象。
2. 实现封装/模块化代码
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
3. 实现面向对象中的对象
这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,
我们可以模拟出这样的机制。还是以上边的例子来讲:
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
Person的两个实例person1 和 person2 互不干扰!因为这两个实例对name这个成员的访问是独立的 。
垃圾回收机制
说到内存管理,自然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收:标记清除 和 引用计数;
标记清除:
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 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中文網其他相關文章!