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