let和閉包
之前一直模模糊糊記得,let解決了某個閉包問題,想用時又不敢肯定,今天終於遇到這個問題了,那我們就一起來分析一下,什麼是let,let有什麼作用,以及,他是如何解決閉包的,當然,也順便好好聊聊閉包。
1、閉包
1.1 閉包的定義
閉包的定義是這樣的:內部函式被儲存到了外部,即為閉包
先來看一個簡單的例子:
//我們宣告一個函式test(),這個函式返回了一個function
function test(){
var i = 0;
return function(){
console.log(i++)
}
};
//把test()的返回值賦給a和b變數,所以其實這時候的a/b=function(){console.log(i++);}
var a = test();
var b = test();
//依次執行a,a,b,控制檯會輸出什麼呢?
a();a();b();
先思考一下,然後去瀏覽器驗證一下
答案是:0,1,0
這是因為,a/b=test()時,a/b各自保留了test的AO,所以各自上面均有一個i=0;
1.2 實現共有變數
這樣其實是實現了一個共有變數,比如我們把上面的程式碼稍稍調整一下,就實現了一個累加計數器;
//累加器
function add(){
var count = 0;
function demo(){
count++;
console.log(count);
}
return demo;
}
var counter = add();
counter();
這也是閉包的第一個功能,實現共有變數;
1.3 可以做快取
閉包的第二個功能是可以用作快取,比如下面這個例子,我們用push把準備用到的東西放進去,當eat呼叫時使用:
//隱式快取應用
function eater(){
var food = "";
var obj = {
eat: function(){
console.log("I'm eating " + food);
food = "";
},
push: function(myFood){
food = myFood;
}
}
return obj;
}
var eater1 = eater();
eater1.push('banana');
eater1.eat();
1.4 可以實現封裝,屬性私有化
這個典型的例子是聖盃繼承實現的雅虎的寫法
雅虎寫法
var inherit = (function(){
var F = function(){};
return function(Target,Origin){
F.prototype = Origin.prototype;
Target.prototype = new F();
Target.prototype.constructor = Target;
//超類
Target.prototype.uber = Origin.prototype;
}
}())
//理解,return時保留了F變數,閉包私有化變數
1.5 模組化開發,防止汙染全域性變數
利用閉包變數私有化,避免名稱空間的問題
var name = "heh";
var init = (function(){
var name = "zhangsan";
function callName(){
console.log(name);
}
return function (){
callName();
}
}())
init();
1.6 閉包的危害
以上四點,其實都是閉包的好處,善加利用是能夠幫助到我們的,所以大家不要先入為主覺得閉包是不好的,閉包其實是我們解決很多問題的一種思路,但當然,閉包確實有它危害的方面:
閉包會導致原有作用域鏈不釋放,造成記憶體洩漏,即佔用導致剩下記憶體變少
1.如何清除閉包:
閉包函式=null;
如第一個例子:a = null;
否則閉包會一直佔用記憶體,直到瀏覽器程式結束。
2.閉包會在父函式外部,改變父函式內部變數的值。
如1.3的例子,我們修改了內部的food值,所以,對於閉包,一定要小心使用。
3.還有一個最常見的情況是for迴圈中的閉包:
我們寫一個ul列表,當點選時輸出對應的i;
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<script>
var items = document.getElementsByTagName('li');
var len = items.length;
for (var i = 0; i < len; i++) {
items[i].onclick = function () {
console.log(i);
}
}
</script>
</body>
這和我們之前事件委託的例子很像,但是這裡我們輸出的不是對應的this物件,而是函式所在作用域的i值,可以看到,我們輸出的都是4,而我們的i應該是從0到1、2、3,加到4的時候已經不滿足條件了,不會進入迴圈。
這到底是怎麼回事呢?
這同樣形成了一個閉包,內部的函式console.log(i)被儲存到了外部的items[i].onclick()之中,所以我們有一個外部的AO,裡面儲存了一個i,但是這個i是for迴圈執行完之後的i,當我們執行點選函式時,始終用到的就是這個i,但這明顯和我們要的不一樣,我們希望每一個執行點選時輸出的都是for迴圈時對應的那個i;
這時候的閉包,是存在一定問題的,利用立即執行函式可以解決這個問題。
2、立即執行函式
立即執行函式,顧名思義,立即會執行的函式(Immediately-Invoked Function Expression),即當js讀取到該函式,會立即執行。
我們1.4、1.5對應的例子中就用到了這個方法。
用法如下:
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<script>
var items = document.getElementsByTagName('li');
var len = items.length;
for (var i = 0; i < len; i++) {
items[i].onclick = (function () {
var index = i;
return function () {
console.log(index);
}
}())
}
</script>
</body>
每次到了立即執行函式時,都會把當前的i賦值給index儲存起來,並返回帶有這個值的函式。
2.1 立即執行函式的寫法
官方的兩種寫法
(function (){}());//w3c建議第一種
(function (){})();
2.2 立即執行函式用於初始化
<!--針對初始化功能的函式-->
var num = (
function (b) {
var a = 123;
console.log(a,b);
d = a + b;
return d;
}(2)
)
<!--返回值賦值給num-->
<!--執行完就被釋放-->
2.3 常見寫法的注意事項
//1.只有表示式才能被執行符號執行,會忽略表示式的名字
//2.我們正常函式執行的寫法如下
function test(){
};
test();
// 但是直接在函式宣告後接執行符號,是不可以的,會報語法錯誤
function test(){
}();
//3.凡是能變成表示式就能被執行
var test = function () {
}();
// 執行一次後被永久銷燬
// 表示式部分 = function(){
// }()
//4.最先識別哪個括號
(--這種寫法最外面先
function test(){}()
--);
(--這種寫法最前面先
function test(){}
--)
();
//可以沒有test名稱
//5.當有引數時,不報錯,但也不執行
function test(a,b,c,d){
console.log(a+b+c+d);
}(1,2,3,4);
實際分成了兩個部分
function test(a,b,c,d){
console.log(a+b+c+d);}
和
(1,2,3,4);//4,輸出個數
2.4 作用
透過定義一個匿名函式,建立了一個新的函式作用域,相當於建立了一個“私有”的名稱空間,該名稱空間的變數和方法,不會破壞汙染全域性的名稱空間。此時若是想訪問外部物件,將外部物件以引數形式傳進去即可。
3、let
還是上面那個問題,我們看看下面的程式碼
// let
for (let i = 0; i < len; i++) {
items[i].onclick = function () {
console.log(i);
}
}
和我們第一個版本一樣,只是把var宣告的i換成了let的宣告方式,但是結果已經沒有任何問題了。
為什麼僅僅改動了這一點,就解決了我們之前的問題呢?
其實這個問題的本質原因,還是var帶來的作用域的問題,接下來,我們來看一看,let,到底是什麼?
3.1 什麼是let
let語句,宣告一個塊範圍變數。
let是ES6中新增關鍵字。它的作用類似於var,用來宣告變數,用法也類似,但是let是存在塊級作用域的。
let variable1 = value1;
3.2 特性
1.使用 let 語句宣告一個變數,該變數的範圍限於宣告它的塊中。不能在外部訪問該變數,可以在宣告變數時為變數賦值,也可以稍後在指令碼中給變數賦值。
var l = 10;
{//注意這裡僅僅是一個塊級作用域
let l = 2;
console.log(l);// 這裡 l = 2.
let m = 4;
console.log(m);// 這裡 m = 4.
}
console.log(l);// 這裡 l = 10.
console.log(m);//報錯
相反,對於var,最小級別是函式作用域的。
所以在for迴圈中,var宣告的i是所在函式的作用域,而let則是for迴圈及迴圈體內的作用域,所以裡面的語句可以訪問到對應的i。
2.使用 let 宣告的變數,在宣告前無法使用,否則將會導致錯誤。(不存在變數提升了)
console.log(index);
let index;
3.如果未在 let 語句中初始化您的變數,則將自動為其分配 JavaScript 值 undefined。
let index;
console.log(index);
3.3 解讀for迴圈中的let
for (let i = 0; i < len; i++) {
items[i].onclick = function () {
console.log(i);
}
}
對於var來說,形成的閉包始終獲取到的都是迴圈完成後被改變的最終的i
而對於let,每一個i都是獨立在當前塊級作用域的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數。而JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。
其實,所謂的for迴圈帶來的閉包問題,其實就是變數作用域的問題,解決方式很多種,基本上可以用立即執行函式和let變數宣告來解決,其次,具體情具體分析。
推薦閱讀
http://web.jobbole.com/82520/
https://www.sogou.com/link?ur...
http://hao.jser.com/archive/5...
https://msdn.microsoft.com/li...
http://www.jb51.net/article/2...
https://segmentfault.com/a/11...