JavaScript作用域和閉包
在本文中,筆者將用通俗的語言和簡單的代碼,介紹以下幾種概念:
- 變量提升
- this的使用場景
- 作用域
- 閉包的應用
最後還有一個例題
變量提升
首先我們要知道,js的執行順序是由上到下的,但這個順序,並不完全取決於你,因為js中存在變量的聲明提升。
這裏比較簡單,直接上代碼
console.log(a) //undefined var a = 100 fn(‘zhangsan‘) function fn(name){ age = 20 console.log(name, age) //zhangsan 20 var age }
結果
打印a的時候,a並沒有聲明,為什麽不報錯,而是打印undefined。
執行fn的時候fn並沒有聲明,為什麽fn的語句會執行?
這就是變量的聲明提升,代碼雖然寫成這樣,但其實執行順序是這樣的。
var a function fn(name){ age = 20 console.log(name, age) } console.log(a) a = 100 fn(‘zhangsan‘)
js會把所有的聲明提到前面,然後再順序執行賦值等其它操作,因為在打印a之前已經存在a這個變量了,只是沒有賦值,所以會打印出undefined,為不是報錯,fn同理。
這裏要註意函數聲明和函數表達式的區別。上例中的fn是函數聲明。接下來通過代碼區分一下。
fn1(‘abc‘)function fn1(str){ console.log(str) } fn2(‘def‘) var fn2 = function(str){ console.log(str) }
本例中fn1是函數聲明,而fn2是函數表達式。函數表達式中的函數體是不會被提升的。
結果
可以看到fn1被提升了,而fn2的函數體並沒有被提升。
效果等同於
var fn2 fn2(‘def‘) fn2 = function(str){ console.log(str) }
這下應該明白為什麽報錯了吧。變量提升就說這麽多,接下來看this
this
this簡單理解就是調用函數的那個對象。
要搞懂this,首先要理解一句話:
this要在執行時才能確認,定義時無法確認。
接下來還是在代碼中解釋這句話
var a = { name: ‘A‘ fn: function () { console.log(this.name) } }
看這段代碼,this指向誰?
現在說指向誰都是不對的,this在定義時是無法確認的,只有執行時才能確認。
繼續上面代碼,判斷以下this的指向。
a.fn() //this === a a.fn.call({name:‘B‘}) //this === {name: ‘B‘} var fn1 = a.fn fn1() //this === window
代碼中已經給出答案了。雖然fn定義在a對象裏,但是fn中this的指向並不總是指向a,誰調用fn,this就指向誰。
看一下輸出結果
window沒有name屬性,所以最後一行為空。
this都有哪種使用場景呢?
主要由以下4點
- 作為構造函數執行
- 作為對象屬性執行
- 作為普通函數執行
- call apply bind
//作為構造函數執行
function (name){ this.name = name } var f = new Foo(‘zhangsan‘) //作為對象屬性 var obj = { name: ‘A‘ printName:function(){ console.log(this.name) } } obj.printName() //作為普通對象 function fn(){ console.log(this) //此時的this指向window } fn()
前三種通過代碼一看就明白了,不用多說,接下說一下call apply 和 bind
這三個的作用都是改變this的指向,call和apply不同的地方就在於傳遞參數的部分,apply要用數組,還是在代碼中看。
function fn1(name,age){ console.log(name,age) console.log(this) } //call和apply用法的不同 fn1.call({x:100},‘zhangsan‘,21) fn1.apply({x:100},[‘zhangsan‘,21]) //參數放在數組裏
call和apply的第一個參數都是this的指向,之後是構造函數的參數。
輸出結果
可以看到,兩種方式的輸出結果是一樣的。
再然後是bind,bind和前面兩個都不太一樣,bind不是在函數執行時調用的,而是在函數聲明時。
看代碼
var fn2 = function(name,age){ console.log(name,age) console.log(this) }.bind({y:200}) //在函數聲明時綁定this的指向 fn2(‘lisi‘,22)
結果·
一看就明白,不多解釋了,但有一點需要註意
只有函數表達式才能使用bind,函數聲明是不能用的。
來一個錯誤的演示
//錯誤演示 function fn3(name,age){ console.log(name,age) console.log(this) }.bind({z:300}) //錯誤演示 //錯誤演示 fn3(‘lisi‘,23)
報錯嘍
作用域鏈
首先要知道,JavaScript是沒有塊級作用域的
if(true){ var name = "zhangsan" } console.log(name)
正常打印,沒有報錯。
其次,還要知道全局作用域和函數作用域
var a = 100 function fn(){ var a = 200 console.log(‘fn‘,a) //這裏的a是200 } console.log(‘global‘,a) // 這裏的a是100 fn()
在函數裏聲明的變量,在是不會影響外面的變量的,看結果
這些都了解了後,我們來看作用域鏈。
說概念我也不知道咋說,直接在代碼中看吧。
var a = 100 function fn(){ var b = 200 console.log(a) //a為自由變量 console.log(b) } fn()
在fn中是沒有a變量的,當在當前作用域下沒有定義變量就是自由變量,當前作用域沒有的話,就會去他的上級作用域找,這就是作用域鏈。
還要註意一點,作用域鏈實在定義時確定的,不是在執行時,不管在哪個作用域下調用某個函數,該函數內的作用域鏈是不會變得,再來看一段代碼
var x = 100 function F1(){ var y = 200 function F2(){ var z = 300 console.log(x) //自由變量 console.log(y) //自由變量 console.log(z) } F2() } F1()
我們看x變量,這是一個自由變量,js引擎在執行到console.log(x)時,會先在F2中尋找x,沒找到就去當前作用域的父級作用域F1中找,還找不到就在往上找直到全局作用域。
因為定義時作用域鏈已經確定了,不管這條鏈上的函數是在哪裏調用的,這條作用域鏈不會在改了。
通過這兩個簡單的代碼你是不是已經了解了作用域鏈的工作原理了呢?接下來,介紹閉包
閉包
出於種種原因,我們有時候需要得到函數內的局部變量。但是,前面已經說過了,正常情況下,這是辦不到的,只有通過變通方法才能實現。
那就是在函數的內部,再定義一個函數,然後把這個函數返回。
function F1(){ var a = 100 //返回一個函數 (函數作為返回值) return function (){ console.log(a) } } //f1得到一個函數 var f1 = F1() var a = 200 f1()
打印出
在本例中就實現了閉包,簡單的說,閉包就是能夠讀取其他函數內部變量的函數。
下面說為什麽打印的是100
看這句 var f1 = F1(); F1這個函數執行的結果是返回一個函數,所以就相當於把F1內的函數付給了f1變量,類似於這樣
var f1 = function(){ console.log(a) //這裏的a是一個自由變量 }
這裏的a是一個自由變量,所以根據作用域鏈的原理,就應該去上一級作用域去找。之前說過,作用域鏈在定義時確定,和執行無關,那就去想上找,這個函數定義定義在F1中,所以會在F1中找a這個變量,所以這裏會打印的100。
通過這種方式,我們在全局下就讀取到了F1函數內部定義的變量,這就是閉包。
閉包主要有以下兩種應用場景
- 函數作為返回值
- 函數作為參數來傳遞
在上例中我們已經實現了函數作為返回值的閉包的應用,接下來再來一例作為參數傳遞的
function F1(){ var a = 100 return function (){ console.log(a) } } var f1 = F1() function F2(fn){ var a = 200 fn() } F2(f1)
還是輸出100
例子比較簡單,但能說明問題,在閉包的實際應用中,往往要很復雜,但掌握了閉包的原理後,就不難實現了。
例題
來檢驗一下你對這部分內容的理解吧
創建10個<a>標簽,點擊的時候輸出對應的序號
看到這道題,最直接的想法或許就i是這樣的
//錯誤代碼演示
var i,a for (i = 0; i < 10; i++) { a = document.createElement(‘a‘) a.innerHTML = i + ‘<br />‘ a.addEventListener(‘click‘,function(e){ e.preventDefault() console.log(i) }) document.body.appendChild(a) }
但這是不對的。
可以看到,我對著左邊的標簽一頓亂點,結果打印的都是10.為什麽呢
因為上例代碼都是同步執行的,在頁面加載的一瞬間,for循環已經執行完畢,我再去點標簽的時候,i的值已經是10了,所以不管我點第幾個標簽,打印的都會是10。
那怎麽解呢?
這麽解
for (var i = 0; i < 10; i++) { (function(i){ a = document.createElement(‘a‘) a.innerHTML = i + ‘<br />‘ a.addEventListener(‘click‘,function(e){ e.preventDefault() console.log(i) }) document.body.appendChild(a) })(i) }
功能實現。
這道題我就不解釋了,如果本文中介紹的內容你已經掌握了話,本例是很容易理解的。
如果你對作用域鏈和閉包已經理解了,建議繼續學習
JavaScript原型鏈
JavaScript類與繼承
最後,如果覺得本文對你有幫助話,點個贊吧^_^
JavaScript作用域和閉包