摘要:而外層的函數不能訪問內層的變量或函數,這樣的層層嵌套就形成了作用域鏈。閉包閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量。
閉包是js中一個極為NB的武器,但也不折不扣的成了初學者的難點。因為學好閉包就要學好作用域,正確理解作用域鏈,然而想做到這一點就要深入的理解函數,所以我們從函數說起。
函數的聲明和調用首先說明一下,本文基于原生js環境,不涉及DOM部分
最基本的就是函數的定義和調用,注意區分以下形式:
//以2下個是函數的定義 function func(){ //函數聲明 /*code*/ } var func = function(){ //函數表達式 /*code*/ }; //以下2個是函數的調用(執行) func(); //無法得到函數的返回值 var returnValue = func(); //執行函數并將返回值賦給returnValue, 如果函數沒有指定返回值,返回undefined //以下2各定義了立即執行函數 (function(){ /*code*/ })(); (function(){ /*code*/ }());
立即執行函數直接聲明一個匿名函數,立即使用,省得定義一個用一次就不用的函數,而且免了命名沖突的問題。如果寫為如下形式可獲得立即執行函的返回值。
var returnValue = (function(){return 1;}()); var returnValue = (function(){return 1;})();
除此之外,函數還有一種非常常見的調用方式——回調函數。將一個函數作為參數傳入另一個函數,并在這個函數內執行。比如下面這個形式
document.addEventListener("click", console.log, false);
理解了上面的部分,我們看一個典型的例子,好好理解一下函數的定義和調用的關系,這個一定要分清。下面這段代碼很具有代表性:
var arr = []; for(var i = 0; i < 10; i++){ arr[i] = function(){ return i; }; } for(var j = 0; j < arr.length; j++){ console.log(arr[j]() + " "); } //得到輸出:10 10 10 10 10 10 10 10 10 10
我們需要理解這里面第一個for循環其實相當于如下形式,它只是定義了10個函數,并把函數放在數組中,并沒有執行函數。由于js遵循詞法作用(lexical scoping), i是一個全局變量,所以第二個for循環調用函數的時候,i等于10
var i = 0; arr[0] = function(){ return i; }; i++; arr[1] = function(){ return i; }; i++; arr[2] = function(){ return i; }; i++; //......省略 arr[9] = function(){ return i; }; i++; //此時i == 10 循環結束
再講完了閉包我們再回來解決這個問題。
關于函數的參數傳遞這里就不多說了,值得強調的是,上述2種定義函數的方式是有區別的,想理解這個區別,先要理解聲明提前。
變量聲明提前這個地方簡單理解一下js的預處理過程。js代碼會在執行前進行預處理,預處理的時候會進行變量聲明提前,每個作用域的變量(用var聲明的變量,沒有用var聲明的變量不會提前)和函數定義會提前到這個作用域內的開頭。
函數中的變量聲明會提前到函數的開始,但初始化不會。比如下面這個代碼。因此我們應該避免在函數中間聲明變量,以增加代嗎的可讀性。
function(){ console.log(a); //undefined f(); //f called /*...*/ function f(){ console.log("f called"); } var a = 3; console.log(a); //3 }
這段代碼等于(并且瀏覽器也是這么做的):
function(){ function f(){ console.log("f called"); } var a; console.log(a); //undefined f(); //f called /*...*/ a = 3; console.log(a); //3 }不同函數定義方式的區別
第一個區別:
function big(){ func();//函數正常執行 func1();//TypeError: func1 is not a function function func(){ //這個函數聲明會被提前 console.log("func is called"); } var func1 = function(){ //這個函數聲明會被提前,但不是個函數,而是變量 console.log("func1 is called"); }; } big();
第二個區別,比較下面2段代碼
function f() { var b=function(){return 1;}; function b(){return 0;}; console.log(b()); console.log(a()); function a(){return 0;}; var a=function(){return 1;}; } f();
不難發現,用表達式定義的函數可以覆蓋函數聲明直接定義的函數;但是函數聲明定義的函數卻不能覆蓋表達式定義的函數。
實際中我們發現,定義在調用之前var f = function(){};會覆蓋function f(){},而定義在調用之后function f(){}會覆蓋var f= function(){};(你可以以不同順序組合交換上面代碼中的行,驗證這個結論)
第三個區別,其實這個算不上區別
var fun = function fun1(){ //內部可見:fun和fun1 console.log(fun1 === fun); }; //外部僅fun可見 fun(); //true 說明這是同一個對象的2各不同引用 fun1(); //ReferenceError: fun1 is not defined
此外還有一個定義方法如下:
var func = new Function("alert("hello")");
這個方式不常用,也不建議使用。因為它定義的函數都是在window中的,更嚴重的是,這里的代碼實在eval()中解析的,這使得這個方式很糟糕,帶來性能下降和安全風險。具體就不贅述了。
詞法作用域C++和Java等語言使用的都是塊級作用域,js與它們不同,遵循詞法作用域(lexical scoping)。講的通俗一些,就是函數定義決定變量的作用域函數內是一部分,函數外是另一部分,內部可以訪問外部的變量,但外部無法直接訪問內部的變量。首先我們看下面這個代碼
//這里是全局作用域 var a = 3; var b = 2; var c = 20; function f(){ //這里是一個局部作用域 var a = 12; //這是一個局部變量 b = 10; //覆蓋了全局變量 var d = e = 15; //只有第一參數d是局部變量,后面的都是全局變量 f = 13; //新的全局變量 console.log(a + " " + b + " " + d); } f(); //12 10 15 console.log(a); //3 console.log(b); //10 console.log(c); //20 console.log(d); //undefined console.log(e); //15 console.log(f); //13
注:原生js在沒有定使用義的變量時會得到undefined,并在使用過程中遵循隱式類型轉換,但現在的瀏覽器不會這樣,它們會直接報錯。不過在函數中使用滯后定義的變量依然是undefined,不會報錯,這里遵循聲明提前的原則。
這是一個最基本的作用域模型。我們上文提到過,函數里面可以訪問外面的變量,函數外部不能直接訪問內部的變量.
我們再看一個復雜一點的:
var g = "g"; function f1(a){ var b = "f1"; function f2(){ var c = "f2"; console.log(a + b + c + g); } f2(); } f1("g"); //gf1f2g
在js中,函數里面定義函數十分普遍,這就需要我們十分了解作用域鏈。
如下這個代碼定義了下圖中的作用域鏈:
var g = 10; function f1(){ var f_1 = "f1"; function f2(){ var f_2 = "f2"; function f3(){ var f_3 = "f3"; /*function f...*/ } } }
這里內層的函數可以由內向外查找外層的變量(或函數),當找到相應的變量(或函數)立即停止向外查找,并使用改變量(或函數)。而外層的函數不能訪問內層的變量(或函數),這樣的層層嵌套就形成了作用域鏈。
值得一提的是,函數的參數在作用于上相當于在函數內部第一行就聲明了的變量,注意這里指的僅僅是聲明,但不一定完成初始化,也就說明參數在沒有傳入值的時候值為undefined。
回調函數那么問題來了,在一個函數外部永遠不能訪問函數內部的變量嗎?答案是否定的,我們可以用回調函數實現這個過程:
function A(arg){ console.log(arg); } function B(fun){ var a = "i am in function B"; var i = 10; fun(a); } B(A); //i am in function B
上面這個過程對于B而言,只把自己內部的變量a給了fun,而外部的A無論如何也訪問不到B中的i變量,也就是說傳入的fun函數只能訪問B想讓它訪問的變量,因此回調函數這樣的設計可以在代碼的隔離和開放中間取得一個極好的平衡。
說句題外話:javascript特別適用于事件驅動編程,因為回調模式支持程序以異步方式運行。
好了,如果上面的你都看懂了,那么可以開始看閉包了。
閉包閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量。閉包主要是為了區分私有和公有的方法和變量,類似于c++和java中對象的public成員和protected成員。
一言以蔽之:作用域的嵌套構成閉包!
構成閉包以下幾個必要條件函數(作用域)嵌套函數
函數(作用域)內部可以引用外部的參數和變量
參數和變量不會被垃圾回收機制回收。可以查看: 內存管理與垃圾回收
閉包的優缺點優點
希望一個變量長期駐扎在內存中(如同c++中static局部變量)
避免全局變量的污染
私有成員的存在
缺點
閉包常駐內存,會增大內存使用量,大量使用影響程序性能。
使用不當很容易造成內存泄露(關于內存管理和垃圾回收的細節以后會專門講一篇的)。
一般函數執行完畢后,局部活動對象就被銷毀,內存中僅僅保存全局作用域。但閉包不會!
為什么有閉包我們考慮實現一個局部變量調用并自加的過程:
var a = 0; function fun(){ return a++; } fun(); //返回0 fun(); //返回1 fun(); //返回2 function func(){ var a = 0; return a++; } func(); //返回0 func(); //返回0 func(); //返回0
看了上面代碼你會發現,當a是全局變量的時候可以實現,但a成為了局部變量就不行了,當然,必須是閉包才可以實現這個功能:
var f = (function(){ var a = 0; return function(){ return a++; } })(); f(); //返回0 f(); //返回1 f(); //返回2
這樣不僅實現了功能,還防止了可能的全局污染。
上文舉了在循環內定義函數訪問循環變量的例子,可結果并不如意,得到了十個10,下面我們用閉包修改這個代碼,使它可以產生0~9:
var arr = []; for(var i = 0; i < 10; i++){ arr[i] = (function(i){ return function(){ return i; }; })(i); } for(var j = 0; j < arr.length; j++){ console.log(arr[j]()); }//這樣就可以得到0~9了
當然還以其他的解決方法:
//方法2 var arr = []; for(var i = 0; i < 10; i++){ arr[i] = console.log.bind(null, i); } for(var j = 0; j < arr.length; j++){ console.log(arr[j]()); //方法3 var arr = []; for(let i = 0; i < 10; i++){ arr[i] = function(){ console.log(i); }; } for(var j = 0; j < arr.length; j++){ console.log(arr[j]()); }//這樣也可以得到0~9了迭代器
好了,是時候放松一下了,看看下面這個代碼,這個會簡單一些
var inc = function(){ var x = 0; return function(){ console.log(x++); }; }; inc1 = inc(); inc1(); //0 inc1(); //1 inc2 = inc(); inc2(); //0 inc2(); //1 inc2 = null; //內存回收 inc2 = inc(); inc2(); //0
你會發現,inc返回了一個函數,這個函數是個累加器,它們可以獨立工作互補影響。這個就是js中迭代器next()的實現原理。下面是一個簡單的迭代器:
//實現對數組遍歷 function iterator(arr){ var num = 0; return { next: function(){ if(num < arr.length) return arr[num++]; else return null; } }; } var a = [1,3,5,7,9]; var it = iterator(a); var num = it.next() while(num !== null){ console.log(num) num = it.next(); }//依次輸出1,3,5,7,9
如果你學了ES6,那么你可以用現成的迭代器,就不用自定義迭代器了。
箭頭函數箭頭函數本身也是一個函數,具有自己的作用域。不過在箭頭函數里面的this上下文同函數定義所在的上下文,具體可以看我的另一篇文章:javascript中this詳解
典型實例這個實例會涉及到對象的相關知識,如果不能完全理解,可以參考:javascript中this詳解 和 javascript對象、類與原型鏈
function Foo() { getName = function () { console.log (1); }; return this; } Foo.getName = function () { console.log (2);}; Foo.prototype.getName = function () { console.log (3);}; var getName = function () { console.log (4);}; function getName() { console.log (5);} //請寫出以下輸出結果: Foo.getName(); //2, 函數的靜態方法,直接調用相關函數就可以了。 getName(); //4, 變量函數定義在調用之前,成功完成初始化,覆蓋函數聲明方式定義的同名函數 Foo().getName(); //1, 這里 Foo()返回的 this 是 window,在 Foo調用時,對全局的變量型函數 getName 重新定義了,所以得到1。 getName(); //1, 上一句改變了全局的 getName 函數為 cosnole.log(1) new Foo.getName(); //2,無參數 new 運算比 . 運算低,所以先運行 Foo.getName,得到2 new Foo().getName(); //3,有參數 new 運算和 . 運算同一等級,故從左到右,先運算 new Foo() 得到一個匿名對象,在該對象上調用getName 函數得到3 new new Foo().getName(); //3,同上,先得到匿名對象,然后將該對象的方法 getName 當做構造函數來調用,得到一個新對象,并輸出3;Curry化
Curry化技術是一種通過把多個參數填充到函數體中,實現將函數轉換為一個新的經過簡化的(使之接受的參數更少)函數的技術。當發現正在調用同一個函數時,并且傳遞的參數絕大多數都是相同的,那么用一個Curry化的函數是一個很好的選擇.
下面利用閉包實現一個curry化的加法函數
function add(x,y){ if(x && y) return x + y; if(!x && !y) throw Error("Cannot calculate"); return function(newx){ return x + newx; }; } add(3)(4); //7 add(3, 4); //7 var newAdd = add(5); newAdd(8); //13 var add2000 = add(2000); add2000(100); //2100
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97461.html
摘要:之前一篇文章我們詳細說明了變量對象,而這里,我們將詳細說明作用域鏈。而的作用域鏈,則同時包含了這三個變量對象,所以的執行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當前的函數調用棧,為當前正在被執行的函數的作用域鏈,為當前的局部變量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初學JavaScrip...
摘要:作用域是最重要的概念之一,想要學好就需要理解作用域和作用域鏈的工作原理。腳本錯誤腳本錯誤由此可以引發作用域鏈的概念在中,函數也是對象,實際上,里一切都是對象。當一個函數創建后,它的作用域鏈會被創建此函數的作用域中可訪問的數據對象填充。 作用域是JavaScript最重要的概念之一,想要學好JavaScript就需要理解JavaScript作用域和作用域 鏈的工作原理。 1. 全局作...
摘要:作用域是最重要的概念之一,想要學好就需要理解作用域和作用域鏈的工作原理。腳本錯誤腳本錯誤由此可以引發作用域鏈的概念在中,函數也是對象,實際上,里一切都是對象。當一個函數創建后,它的作用域鏈會被創建此函數的作用域中可訪問的數據對象填充。 作用域是JavaScript最重要的概念之一,想要學好JavaScript就需要理解JavaScript作用域和作用域 鏈的工作原理。 1. 全局作...
摘要:作用域是最重要的概念之一,想要學好就需要理解作用域和作用域鏈的工作原理。腳本錯誤腳本錯誤由此可以引發作用域鏈的概念在中,函數也是對象,實際上,里一切都是對象。當一個函數創建后,它的作用域鏈會被創建此函數的作用域中可訪問的數據對象填充。 作用域是JavaScript最重要的概念之一,想要學好JavaScript就需要理解JavaScript作用域和作用域 鏈的工作原理。 1. 全局作...
摘要:在的開發者工具中,通過斷點調試,我們能夠非常方便的一步一步的觀察的執行過程,直觀感知函數調用棧,作用域鏈,變量對象,閉包,等關鍵信息的變化。其中表示當前的局部變量對象,表示當前作用域鏈中的閉包。 showImg(https://segmentfault.com/img/remote/1460000008404321); 在前端開發中,有一個非常重要的技能,叫做斷點調試。 在chrome...
摘要:圖片中的作用域鏈,是全局執行環境中的作用域鏈。然后此活動對象被推入作用域鏈的最前端。在最后調用的時候,創建先構建作用域鏈,再創建執行環境,再創建執行環境的時候發現了一個變量標識符。 從圖書館翻過各種JS的書之后,對作用域/執行環境/閉包這些概念有了一個比較清晰的認識。 栗子說明一切 第一個栗子 來看一個來自ECMA-262的栗子: var x = 10; (function foo(...
閱讀 1845·2021-08-19 11:12
閱讀 1422·2021-07-25 21:37
閱讀 985·2019-08-30 14:07
閱讀 1264·2019-08-30 13:12
閱讀 649·2019-08-30 11:00
閱讀 3526·2019-08-29 16:28
閱讀 987·2019-08-29 15:33
閱讀 2965·2019-08-26 13:40