如何延长作用域链_第4部分2:作用域(链)和 闭包
知識列表作用域/作用域鏈 閉包(涉及JS垃圾回收機(jī)制 )https://zhuanlan.zhihu.com/p/27110726 【 js 基礎(chǔ) 】【讀書筆記】作用域和閉包https://juejin.im/post/5afb0ae56fb9a07aa2138425 深入理解閉包之前置知識→作用域與詞法作用域
(一)前置知識
一、作用域和作用域鏈
前言: 在記錄作用域和作用域鏈知識,我一度陷入了錯誤理解的邊緣 ,它也是之后理解閉包知識非常重要的前置知識。查了資料,做了習(xí)題,問了老師驗(yàn)證了自己的理解,終于輸出了一些我自己的理解。部分知識的表述有引用如有不妥之處,請老司機(jī)拍參考資料:lce_shou《深入理解閉包之前置知識→作用域與詞法作用域》
1、什么是作用域
先看這段代碼:
function fn() {var a = 'miya';console.log(a); // 輸出"miya" } fn(); //miya在fn 執(zhí)行的時候,輸出一個 a 變量 ,那么這個a變量是哪里來?有看到函數(shù)第一行有 定義 a變量的代碼var a = 'miya'
繼續(xù)看另外一段代碼:
var b = 'programmer'; function fn() {console.log(b); // 輸出"programmer" } fn(); //programmer同樣,在輸出 b 時,自己函數(shù)內(nèi)部沒有找到變量 b ,那么就 在外層的全局中查找 ,找到了就停止查找并輸出結(jié)果。
究竟是啥回事呢?慢慢道來: 那么,可以注意到以上兩段代碼都有查找變量。第一段代碼是在函數(shù)中找到a變量,第二段代碼是在全局中找到b變量。 當(dāng)然也要注意:JavaScript 中 { } 并沒有帶來塊級作用域,如:
{var a=1 } console.log(a) //1 即使這樣的定義同樣能輸出結(jié)果 1JavaScript 的作用域是 通過 函數(shù) 來形成,也就是說一個函數(shù)內(nèi)定義的變量,函數(shù)外是不可以訪問。
例子如下:
function fn(){var a =1; } //js編譯器從此函數(shù)作用域出來之后,外界的變量函數(shù)或者聲明變量均與之無關(guān) fn(); console.log(a); //"ReferenceError: a is not defined" 說明在全局作用域中變量未被聲明接下來,我們在函數(shù)、全局,兩個概念名分別都加上作用域三個字,是不是又打開一個新世界了? 關(guān)注細(xì)節(jié)的就知道,作用域,本質(zhì)是一套規(guī)則,用于 確定在何處 以及 如何查找變量(標(biāo)識符)的規(guī)則。關(guān)鍵點(diǎn)在于:查找變量(或標(biāo)識符)。接下來讓我們繼續(xù)探索 作用域鏈 吧。
2、作用域鏈
還是看剛才這段代碼:
var b = 'programmer'; function fn() {console.log(b); // 輸出"programmer" } fn();一般來說,我們在查找 b 變量時,先在函數(shù)作用域中 查找,沒有找到,再去 全局作用域中 查找。你會注意到,這是一個往外層查找的過程,即順著一條鏈條 從下往上查找變量 。這條鏈條,我們就稱之為作用域鏈。
1、全局作用域 從 JS 頁面中同時存在 函數(shù)fn 和 變量a 所處的位置來看。在頁面里所寫的代碼 都是出于 一個全局作用域下。
全局作用域,相當(dāng)于頁面上有一個含有聲明 變量a 或者 函數(shù)fn 的window對象。所聲明的全局變量都是window對象下對應(yīng)的一個屬性。
還沒有接觸到 ES6 的 let、const 之前,只有函數(shù)作用域和全局作用域。函數(shù)作用域肯定是在全局作用域里面的,而函數(shù)作用域中又可以繼續(xù)嵌套函數(shù)作用域,如:
那么代碼則是:
二、從面試題解析作用域和作用域鏈
1、解密原理
- 每當(dāng)執(zhí)行完一塊 作用域里的函數(shù)后,它就進(jìn)入一個新的作用域下(一般從下往上找)
- 當(dāng)你使用一個變量 或者 給一個變量賦值時,變量是從當(dāng)前的作用域先找,再從上層作用域找
2、具體運(yùn)用
var name = 'iceman'這段小小的js代碼有其編譯過程,經(jīng)歷了下面的步驟: A、編譯器在當(dāng)前作用域中 聲明一個變量name B、運(yùn)行時引擎在作用域中查找該變量,找到了name變量并為其賦值 編譯器工作,在代碼執(zhí)行前從上到下進(jìn)行編譯,當(dāng)遇到某個用 var 聲明變量時,先檢查在當(dāng)前作用域下是否存在該變量。如存在,則忽略這個聲明;如果不存在,則在當(dāng)前作用域中聲明該變量。 以下例子則證明以上說法:
console.log(name); // 輸出undefined var name = 'miya';在var name = 'miya'的上一行已輸出 name變量,并沒有報(bào)錯,輸出undefined,說明輸出時該變量已存在,只是沒有賦值而已。【這段很重要】
查找的規(guī)則:是從當(dāng)前作用域開始找,如果沒找到再到父級作用域中找,一層層往外找,如果在全局作用域如果還沒找到的話,就會報(bào)錯了:ReferenceError: 某變量 is not defined
(1)第一題:
var a = 1 function fn1(){ function fn2(){console.log(a)}function fn3(){var a = 4fn2()}var a = 2 return fn3 } var fn = fn1() fn() //輸出多少//輸出a=2 //執(zhí)行fn2函數(shù),fn2找不到變量a,接著往上在找到創(chuàng)建當(dāng)前fn2所在的作用域fn1中找到a=2(2)第二題:
var a = 1 function fn1(){function fn3(){ var a = 4fn2() }var a = 2return fn3 }function fn2(){console.log(a) } var fn = fn1() fn() //輸出多少//輸出a=1 //最后執(zhí)行fn2函數(shù),fn2找不到變量a,接著往上在找到創(chuàng)建當(dāng)前fn2所在的全局作用域中找到a=1(3)【重點(diǎn)】第三題:
var a = 1 function fn1(){function fn3(){function fn2(){console.log(a)}var afn2()a = 4} var a = 2return fn3 } var fn = fn1() fn() //輸出多少//輸出undefined //函數(shù)fn2在執(zhí)行的過程中,先從自己內(nèi)部找變量找不到,再從創(chuàng)建當(dāng)前函數(shù)所在的作用域fn去找,注意此時變量聲明前置,a已聲明但未初始化為undefined事實(shí)上,我發(fā)現(xiàn)上一節(jié)函數(shù)中 立刻執(zhí)行的函數(shù)表達(dá)式 本質(zhì)上也可以感受一下局部作用域和全局作用域的區(qū)別。
(4)再深入看幾道網(wǎng)上的經(jīng)典題,感受一下已經(jīng)模糊的智商~ (其實(shí)也還好啦,認(rèn)真點(diǎn)還是并沒有對智商多大的撞擊) 在作用域鏈中查找過程的偽代碼
第1道題
var x = 10 bar() function foo() {console.log(x) } function bar(){var x = 30foo() }/* 第2行,bar()調(diào)用bar函數(shù) 第6行,bar函數(shù)里面調(diào)用foo函數(shù) 第3行,foo函數(shù)從自己的局部環(huán)境里找x,結(jié)果沒找到 第1行,foo函數(shù)從上一級環(huán)境里找x,即從全局環(huán)境里找x,找到了var x=10。 foo()的輸出結(jié)果為10。 */第2道題
var x = 10; bar() //30 function bar(){var x = 30;function foo(){console.log(x) }foo(); } /* 第2行,bar()調(diào)用bar函數(shù) 第3行,bar函數(shù)里面是foo函數(shù) 第4行,foo函數(shù)在自己的局部環(huán)境里尋找x,沒找到。 foo函數(shù)到自己的上一級環(huán)境,即bar函數(shù)的局部環(huán)境里找x,找到var x = 30 所以第2行的bar()輸出為30 */第3道題
var x = 10; bar() function bar(){var x = 30;(function (){console.log(x)})() } /* 第2行,bar()調(diào)用bar函數(shù) 第三行,bar函數(shù)里的function()在自己的局部環(huán)境里尋找x,但沒找到 function()在上級環(huán)境即bar的局部環(huán)境里尋找x,找到var x =30,于是顯示結(jié)果為30 */二、補(bǔ)充作用域下標(biāo)識符的查找規(guī)則
- 函數(shù)在執(zhí)行的過程中,先從自己內(nèi)部中找聲明過的變量。如果找不到,下一步
- 從創(chuàng)建當(dāng)前函數(shù)所在的作用域(詞法作用域)去找, 以此往外。注意,如果找不到則為undefined或報(bào)錯
- 注意找的是變量的當(dāng)前的狀態(tài)
看完了作用域和作用域鏈知識,我們有必要了解一下JS編譯過程,JavaScript是有編譯過程。
先從這段簡單的代碼開始:var name = 'iceman',它的編譯過程其實(shí)有兩個動作:
- 編譯器在當(dāng)前作用域中聲明一個變量name
- 運(yùn)行時引擎在作用域中查找該變量,找到了name變量并為其賦值
證明以上的說法:
console.log(name); // 輸出undefined var name = 'iceman';在var name = 'iceman'的上一行輸出name變量,并沒有報(bào)錯,輸出undefined,說明輸出的時候該變量已經(jīng)存在了,只是沒有賦值而已。
其實(shí)編譯器是這樣工作的,在代碼執(zhí)行之前從上到下的進(jìn)行編譯,當(dāng)遇到某個用var聲明的變量的時候,先檢查在當(dāng)前作用域下是否存在了該變量。如果存在,則忽略這個聲明;如果不存在,則在當(dāng)前作用域中聲明該變量。
上面的這段簡單的代碼包含兩種查找類型:
- 輸出變量的值時查找類型是:RHS,即變量出現(xiàn)在右側(cè)時進(jìn)行RHS查詢。(作用域中查找變量都是RHS)。RHS就是取到它的源值。
- 找到變量為其賦值的查找類型是:LHS,即變量出現(xiàn)在賦值操作的左側(cè)時進(jìn)行LHS查詢。(所有的賦值操作中查找變量都是LHS) 注:“賦值操作的左側(cè)和右側(cè)”,并不意味著只是“=”,實(shí)際上賦值操作還有好幾種形式。
作用域中查找變量都是RHS。查找規(guī)則是從當(dāng)前作用域開始找,如果沒找到再到父級作用域中找,一層層往外找,如果在全局作用域如果還沒找到的話,就會報(bào)錯了:ReferenceError: 某變量 is not defined
所有的賦值操作中查找變量都是LHS。其中a=4這類賦值操作,也是會從當(dāng)前作用域中查找,如果沒有找到再到外層作用域中找,如果到全局變量啊這個變量,在非嚴(yán)格模式下會創(chuàng)建一個全局變量a。
注意:不過,非常不建議這么做,因?yàn)檩p則污染全局變量,重則造成內(nèi)存泄漏(比如:a = 一個非常大的數(shù)組,a在全局變量中,一直用有引用,程序不會自動將其銷毀)。
三、詞法作用域是什么?
熟悉作用域后,通常我們將其定義為一套規(guī)則,這套規(guī)則來管理瀏覽器引擎如何在當(dāng)前作用域以及嵌套的作用域中根據(jù) 變量(標(biāo)識符)進(jìn)行變量查找。
我們在前面有拋出一個概念:“詞法作用域是作用域的一種工作模型”,作用域有兩種工作模型:一種主流的是,JavaScript的靜態(tài)作用域——詞法作用域,另一種則是動態(tài)作用域(比較少的語言在用)。
先看一下這個代碼:
function fn1(x) {var y = x + 4;function fn2(z) {console.log(x, y, z);}fn2(y * 5); } fn1(6); // 6 10 50例子中有三個嵌套的作用域:A、B、C,
- A 為全局作用域,有一個標(biāo)識符:fn1
- B 為fn1所創(chuàng)建的作用域,有三個標(biāo)識符:x、y、fn2
- C為fn2所創(chuàng)建的作用域,有一個標(biāo)識符:z
如圖:
作用域,是由其代碼寫在哪里決定的,并且是從外向內(nèi)逐級包含的。
詞法作用域,即在你寫代碼時將變量和塊作用域?qū)懺谀睦飦頉Q定,編譯階段就能夠知道全部標(biāo)識符在哪里以及是如何聲明的,詞法作用域就是靜態(tài)的作用域,能夠預(yù)測在執(zhí)行代碼的過程中如何查找變量(標(biāo)識符),它在你書寫代碼時就確定。
(二)閉包是什么?
一、對閉包的各種解釋
MDN的解釋:
A closure is the combination of a function and the lexical environment within which that function was declared.閉包,是一個變量所聲明的函數(shù)+它的詞法作用域的結(jié)合。JavaScriptKit的解釋
A closure is the local variables for a function - kept alive after the function has returned 閉包對于函數(shù)來說是個本地變量,這個變量是當(dāng)這個函數(shù)返回的時候,變量還存在。閉包,能訪問當(dāng)前函數(shù)外的變量。《JavaScript高級程序設(shè)計(jì)》的解釋:
閉包是一個函數(shù),指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。《JavaScript權(quán)威指南》的解釋:
從技術(shù)的角度講,所有的JavaScript函數(shù)都是閉包。它們都是對象,它們都關(guān)聯(lián)到作用域鏈。【較認(rèn)可】《你不知道的JavaScript》的解釋:
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時,就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。閉包,是基于詞法作用域書寫代碼時產(chǎn)生的一種現(xiàn)象。通過下面的實(shí)踐你會知道,閉包在代碼中隨處可見,不用特意為其創(chuàng)建而創(chuàng)建,隨著深入做項(xiàng)目后,打代碼的不經(jīng)意間就已經(jīng)用了閉包。
二、閉包的作用
- 封裝數(shù)據(jù)
- 暫存數(shù)據(jù)
三、從實(shí)例解析閉包
實(shí)例1:
function fn1() {var name = 'iceman';function fn2() {console.log(name);}fn2(); } fn1();根據(jù)《JavaScript高級程序設(shè)計(jì)》中可知,這個函數(shù)出現(xiàn)了閉包。fn2訪問到了fn1的變量,滿足了書中對閉包的定義“有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)”,fn2本身是個函數(shù),所以滿足《JavaScript權(quán)威指南》所說的“所有的JavaScript函數(shù)都是閉包”
不過,看得出這是多個函數(shù)嵌套,特別是fn2本身是函數(shù),也是一個返回值,也是fn1的賦值變量,對于基礎(chǔ)不牢的小白來說還是很容易混淆的。
實(shí)例2:
function car(){var speed = 0function fn(){speed++console.log(speed)}return fn }var speedUp = car() speedUp() //1 speedUp() //2分析如下:
- fn的詞法作用域能訪問car的作用域。根據(jù)條件執(zhí)行fn函數(shù)內(nèi)的代碼,fn當(dāng)做值返回
- car執(zhí)行后,將fn的引用賦值給speedUp,賦值過程即speedUp=car=fn
- 執(zhí)行speedUp,分別輸出1,2
通過引用的關(guān)系,speedUp就是fn函數(shù)本身(speedUp=fn)。執(zhí)行speedUp能正常輸出變量speed的值,這不就是“fn能記住并訪問它所在的詞法作用域”,而fn(被speedUp調(diào)用)的運(yùn)行是在當(dāng)前詞法作用域之外。
當(dāng)car函數(shù)執(zhí)行完畢之后,其作用域是會被銷毀的,然后 垃圾回收器 會釋放閉包那段內(nèi)存空間,但是閉包就這樣神奇地將car的作用域存活了下來,fn依然持有該作用域的引用,
這塊引用為以下:
以上引用就是閉包
總結(jié):某個函數(shù)在定義時的詞法作用域之外的地方被調(diào)用,閉包可以使該函數(shù)訪問定義時的詞法作用域。
實(shí)例3:用for循環(huán)輸出函數(shù)值的問題
var fnArr = []; for (var i = 0; i < 10; i++) {fnArr[i] = function(){return i}; } console.log( fnArr[3]() ) // 10通過for循環(huán),預(yù)期的結(jié)果我們是會輸出0-9,但最后執(zhí)行的結(jié)果,在控制臺上顯示則是全局作用域下的10個10。
這是因?yàn)楫?dāng)我們執(zhí)行fnArr[3]時,先從它當(dāng)前作用域中找 i 的變量,沒找到 i 變量,從全局作用域下找。開始了從上到下的代碼執(zhí)行,要執(zhí)行匿名函數(shù)function時,for循環(huán)已經(jīng)結(jié)束(for循環(huán)結(jié)束的條件是當(dāng)i大于或等于10時,就結(jié)束循環(huán)),然后執(zhí)行函數(shù)function,此時當(dāng) i 等于[0,1,2,3,4,5,6,7,8,9]時,此時i 再執(zhí)行函數(shù)代碼,輸出值都是 i 循環(huán)結(jié)束時的最終值為:10,所以是輸出10次10。
由此可知:i 是聲明在全局作用域中,function匿名函數(shù)也是執(zhí)行在全局作用域中,那當(dāng)然是每次都輸出10了。
延伸:
那么,讓 i 在每次迭代的時候都產(chǎn)生一個私有作用域,在這個私有的作用域中保存當(dāng)前 i 的值
var fnArr = []; for (var i = 0; i < 10; i++) {fnArr[i] = (function(){var j = ireturn function(){return j} })() } console.log(fnArr[3]()) //3用一種更簡潔、優(yōu)雅的方式改造:
將每次迭代的 i 作為實(shí)參傳遞給自執(zhí)行函數(shù),自執(zhí)行函數(shù)用變量去接收輸出值
var fnArr = [] for (var i = 0; i < 10; i ++) {fnArr[i] = (function(j){return function(){return j} })(i) } console.log( fnArr[3]() ) // 3實(shí)例:用做一個計(jì)時器來實(shí)際理解閉包的作用
理解JS閉包——以計(jì)數(shù)器為例
總結(jié)
以上是生活随笔為你收集整理的如何延长作用域链_第4部分2:作用域(链)和 闭包的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringBoot集成Mybatis用
- 下一篇: 太极团队首发:iOS 8.3完美越狱工具