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