JS的作用域和閉包
作用域
作用域是什麼?程序設計概念,通常來說,一段程序代碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的代碼範圍就是這個名字的作用域。
塊級作用域
在let,const出現之前,使用var聲明的變數是沒有塊級作用域的,
var a = 5;{ var a = 6;}console.log(a) //6
而使用let或者const之後
let a = 5;{ let a = 6;}console.log(a); //5
函數作用域
在let,const出現之前只有函數作用域,至於為什麼這樣設計,還不太清楚
function test(){ var a = 5;}console.log(a) //Uncaught ReferenceError: a is not defined
但是函數內部是可以訪問的a變數的,同時也可以訪問其外部環境的變數
var a = 5;function test(){ var b = 6; console.log(a) console.log(b)}test() //先列印出 5,再列印出6
作用域鏈
每個函數都有都有自己的執行環境,當執行流進入一個函數時,函數的環境就會推入一個環境棧中,函數執行之後,棧將其推出環境,並返回之前的執行環境。當代碼在一個環境中執行的時候,會創建變數對象的一個作用域鏈,用來保證執行環境有權訪問的所有變數和函數的有序訪問。
//globalvar a = 5;function test1(){ var b = 6; function test2(){ var c = 7; //.... }}
- global環境中的變數對象
a,test1
- test1的變數對象 b,test2
- test2的變數對象 c,.... global <-- test1 <--test2 <-- ··· 並形成這樣的一天鏈,使得變數訪問變得有序,子級能訪問父級,但不能訪問其子級
詞法作用域
詞法作用域也即靜態作用域。
變數提升
代碼執行時首先會將代碼詞法化,簡單點說就是將每個執行環境中的變數和函數保存在上面提到的變數對象中,這樣也帶來了一個新的問題:變數提升
console.log(a)var a = 5;
執行後列印undefined
,因為在當前的執行環境將變數a進行了聲明,當代碼執行到var a = 5
的時候才會將5賦值給a,那麼在ES6中使用let之後呢?
console.log(a)let a = 5;
執行後列印Uncaught ReferenceError: a is not defined
,使用let之後不存在變數提升
函數優先
foo(); //1var foo;function foo(){ console.log(1)}var foo = function(){ console.log(2)}
以上代碼執行之後列印出了1.這是因為在執行之前首先將函數和變數保存在了執行環境中的變數對象,也即所謂的變數提升,函數提升,並且在這裡函數首先會被提升,然後才是變數。函數foo首先提升之後,當代碼執行到var foo = function(){console.log(2)}
之後,再執行foo()
,這時才會列印出2
閉包
由於函數作用域的問題,即函數外部不能訪問函數內部的變數,所以如何能訪問函數內部的變數就變成了一個問題。而解決這個問題的方法簡單來說就是閉包。
function test(){ var a = 5; return function(){ return a; }}var innerAFun = test();var innerA = innerAFun();console.log(innerA); // 5
在上面的代碼中innerAFun 就是test函數中的閉包,也就是test中return的函數
閉包的用處
本質上無論何時何地,如果函數當作第一級的值類型併到處傳遞,你就會看到閉包在這些函數中的應用。在定時器、事件監聽器、Ajax請求、跨窗口通信,webWorkers或者任何其他的非同步或者同步任務中,只要使用了回調函數,實質上就是在使用閉包。(內容來自《你不知道的Javascript》上卷)
- 循環和閉包for(var i=0;i<5;i++){ setTimeout(function(){ console.log(i) },i*1000)}
按照正常思維會每隔1s從0遞增列印,而實際上是每隔1s列印5。
首先為什麼會列印出5,因為當循環到i=4的時候,執行循環中的代碼,這時i++會再執行一次,使得i=5。那為什麼又每次都列印出5呢。因為i使用var聲明,沒有塊級作用域,所以在這裡i實際上是一個全局變數,而全局變數只有一個使得每次定時器到的時候只能取到5這個值。
如何解決?如果有塊級作用域的話,每次循環就取當前塊級作用域內的值列印,1. 使用letfor(let i=0;i<5;i++){ setTimeout(function(){ console.log(i) },i*1000)}//0,1,2,3,4
這樣執行會每個1s列印出i,0,1,2,3,4
- 利用IIFE創建作用域
for(var i=0;i<5;i++){ (function(j){ setTimeout(function(){ console.log(j) },j*1000) })(i)}//0,1,2,3,4
這樣執行,i會按值傳遞給j,這樣定時器每次執行都會找到當前匿名函數作用域內的j值,從而能每隔1s列印出i的值
- 模塊使用閉包還有一個大的作用就是創建模塊var module = (function(){ var a = 5; function test1(){ //doing sth } function test2(){ //doing sth } return { a:a, test1:test1, test2:test2 }})()
這段代碼中有兩個明顯的閉包就是test1和test2
閉包帶來的弊端
function test(){ var a = 5; return function(){ a += 1; return a; }}var innerAFun = test();var innerA = innerAFun(); console.log(innerA); // 6var innerA1 = innerAFun();console.log(innerA1); // 7
正常情況下,函數執行完畢之後會由垃圾回收機制處理使得在局部變數執行完畢之後釋放內存,
而在這裡我們發現,test執行完畢之後依舊存在於內存之中,a由第一次的6,變成了接下來的7。主要的原因就是return處理的a值賦值給了一個全局變數innerA,是的局部變數a長久的保存在了內存中。如果不注意這個問題,就會導致大量的變數存在於內存中,使得應用或者瀏覽器的性能降低。推薦閱讀:
※為什麼 input 元素需要指定 height (以及其他內容)
※幾個簡單的transform效果筆記
※這可能是史上最全的CSS自適應布局總結
※剛開始學HTML5 + CSS,用什麼軟體好?
※CSS實現兼容性的漸變背景(gradient)效果