圖解Javascript——作用域、作用域鏈、閉包
什么是作用域?
作用域是一種規(guī)則,在代碼編譯階段就確定了,規(guī)定了變量與函數(shù)的可被訪問的范圍。全局變量擁有全局作用域,局部變量則擁有局部作用域。 js是一種沒有塊級作用域的語言(包括if、for等語句的花括號代碼塊或者單獨的花括號代碼塊都不能形成一個局部作用域),所以js的局部作用域的形成有且只有函數(shù)的花括號內(nèi)定義的代碼塊形成的,既函數(shù)作用域。
什么是作用域鏈?
作用域鏈是作用域規(guī)則的實現(xiàn),通過作用域鏈的實現(xiàn),變量在它的作用域內(nèi)可被訪問,函數(shù)在它的作用域內(nèi)可被調(diào)用。
作用域鏈是一個只能單向訪問的鏈表,這個鏈表上的每個節(jié)點就是執(zhí)行上下文的變量對象(代碼執(zhí)行時就是活動對象),單向鏈表的頭部(可被第一個訪問的節(jié)點)始終都是當前正在被調(diào)用執(zhí)行的函數(shù)的變量對象(活動對象),尾部始終是全局活動對象。
作用域鏈的形成?
我們從一段代碼的執(zhí)行來看作用域鏈的形成過程。
function fun01 () { console.log('i am fun01...'); fun02(); } function fun02 () { console.log('i am fun02...'); } fun01();
數(shù)據(jù)訪問流程
如上圖,當程序訪問一個變量時,按照作用域鏈的單向訪問特性,首先在頭節(jié)點的AO中查找,沒有則到下一節(jié)點的AO查找,最多查找到尾節(jié)點(global AO)。在這個過程中找到了就找到了,沒找到就報錯undefined。
延長作用域鏈
從上面作用域鏈的形成可以看出鏈上的每個節(jié)點是在函數(shù)被調(diào)用執(zhí)行是向鏈頭unshift進當前函數(shù)的AO,而節(jié)點的形成還有一種方式就是“延長作用域鏈”,既在作用域鏈的頭部插入一個我們想要的對象作用域。延長作用域鏈有兩種方式:
1.with語句
function fun01 () { with (document) { console.log('I am fun01 and I am in document scope...') } } fun01();
2.try-catch語句的catch塊
function fun01 () { try { console.log('Some exceptions will happen...') } catch (e) { console.log(e) } } fun01();
ps:個人感覺with語句使用需求不多,try-catch的使用也是看需求的。個人對這兩種使用不多,但是在進行這部分整理過程中萌發(fā)了一點點在作用域鏈層面的不成熟的性能優(yōu)化小建議。
由作用域鏈引發(fā)的關(guān)于性能優(yōu)化的一點不成熟的小建議
1.減少變量的作用域鏈的訪問節(jié)點
這里我們自定義一個名次叫做“查找距離”,表示程序訪問到一個非undefined變量在作用域鏈中經(jīng)過的節(jié)點數(shù)。因為如果在當前節(jié)點沒有找到變量,就跳到下一個節(jié)點查找,還要進行判斷下一個節(jié)點中是否存在被查找變量。“查找距離”越長,要做的“跳”動作和“判斷”動作也就越多,資源開銷就越大,從而影響性能。這種性能帶來的差距可能少數(shù)的幾次變量查找操作不會帶來太多性能問題,但如果是多次進行變量查找,性能對比則比較明顯了。
(function(){ console.time() var find = 1 //這個find變量需要在4個作用域鏈節(jié)點進行查找 function fun () { function funn () { var funnv = 1; var funnvv = 2; function funnn () { var i = 0 while(i <= 100000000){ if(find){ i++ } } } funnn() } funn() } fun() console.timeEnd() })()
(function(){ console.time() function fun () { function funn () { var funnv = 1; var funnvv = 2; function funnn () { var i = 0 var find = 1 //這個find變量只在當前節(jié)點進行查找 while(i <= 100000000){ if(find){ i++ } } } funnn() } funn() } fun() console.timeEnd() })()
在mac pro的chrome瀏覽器下做實驗,進行1億次查找運算。
實驗結(jié)果:前者運行5次平均耗時85.599ms,后者運行5次平均耗時63.127ms。
2.避免作用域鏈內(nèi)節(jié)點AO上過多的變量定義
過多的變量定義造成性能問題的原因主要是查找變量過程中的“判斷”操作開銷較大。我們使用with來進行性能對比。
(function(){ console.time() function fun () { function funn () { var funnv = 1; var funnvv = 2; function funnn () { var i = 0 var find = 10 with (document) { while(i <= 1000000){ if(find){ i++ } } } } funnn() } funn() } fun() console.timeEnd() })()
在mac pro的chrome瀏覽器下做實驗,進行100萬次查找運算,借助with使用document進行的延長作用域鏈,因為document下的變量屬性比較多,可以測試在多變量作用域鏈節(jié)點下進行查找的性能差異。
實驗結(jié)果:5次平均耗時558.802ms,而如果刪掉with和document,5次平均耗時0.956ms。
當然,這兩個實驗是在我們假設(shè)的極端環(huán)境下進行的,結(jié)果僅供參考!
關(guān)于閉包
1.什么是閉包?
函數(shù)對象可以通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)體內(nèi)的數(shù)據(jù)(變量和函數(shù)聲明)都可以保存在函數(shù)作用域內(nèi),這種特性在計算機科學(xué)文獻中被稱為“閉包”。既函數(shù)體內(nèi)的數(shù)據(jù)被隱藏于作用于鏈內(nèi),看起來像是函數(shù)將數(shù)據(jù)“包裹”了起來。從技術(shù)角度來說,js的函數(shù)都是閉包:函數(shù)都是對象,都關(guān)聯(lián)到作用域鏈,函數(shù)內(nèi)數(shù)據(jù)都被保存在函數(shù)作用域內(nèi)。
2.閉包的幾種實現(xiàn)方式
實現(xiàn)方式就是函數(shù)A在函數(shù)B的內(nèi)部進行定義了,并且當函數(shù)A在執(zhí)行時,訪問了函數(shù)B內(nèi)部的變量對象,那么B就是一個閉包。如下:
如上兩圖所示,是在chrome瀏覽器下查看閉包的方法。兩種方式的共同點是都有一個外部函數(shù)outerFun(),都在外部函數(shù)內(nèi)定義了內(nèi)部函數(shù)innerFun(),內(nèi)部函數(shù)都訪問了外部函數(shù)的數(shù)據(jù)。不同的是,第一種方式的innerFun()是在outerFun()內(nèi)被調(diào)用的,既聲明和被調(diào)用均在同一個執(zhí)行上下文內(nèi)。而第二種方式的innerFun()則是在outerFun()外被調(diào)用的,既聲明和被調(diào)用不在同一個執(zhí)行上下文。第二種方式恰好是js使用閉包常用的特性所在:通過閉包的這種特性,可以在其他執(zhí)行上下文內(nèi)訪問函數(shù)內(nèi)部數(shù)據(jù)。
我們更常用的一種方式則是這樣的:
//閉包實例 function outerFun () { var outerV1 = 10 function outerF1 () { console.log('I am outerF1...') } function innerFun () { var innerV1 = outerV1 outerF1() } return innerFun //return回innerFun()內(nèi)部函數(shù) } var fn = outerFun() //接到return回的innerFun()函數(shù) fn() //執(zhí)行接到的內(nèi)部函數(shù)innerFun()
此時它的作用域鏈是這樣的:
3.閉包的好處及使用場景
js的垃圾回收機制可以粗略的概括為:如果當前執(zhí)行上下文執(zhí)行完畢,且上下文內(nèi)的數(shù)據(jù)沒有其他引用,則執(zhí)行上下文pop出call stack,其內(nèi)數(shù)據(jù)等待被垃圾回收。而當我們在其他執(zhí)行上下文通過閉包對執(zhí)行完的上下文內(nèi)數(shù)據(jù)仍然進行引用時,那么被引用的數(shù)據(jù)則不會被垃圾回收。就像上面代碼中的outerV1,放我們在全局上下文通過調(diào)用innerFun()仍然訪問引用outerV1時,那么outerFun執(zhí)行完畢后,outerV1也不會被垃圾回收,而是保存在內(nèi)存中。另外,outerV1看起來像不像一個outerFun的私有內(nèi)部變量呢?除了innerFun()外,我們無法隨意訪問outerV1。所以,綜上所述,這樣閉包的使用情景可以總結(jié)為:
(1)進行變量持久化。
(2)使函數(shù)對象內(nèi)有更好的封裝性,內(nèi)部數(shù)據(jù)私有化。
進行變量持久化方面舉個栗子:
我們假設(shè)一個需求時寫一個函數(shù)進行類似id自增或者計算函數(shù)被調(diào)用的功能,普通青年這樣寫:
var count = 0 function countFun () { return count++ }
這樣寫固然實現(xiàn)了功能,但是count被暴露在外,可能被其他代碼篡改。這個時候閉包青年就會這樣寫:
function countFun () { var count = 0 return function(){ return count++ } } var a = countFun() a()
這樣count就不會被不小心篡改了,函數(shù)調(diào)用一次就count加一次1。而如果結(jié)合“函數(shù)每次被調(diào)用都會創(chuàng)建一個新的執(zhí)行上下文”,這種count的安全性還有如下體現(xiàn):
function countFun () { var count = 0 return { count: function () { count++ }, reset: function () { count = 0 }, printCount: function () { console.log(count) } } } var a = countFun() var b = countFun() a.count() a.count() b.count() b.reset() a.printCount() //打?。? 因為a.count()被調(diào)用了兩次 b.printCount() //打印出:0 因為調(diào)用了b.reset()
以上便是閉包提供的變量持久化和封裝性的體現(xiàn)。
4.閉包的注意事項
由于閉包中的變量不會像其他正常變量那種被垃圾回收,而是一直存在內(nèi)存中,所以大量使用閉包可能會造成性能問題。
以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對大家的學(xué)習或者工作能帶來一定的幫助,同時也希望多多支持腳本之家!
相關(guān)文章
JavaScript生成指定范圍隨機數(shù)和隨機序列的方法
這篇文章主要介紹了JavaScript生成指定范圍隨機數(shù)和隨機序列,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2018-05-05解決option標簽selected="selected"屬性失效的問題
下面小編就為大家?guī)硪黄鉀Qoption標簽selected="selected"屬性失效的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望對大家有所幫助2017-11-11echarts使用中關(guān)于y坐標軸無法正常顯示的問題解決記錄
Echarts是由百度提供的數(shù)據(jù)可視化解決方案,下面這篇文章主要給大家介紹了關(guān)于echarts使用中關(guān)于y坐標軸無法正常顯示的問題解決記錄,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2023-12-12