JS被很多人認為是『拙劣的語言』,被這門語言里的各種離奇的事情整的團團轉,這篇文章主要來講講JS中的Scope鏈,其主要是影響JS中的變量作用域。
注:本文適合稍有一定JS基礎的同學
目錄:
- 初步認識
- 預編譯
- 不同Scope進行操作
- Scope鏈
- 例題
- this變量
- new操作符
初步認識
首先,來看一段代碼:
var a = 1;
if(true){
var b = 1;
}
console.log(b) //1
我們從最基本的開始,在面向對象的強語言中(Java,C……),其作用域都是基於塊的(即:{}),塊內可以對塊外的變量進行操作,但是塊外卻對塊內的變量是無法操作的。但是JS呢?一門弱語言,其並沒有實現基於塊的作用域,而是基於function
的,因此上面的代碼運行出來的結果b並不是undefined,說明最終a和b是定義在一個Scope內的。
其Scope如圖:
預編譯
var a = 1;
var b = 2;
function doit(){
console.log(b);
var b = 3;
console.log(b);
}
doit();
console.log(b);
//undefined
//3
//2
講到了Scope,不得不講一講js的預編譯,為什么我們得到的第一個log的結果為undefined呢?按照強語言的思路來說這里應該是2才對呀,這就是js的預編譯。js的代碼在首次被加載完成后進行編譯時,會將所有的function和var提前進行聲明,但是並不會對其進行賦值,賦值則都是在該代碼塊進行執行時才會對其進行賦值,那么第一個log則是在預編譯
為b進行了聲明后,這時b是沒有時行賦值的,所以會log出undefined。
由於js是基於function來創建Scope的,所以只有doit執行時才會創建新的Scope,其Scope如圖:
如果不對變量進行var的話,它是不會存在於function執行的時創建的新Scope的。
不同Scope進行操作
var a = 1;
var b = 2;
function doit(){
var b = 3;
console.log(a);
}
doit();
在doit的function里,加了一句console.log(a)
,這里就有個問題了,這里的a是從哪里來呢?
這里的doit funciton在執行的時候創建了一個新的Scope,如上一個例子講的,但是這個Scope里只創建了b變量,去哪找a呢?我們把上面這個換一換,才是真正正確的Scope:
在js中,var和function在預編譯中,作用都是一樣的,都是提前聲明了變量,但是並沒有對其進行賦值,所以這段代碼完整的Scope應該是擁有這三個變量的,只是doit是一個指向堆的引用。而在doit執行時,才會創建新的Scope,由於js語言的特殊性,雖然doit在這個Scope里定義的,但是其執行環境可以通過引用改變到任何地方,但是doit這個函數的定義環境永遠都是確定的,即這個Scope內。
我們使用__proto__
的思想去理解Scope鏈,當函數執行時,會在新的Scope內創建一個引用(我們假設它為__parent__
),而這個引用指向則是在function定義時的Scope,在進行變量查找時,則會先在自身的Scope內進行查找,如果沒有找到變量,則會根據__parent__
來查找到定義時的Scope,在該Scope里進行變量的查找,如圖:
Scope鏈
就像上圖那樣,每個Scope都擁有一個__parent__
,所有即使這個function無論在什么環境中進行執行,其父Scope都是這個function創建的Scope,雖然js很亂,但是是亂得有規有矩。
當代碼在瀏覽器中執行,去查找變量時,往往都是如下圖過程:
優先查找自己Scope,如果查找不到則根據Scope鏈去查找最近的同名變量,如果一直查找到了Top Scope(在瀏覽器中則是window)還未找到的話,則這個變量會被認為_"Uncaught ReferenceError: **** is not defined"_,如果直接使用的話則會報錯。
我們為什么可以在代碼中的任何地方使用document
,location
等太多變量,都是一直通過Scope鏈查找到了Top Scope從window中取得的。
例題
拿來最基礎的前端面試題來進行分析:
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<script>
var buttons = document.getElementsByTagName("button");
for(var i = 0, l = buttons.length; i < l; i++){
buttons[i].onclick = function(){
alert(i);
}
}
</script>
很多人知道這個例子最終結果是什么樣的,即點擊每個button則都alert(6),並沒有達到我們預想中的結果,但是大部分人並不能對這個問題說出個所以然,只能說到“最后不就是加到6了嘛”。我們從Scope鏈來分析這個例子,這里遍歷了6次,定義了6個匿名function,並將其賦值於了不同按鈕的onclick事件,而這6個匿名function的定義Scope都是相同的,當用戶進行點擊時,會執行對應的一個匿名function,該function創建的Scope中並沒有i這個變量,所以它會根據__parent__
來找到定義這個function的Scope,找到了i,但是這個i的這時值為6,並且這六個function都是找到了這個值為6的i,所以點擊它們都會相同地彈出i這個值。
正確的寫法有很多,但是思路只有一個,那就是改變匿名函數的創建Scope,並且該Scope又與i存在的Scope不同,這就是大家說的閉包,其實閉包就是Scope,每個函數都會創建Scope,創建閉包,下面兩種寫法都是改變了匿名函數的創建Scope,並在該Scope中保存了獨一無二的index值。
寫法1:
for(var i = 0, l = buttons.length; i < l; i++){
(function(index)
buttons[index].onclick = function(){
alert(index);
}
)(i);
}
寫法2:
for(var i = 0, l = buttons.length; i < l; i++){
buttons[i].onclick = (function(index){
return function(){
alert(index);
}
})(i);
}
this變量
this
作為js中最為靈活的變量,也是弄暈了一批一批青年們。開始之前,我們先來上一段代碼:
var a = 1;
function doit(){
var b = 2;
return function(){
var c = 3;
console.log(this);
}
}
doit()();
那么問題來了,這里的this
會和a
,b
,c
中的哪一個變量有關系呢?
先思考一段時間
···
···
···
···
答案是a
,通過this.a
可以獲取到a
的值,即:1。
對於this
,我們可以理解為:特殊的Scope引用變量,其指向當前函數的執行環境Scope(並不是定義時的Scope)
我們用上面的例子來理解,雖然this
是寫在最里面的function的,但是這個function的最終執行是在最外面的Scope進行執行的,所以this指向的是最外層的Scope,而a是定義在最外層的Scope中的,則這時我們可以使用this.a
來獲取到a
的值。
在使用this時:明確了this指向的Scope再使用this,由於js的對引用並沒有限制,所以這個函數的執行環境永遠是不確定的,所以this去對應的Scope中取值時是不一定能取得到的。
call
和apply
則可以去改變函數執行的Scope,從而改變this的指向,對於這兩個方法的使用,這里不再詳解。
new操作符
js作為一個弱語言,在ES6之前並沒有class之說,現在所有瀏覽器都不直接支持ES6(除非手動打開),但是我們想要實現對類的創建該怎么做呢?乒乒乓乓來段代碼:
function Person(name,sex,age){
this.name = name;
this.sex = sex;
var Age = age;
}
var man = new Person("Bob", "male", "17");
在這個new
的過程中,它到底做了什么呢?我們來按步分析一下:
1.創建Object Scope
創建一個空的Scope
2.在該Object Scope下執行
Person("Bob", "male", "17");
則這時Person里的this是指向這個Object Scope,所以this.name
與this.sex
則是為Object Scope賦值了新的變量和值。
3.得到:
而Age去哪了呢?根據上面Scope的知道,Age則是被創建在Person自身的Scope內,並非Object Scope,這時Person函數創建出來的Scope則擁有四個變量,即:name
,sex
,age
,Age
;這個Age就像是強語言中的private一樣,外界是無法獲取到的,這樣我們則會生成一個類似於類的實現方法。
所以來說,下面的代碼:
console.log(man.name) //Bob
console.log(man.sex) //male
console.log(man.Age) //undefined
我們可以得到name和sex,但是並不能得到Age。
End
對於Scope的介紹結束啦,希望本文能為你更深地理解js起到幫助,BTW,js並不是拙劣的語言,當你真正熟悉了它,你會覺得它如此地好用。
Finish.