JavaScript變量聲明的var、let、const詳解
前言
一個程序語言在運行的過程中,變量的聲明在整個程序的生命周期中,是不斷在進行的過程。任何程序的計算都會涉及至少一個變量,而計算的結(jié)果的則可能會涉及到另外的一個或者多個變量。變量在使用前是要聲明,變量聲明的過程在計算機的底層,牽涉到的是內(nèi)存空間和內(nèi)存地址的分配。當然啦,有內(nèi)存空間的分配,就會有內(nèi)存空間的回收和再分配。可以看出,變量聲明是我們在接觸一門程序語言中起始的也是很關鍵的一步。
而本篇文章想跟大家聊聊的,是關于JavaScript的變量聲明中的一些內(nèi)容。
內(nèi)容
JavaScript的變量聲明
雖然一門語言的語法發(fā)展,變量聲明的語法和方式通常是比較起始的。程序語言的設計者通常都會在語言的初始的版本盡量根據(jù)自己對語言的設計思想,設計好語言的變量聲明的語法和方式,以保證好程序語言在未來的很長的道路上有一個比較好的起始。但是,JavaScript卻沒有一個這么好的起始。
JavaScript的設計之初衷,就是一門為了解決簡單的網(wǎng)頁互動的腳本語言。它的設計,只用了短短的10天時間。就像初生的嬰兒一樣,稚嫩而沒有太多的規(guī)劃和愿景。設計者做夢都沒有想到,JavaScript將來可以在整個互聯(lián)網(wǎng)的發(fā)展中占有這么大的一個分量,不僅在瀏覽器端,而且在移動端、APP端、服務端、桌面應用上都扮演著重要的角色。
JavaScript的設計的雛形是稚嫩的,但是隨著這么語言逐漸的受到關注和使用,語言本身的設計和語法,也在不斷的發(fā)展著。而JavaScript的變量聲明方式也從野蠻的var
的聲明方式,前行到了ECMAScript標準的let
和const
的聲明方式。
接下來,我們一起來討論它們。
var的變量聲明
var用于聲明一個函數(shù)范圍或者全局范圍的變量,并且可以為其初始化一個值。它是ECMAScript2015之前的變量聲明的唯一關鍵字,曾經(jīng)承載過一代前端開發(fā)者的青蔥歲月以及水深火熱。它具有如下的特性:
變量聲明在函數(shù)作用域中
與大多數(shù)的語言不同,作為過去的JavaScript的唯一變量聲明方式,var
的所聲明的變量并不是聲明在塊級作用域中 ,而是聲明在最近的函數(shù)上下文中,也就是局部函數(shù)作用域。對于未在任何函數(shù)中聲明的變量,其聲明范圍則是在全局對象global
之中。
/* === 全局作用域 start === */ /* 變量globalVar聲明在任何函數(shù)之外,為全局作用域中的變量 任何的函數(shù)和方法都能訪問到這個變量 */ var globalVar = 'global var'; /* === f1函數(shù)作用域 start === */ function f1() { /* 變量localVar1聲明在函數(shù)f1中,為f1的局部函數(shù)作用域中的變量 只有代碼詞法分析中的f1函數(shù)的內(nèi)部函數(shù)能夠訪問到這個變量 在這個作用域中可以訪問到全局作用域中的變量globalVar */ var localVar1 = 'local var1'; /* === f2函數(shù)作用域 start === */ return function f2() { /* 變量localVar2聲明在函數(shù)f2中,為f2的局部函數(shù)作用域中的變量, 只有代碼詞法分析中的f2函數(shù)的內(nèi)部函數(shù)能夠訪問到這個變量, 在這個作用域中可以訪問到全局作用域中變量globalVar以及外層f1函數(shù)作用域中的localVar1 */ var localVar2 = 'local var2'; return localVar2; /* === f2函數(shù)作用域 end === */ } /* === f1函數(shù)作用域 end === */ } f1(); /* === 全局作用域 end === */
上面的代碼中,globalVar
作為定義在全局對象global
中的一個全局變量,除了可以直接通過變量標識符globalVar
訪問到之外,也可以通過global.globalVar
或者window.globalVar
的對象屬性訪問方式來訪問到。
同時還能看到,上面的作用域定義構(gòu)建成了一條global -> f1 -> f2
的函數(shù)作用域鏈, 函數(shù)執(zhí)行過程中的變量訪問會根據(jù)這條函數(shù)作用域鏈進行規(guī)則查找和訪問。
變量重復聲明
var
的變量聲明支持在同一作用域中進行同一個變量的多次重復的聲明,多次聲明中只有首次的變量聲明會被執(zhí)行,其他聲明會因為當前的執(zhí)行上下文對象中已存在當前變量而被忽略。但是,聲明變量同時如果對變量進行了初始賦值,賦值操作依舊會被執(zhí)行。
/* 變量的首次聲明在當前執(zhí)行上下文對象中新增一個value變量 */ var value = 1; /* 除了變量的首次聲明外,其他相同的聲明,都不會被執(zhí)行, 但是,聲明中的初始賦值依舊會被當作正常的賦值操作執(zhí)行 */ var value = 2; console.log(value); // 2
變量聲明提升
var
的變量聲明存在“提升”(hoisting)的特性。JavaScript在執(zhí)行函數(shù)代碼的前,首先會對函數(shù)內(nèi)當前執(zhí)行上下文中的所有var
的變量聲明進行掃描確認,并將所有的聲明按順序提前到當前執(zhí)行上下文的頂部,也就是函數(shù)內(nèi)的頂部。這種變量聲明提升的特性,讓我們在同一作用域中的可以直接訪問和使用一個在后續(xù)的代碼才進行了首次變量聲明的變量,即在變量聲明之前使用變量。
雖然,變量的聲明在執(zhí)行時被提升了,但是,變量聲明中的初始賦值操作卻并沒有被提升,從而形成一種聲明和初始賦值的執(zhí)行邏輯上的割裂。也就是說,即使我們可以直接訪問和使用在后續(xù)的代碼才被聲明和賦值的變量,然而,變量的值卻是undefined
。代碼一直執(zhí)行到聲明和初始賦值語句后,變量的值才會被賦值為它的初始賦值。
/* 由于變量聲明提升了,可以在變量聲明前訪問使用變量, 但是,訪問到的變量值是undefined */ console.log(value); // undefined /* 變量賦值并沒有被提升,只有執(zhí)行完這一句語句,變量才被賦值為1 */ var value = 1; console.log(value); // 1
怪異危險的var
var
的這些的怪異的特性,雖然在程序上都屬于合法可運行,但是,對于編寫程序的人來說,有著許多有違正常邏輯的地方,而且容易造成代碼調(diào)試的困難。例如:面對變量的聲明和管理,我們不得不以函數(shù)為基本的管理區(qū)進行管理;對變量的重復聲明,讓我們對代碼從上而下的變量的安全訪問和變量值的確認變得不容易控制;
雖然,為了防止這些危險的出現(xiàn),我們可以通過編程習慣的方式來進行約束。但是,這些終究只是軟約束,隨著JavaScript的不斷發(fā)展和使用來構(gòu)建復雜的應用,這些約束就愈加捉襟見肘。
let和const的變量聲明
黑暗和混沌,終于迎來了曙光的到來。ECMAScript2015的到來,就像一束光輝照耀進來,給JavaScript變量聲明這一塊帶來了翻天覆地的變化。不僅帶來了let
和const
這兩個新的變量聲明關鍵字,而且還直接一躍變成了最佳的變量聲明方式。
而這些驚天動地變化的出現(xiàn)都是因為,相對于var
,let
和const
帶來了如下新的共同特征:
塊級作用域
let
和const
關鍵字帶給我們的第一個讓人興奮的特性就是塊級作用域??嘤谥?code>var關鍵字的基于函數(shù)作用域的特性,我們不得在函數(shù)范圍內(nèi)小心的定義變量名稱和使用變量,特別是在條件判斷和循環(huán)邏輯中,甚至不得不在循環(huán)中使用上立即執(zhí)行函數(shù)來進行變量值的讀取和保存。
function f() { /* 由于var基于函數(shù)作用域,回調(diào)函數(shù)內(nèi)的i變量都為同一個副本, 下面程序的輸出結(jié)果為5、5、5、5、5 */ for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }); } /* 通過立即執(zhí)行函數(shù)來構(gòu)建基本數(shù)據(jù)類型的值傳遞,從而創(chuàng)建新的變量副本, 下面程序的輸出結(jié)果為0、1、2、3、4 */ for (var i = 0; i < 5; i++) { (function(i) { setTimeout(() => { console.log(i); }, 0); })(i); } } f();
現(xiàn)在,我們可以通過花括號{}
以及let
和const
關鍵詞聲明限制于塊級作用域中的變量了。if
塊、while
塊、function
塊甚至單獨的{}
都是良好的塊級作用域聲明區(qū)塊。還有一點,相對于var
,在全局作用域中使用let
和const
聲明的變量并不會添加到全局上下文對象global
中。綜合看來,塊級作用域的特性使我們的變量的聲明和使用更加的簡潔和安全。
/* 由于使用的基于跨級作用域的let,每次循環(huán)體都會產(chǎn)生一個i變量的副本, 下面程序的輸出結(jié)果為0、1、2、3、4 */ for (let i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 0); } if (true) { let a = 1; } /* 拋出ReferenceError, 因為用let定義的a變量限制于塊級作用域,只在前面方括號中可以訪問 */ console.log(a); // ReferenceError /* let和const聲明在全局作用域中聲明的變量,不會被隱晦添加到global對象中, 保證了全局環(huán)境的安全, */ let b = 1; console.log(window.b); // undefined
不可重復聲明
相比于var
的重復聲明會被忽略,let
和const
在同一作用域中對同一標識符的變量不能重復聲明。這種變量聲明執(zhí)行上的約束,避免了同作用域下不同位置的變量聲明沖突,消除了許多不容易預見的運行問題,讓JavaScript在構(gòu)建復雜應用中的變量聲明過程變得更加的安全。
let a = 1; { /* 不在同一塊級作用域內(nèi),而在子級塊級作用域內(nèi) */ let a = 1; } /* 和最上面的聲明在同一塊級作用域內(nèi),屬于重復聲明, 這條語句執(zhí)行會拋出SyntaxError */ let a = 1; // SyntaxError
暫時性死區(qū)
相對于var
的變量聲明提前導致的變量在函數(shù)作用域中可以“先使用后聲明”的現(xiàn)象。let
和const
關鍵字聲明的變量也具有執(zhí)行中變量聲明提前的特性,但是卻不能在變量完成聲明之前進行訪問和修改,否則會拋出ReferenceError
的錯誤。
這種變量聲明所綁定的塊級作用域的頂部,一直到變量聲明語句執(zhí)行完成之前的,變量不能訪問和修改的區(qū)域,稱為這個變量的暫時性死區(qū)(TDZ,temporal dead zone)。暫時性死區(qū)的引入,在代碼執(zhí)行上約束了變量必須遵循“聲明后才能使用”的原則,使JavaScript的變量聲明和使用更加安全和具有更好的可讀性。
{ /* === 變量a和b的TDZ start === */ /* 拋出ReferenceError, TDZ范圍內(nèi)不能讀取和修改變量b */ console.log(b); // ReferenceError let a = 1; /* === 變量a的TDZ end === */ let b = 2; /* === 變量b的TDZ end === */ console.log(a); // 1 }
值得注意的是,暫時性死區(qū)是基于執(zhí)行順序(時間)上的,而不是編寫代碼順序(位置)上的。只要變量的訪問和修改的代碼的執(zhí)行,是在變量聲明之后,就是合法的。
{ /* === 變量value的TDZ start ===*/ function f() { console.log(value); } /* 在DZT內(nèi)訪問和修改變量value,會拋出錯誤 */ let value = 1; /* === 變量value的TDZ end ===*/ /* 函數(shù)f中對value的訪問發(fā)生在TDZ外面, 所以訪問合法 */ f(); // 1 }
使用好let和const
let
和const
兩者的這些新的共同特性,讓它們直接變成了變量聲明的最佳方式和實踐。但是,兩者的除了上面說道的共同特性,還存在一個兩者的差異。
簡單來說,let
和const
的區(qū)別在于,let
用于聲明基于塊級作用域的變量,而const
用于聲明基于塊級作用域的常量。
- 使用
let
聲明的變量,在變量的整個生命周期中,能夠?qū)ψ兞侩S時進行賦值修改。 - 使用
const
聲明的變量為引用常量,必須在聲明的同時進行初始賦值,在變量的整個生命周期中,無法再通過賦值的方式來修改變量值。
const
的這個無法再賦值的特性,在不同的變量類型下會有不同的表現(xiàn):
- 如果
const
聲明并且初始化賦值是基礎數(shù)據(jù)類型變量(String
、Number
、Boolean
、Symbol
),那么該變量之后就不能再進行任何值的修改了。因為基礎數(shù)據(jù)類型變量的值必須通過變量賦值來進行修改。 - 如果
const
聲明并且初始化賦值是引用數(shù)據(jù)類型變量(Array
、Object
、Map
、Set
),那么我們則可以隨意對該變量的字段屬性進行修改。因為引用數(shù)據(jù)類型變量的內(nèi)容修改(新增、修改、刪除屬性)并不會出現(xiàn)所聲明變量的賦值操作.
/* let聲明的變量可以任意賦值和修改 */ let value1 = 1; value1 = 2; /* 拋出SyntaxError, const聲明的變量必須賦值初始值 */ const value2; // SyntaxError /* 拋出TypeError, const聲明的變量在整個變量生命周期內(nèi)不能再賦值 */ const value3 = 3; value3 = 4 // TypeError /* const聲明的引用數(shù)據(jù)類型變量,依舊可以修改其字段值 */ const value4 = { key1: 1, key2: 2, }; value4.key = 3;
在實踐中,我們應該盡可能的多使用const
進行變量聲明,當確定一個變量需要重新賦值的時候才將其改用let
進行聲明,這樣可以讓我們對程序執(zhí)行中的可能發(fā)生重新賦值的變量有一個清晰的了解,減少可能出現(xiàn)的bug。
總結(jié)
ECMAScript2015帶來了let
和const
這兩個新的變量聲明關鍵字,帶來了塊級作用域和暫時性死區(qū)等新特性,解決過去var
的變量聲明中的一些怪異問題,在語法層面和運行層面上,保證了變量聲明的“聲明后才可以使用”的安全特性,提高了JavaScript在編寫大型應用時的變量聲明和使用安全。
當前,let
和const
已經(jīng)是我們?nèi)粘i_發(fā)中的變量聲明的最佳時間方式。了解它們,才能用好它們。
參考資料
- 《JavaScript高級程序設計》
- 《你不知道的JavaScript》
- MDN
到此這篇關于JavaScript變量聲明的var、let、const詳解的文章就介紹到這了,更多相關JS變量聲明var、let、const內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript實現(xiàn)倒計時跳轉(zhuǎn)頁面功能【實用】
本文分享了JavaScript實現(xiàn)倒計時跳轉(zhuǎn)頁面功能的具體實例代碼,頁面代碼簡單,直接拷貝就能運行,頁面可以自己美化下哦。需要的朋友一起來看下吧2016-12-12javascript判斷ie瀏覽器6/7版本加載不同樣式表的實現(xiàn)代碼
ie6/ie7的兼容問題很讓人苦惱,我們可以針對這兩個版本的瀏覽器單獨寫獨立的樣式表,來解決兼容問題。這里的例子以判斷ie6與ie7來加載不同的樣式表2011-12-12學習Bootstrap滾動監(jiān)聽 附調(diào)用方法
這篇文章主要為大家全面解析Bootstrap中滾動監(jiān)聽的使用方法,感興趣的小伙伴們可以參考一下2016-07-07