淺談js變數宣告
----淺談js變數宣告
或者確切地說應該是變數宣告以及與之相關的一些東西。
這裡只談四種宣告方式(var,function,let,const),其它的宣告方式以後有接觸再更新。
‘var’ VS ‘let’
var貌似是最常用的,在ES6還沒出來的時候只能用var來宣告一個變數,var宣告的變數只有函式能對其構成作用域。這樣的話會出現一些意料之外的問題,比如如果你的頁面裡有一個元素的id是xxx,那麼你在控制檯打印出來的就是xxx這個元素本身,但是如果這個id名是parent,那麼它打印出來的就是window全域性屬性。如果你在控制檯再輸入var parent = document.getElementById('parent')
全域性變數。是的,答案是全域性變數。在我們還沒有寫程式碼的時候,瀏覽器就為我們提供了一個全域性物件window,window裡邊的屬性我們稱之為全域性屬性。其中parent就是window的一個全域性屬性,所以在我們還沒有寫id=parent的時候,parent就已經存在了。所以我們直接呼叫parent並不能得到那個元素。
但是,你也知道,如果你寫了下邊這兩句話
var a = 10
var a = 20
得到的答案是20,因為變數存在覆蓋。所以,在我們重新宣告var parent = document.getElementById('parent')
所以使用區域性變數。如果只是在一個小的範圍內宣告並使用一個變數,那麼這個變數在這個區域內把全域性屬性覆蓋了也基本上不會出什麼問題。
只有函式能構成var的作用域,所以要想使用一個區域性變數,我們就需要宣告並執行一個函式。這就是立即執行函式。是不是很熟悉?所以說最討厭這些概念了,明明很簡單的東西,非得喊個聽上去很牛逼的名詞。
鑑於我們寫這個函式的目的只是為了造成一個區域性作用域,並不需要後邊繼續進行呼叫,所以為了提高效率(也為了讓你少費腦子想一個函式名),選擇直接在宣告之後執行它。
function(){
var parent = document.getElementById('parent')
console.log(parent)
}.call()
但是這樣子的話並不是萬無一失的,在你重新整理頁面之後會報錯,瀏覽器以為我們的語法錯了。避免瀏覽器報錯的方法很多,但是萬變不離其宗,不論使用的是什麼方法,我們的目的都是讓瀏覽器知道這是一條語句,而不是別的什麼。所以這樣子給整個函式及其呼叫方法加一個小括號可以做到。
(
function(){
var parent = document.getElementById('parent')
console.log(parent)
}.call()
)
或者只在函式宣告上加小括號也行。比較通用的是在函式前邊加一個運算子,比如說+
,-
,!
(取反),~
(二進位制取反),告訴瀏覽器後邊的是一個值而不是一個函式宣告,讓瀏覽器宣告並呼叫求值。雖然這樣得到的函式返回值與原先的結果可能會有偏差(比如用-號的話得到一個負值),但是我們並不需要這個返回值。一開始就說過,這個函式只是用來造一個區域性作用域。
你一定會認為這樣很麻煩。不過是想要用一個區域性變數而已,這樣大費周章多少讓人心有不甘。所以ES6出了一個let
宣告方法。
let的作用域在包裹著它的程式碼塊裡,也就是說,如果你想要使用一個區域性變數,並不需要宣告一個立即執行函式,而只需要寫一個程式碼塊。上邊的程式碼完全可以變成這個樣子
{
let parent = document.getElementById('parent')
console.log(parent)
}
在區域外parent依舊指的是window.parent。這就是let。
此外,關於var還有一個很容易犯的錯誤,用let就可以完全避免。用js通過父元素為子元素繫結click事件。
html如下:
<div class="buttons" id="buttons">
<span>按鈕0</span>
<span>按鈕1</span>
<span>按鈕2</span>
<span>按鈕3</span>
</div>
js如下:
var children = buttons.children //用var聲明瞭一個全域性變數children,儲存buttons的所有子元素
for(var i=0; i<children.length; i++){
children[i].onclick = function(){ //執行點選事件的時候輸出對應的第幾個按鈕
console.log(i)
}
}
我們想要得到的效果是點選按鈕0的時候輸出0,點選按鈕1的時候輸出1…但事實上無論你點選什麼,輸出結果都是4,因為記憶體裡只存了一個i
,這個i的最終運算結果是4。但是如果你把var i = 0
改成let i = 0
就可以得到你想要的結果。因為如果你使用let的話,每次迴圈引用的都是不同的i(引用了i變數的不同例項),故而能實現你的需求。
如果你一定要用var來實現的話,那你需要手動找到它的index,也就是這個元素在數組裡的排行。
for(var i=0; i<children.length; i++){
children[i].onclick = function(x){
for(var index=0; index<children.length; index++){
if(x.currentTarget === children[index]){ break }
}
console.log(index)
}
}
當然那個方法稍微有點麻煩,你可以試試把變數i傳入進去,像這樣
var children = buttons.children
for(var i=0; i<children.length; i++){
(function(x){
children[x].onclick = function(){
console.log(x)
}
}.call(this, i))
}
使用立即執行函式把變數i傳進去的話,由於函式對var構成作用域,所以每次對這個元素進行click事件繫結所使用的變數x不會衝突,這樣我們便可以得到想要的結果了。
變數提升
var和function都有變數提升機制,也就是說,你可以先使用一個變數,然後再宣告它。
var的變數提升:
console.log(a) //undefined
var a = 10
var的變數提升會把宣告部分提升到前邊。所以上邊的程式碼真正的執行順序應該是
var a
console.log(a)
a = 10
function的變數提升:
console.log(f) //f(){}
function f(){}
function的變數提升會把整個函式提升到前邊,所以上邊程式碼的真正執行順序應該是
function f(){}
console.log(f)
我們知道函式還可以用var f = function(){}
來宣告,這種宣告方式的話事實上是
var f
f = function(){}
所以變數提升依舊只是提升var f
。
let沒有變數提升
是的,let算是比較正常的了,它不存在變數提升。也就是說,瀏覽器不會那麼賤多此一舉地幫你改變程式碼的順序。但是這樣的話就會出現“暫時性死區(temporal dead zone)”,如果你用let在一個作用域內聲明瞭一個變數,那麼在這個作用域內,let宣告之前,這個變數是不可以被使用的,會報錯。但是由於js可以有非宣告變數(不使用任何宣告語句直接對一個變數進行操作),所以這就會出現一個很奇怪(發現自己寫js相關blog的時候用“奇怪”的頻率好高。。。)的現象。
typeof a //error: a is not defined
a = 20 //error: a is not defined
typeof a //error: a is not defined
let a = 10
但是上述程式碼如果把let a = 10
刪掉就會是這樣的結果。
typeof a //"undefined"
a = 20 //不報錯,正常賦值
typeof a //"number"
除了暫時性死區之外,還需要注意的一點是let宣告的變數不可重複宣告
。在同一個程式碼塊內,如果你用let聲明瞭一個變數a,那你就不能再繼續用任何宣告方法(包括let,var和function)來宣告a變數。
const宣告常量
使用const宣告的變數往往是一個常量,但是這個常量只是說它們在棧記憶體中儲存的東西不變,對於複雜型別(object)而言,const意味著這個變數裡邊存的地址不會改變,但是地址裡邊的東西其實是可變的。除此之外,const的特性與let基本相同。兩者同是ES6新增的命令。
const N = 10 //常量一般用全大寫字母表示
N = 20 //error
const OBJ = {
'a': 'ccc',
'b': 'bbb'
}
OBJ = {} //error
OBJ['a'] = 'aaa' //成功賦值