摘要:聲明的變量存在變量提升,聲明的變量不存在變量提升。聲明的變量允許重新賦值,聲明的變量不允許重新賦值。注意跨腳本聲明重復(fù)變量也會報錯。中出現(xiàn)的任何元素在聲明中出現(xiàn),語法錯誤。中的是如此的怪異。對中的聲明進(jìn)行實例化。
我在上一篇文章javascript中詞法環(huán)境、領(lǐng)域、執(zhí)行上下文以及作業(yè)詳解中的最后稍微提到了有關(guān)var、let、const聲明的區(qū)別,在本篇中我會重點來分析它們之間到底有什么不同。
提到var、let、const中的區(qū)別很多人一下子就想到了,var聲明的變量是全局或者整個函數(shù)塊的而let、const聲明的變量是塊級的變量。var聲明的變量存在變量提升,let、const聲明的變量不存在變量提升。let聲明的變量允許重新賦值,const聲明的變量不允許重新賦值。那么它們之間真的只有這么一點區(qū)別嗎,我們先來看下面一個例子:
注:本篇文章中的所有例子都以最新版chrome瀏覽器為標(biāo)準(zhǔn)(低版本瀏覽器實現(xiàn)會有區(qū)別)。
//我們看一下這三句話,你認(rèn)為會發(fā)生什么 let let = 1; console.log(let); // const let = 1; console.log(let); // var let = 1; console.log(let);
很多人會認(rèn)為,let是關(guān)鍵字,上面這三句聲明都會報錯。可事實真的是這樣嗎?不是。let、const的聲明會報錯,但是var聲明被認(rèn)為是規(guī)范的,更重要的是let、const聲明報錯的原因也不是因為let是關(guān)鍵詞而是由于ECMAScript語言規(guī)范中規(guī)定了當(dāng)用let、const聲明時如果標(biāo)識符是let則報錯。
該代碼是運行在非嚴(yán)格模式下的,嚴(yán)格模式則報錯,值得注意的是嚴(yán)格模式下上面三句話都是因為標(biāo)識符let是保留字而報錯的。有興趣可以在嚴(yán)格模式和非嚴(yán)格模式下測試let let = 1;報錯原因是不同的。
下面的所有代碼都在非嚴(yán)格模式下進(jìn)行,如果是嚴(yán)格模式我會明確指出。
那么上面三句話中的標(biāo)識符let改為const會怎么樣?無論是嚴(yán)格模式還是非嚴(yán)格模式都報錯,錯誤原因是因為const是關(guān)鍵字,這時候問題又來了,為什么標(biāo)識符let和const的行為會不同呢?這個鍋說到底還是得ES5規(guī)范背,在ES5規(guī)范中const被認(rèn)為是未來保留字(FutureReservedWords)而let只有在嚴(yán)格模式下才被認(rèn)為是未來保留字,這導(dǎo)致var可以聲明let卻不能聲明const,那到了ES6時代為什么不改呢?哎!不是不改而是心有力而余不足啊,鬼知道在ES6時代之前有多少代碼中出現(xiàn)過var let這個聲明啊,這要是改了得有多少網(wǎng)站得炸啊。
基于上面的原因,你看到下面的代碼時不要驚訝:
var let = 1; console.log(let); //1 let a = 2; console.log(a); //2 //看著怪異但是完全可以工作,不會有任何錯誤
看完上面一個不同點,我們再看下面這個例子:
var a; console.log(a); //undefined // let a; console.log(a); //undefined // const a; console.log(a); //?
我們都知道如果var和let只聲明變量而不賦值,那么默認(rèn)賦值undefined,那么const會怎樣呢?
你在Chrome控制臺上試一下就知道了,語法錯誤缺少初始化,ES6規(guī)范指出const聲明的標(biāo)識符一定要初始化賦值,這不是運行時錯誤,這是個早期錯誤,編譯器在執(zhí)行腳本之前會檢測早期錯誤。
我們接著看下一個問題:
let a = 1; let a = 2;
var可以重復(fù)聲明變量,那么let和const可以嗎?答案是不可以。你可以認(rèn)為let和const聲明的變量名稱在該作用域內(nèi)是唯一的,不能重復(fù)聲明。那如果用var可以覆蓋let聲明的變量嗎?答案是不能。不管你是let或const先聲明變量var后面重復(fù)聲明,還是var先聲明變量let或const后聲明都會報錯。這個錯誤是一個早期錯誤。
塊(Block)注意:let/const跨腳本聲明重復(fù)變量也會報錯。但這個時候的錯誤被認(rèn)為是運行時錯誤,不是早期錯誤。上面所指的let/const聲明都指在同一作用域下。
上面列出了var、let、const靜態(tài)語義上的區(qū)別。在該小節(jié)中我會講述在javascript內(nèi)部它們之間的不同,不過在此我們先要了解(塊)Block,可以說let、const是因為Block存在的。
不過提到Block之前我們需要花幾分鐘了解幾個名詞:
我拿個例子簡單說明一下:
//全局聲明 var a=1; let b=1; const c=1; function foo(){}; class Foo{}; { //塊級聲明 var ba=1; let bb=1; const bc=1; class BFoo{}; function bfoo(){} }
LexicallyDeclaredNames(詞法聲明名稱列表):? bb,bc,bfoo,BFoo ?
LexicallyScopedDeclarations(詞法作用域聲明列表):? let bb=1,const bc=1,function bfoo(){},class BFoo{} ?
VarDeclaredNames(var聲明名稱列表):? ba ?
VarScopedDeclarations(var作用域聲明列表):? ba=1 ?
TopLevelLexicallyDeclaredNames(頂級詞法聲明名稱列表):? b,c,Foo ?
TopLevelLexicallyScopedDeclarations(頂級詞法作用域聲明列表):? let b=1,const c=1,class Foo{} ?
TopLevelVarDeclaredNames(頂級var聲明名稱列表):? a,ba,bfoo ?
TopLevelVarScopedDeclarations(頂級var作用域聲明列表):? a=1,ba=1,function foo(){}?
注:? ?結(jié)構(gòu)是ECMAScript中的一個規(guī)范類型,表示一個List,具體你可以認(rèn)為它是一個類數(shù)組(當(dāng)然實際肯定不是,只是方便理解)
有沒有看到怪異的地方?function聲明在頂級作用域(TopLevel)中被視為var聲明,而不在頂級作用域也就是Block或catch塊中被認(rèn)為是詞法聲明,這就導(dǎo)致了一些有趣的事情。
Block只有前四個列表,函數(shù)(function)和腳本(script)只有后四個列表(其實函數(shù)和腳本也只有前四個,不過前四個列表的值取的是后四個列表的值)。Block雖然有自己的作用域但是它和函數(shù)有著本質(zhì)上的區(qū)別。函數(shù)和腳本你可以看成是相互獨立的而Block是屬于function和script的一部分。具體就是Block中的var聲明同時也被認(rèn)為是頂級聲明,不管你嵌了多少層塊在里面都不會變,因為Block沒有頂級作用域。
理解了上面的8個名稱,我們再來看看Block中的聲明與function和script中有何不同:
LexicallyDeclaredNames中如果包含任何重復(fù)項,則語法錯誤。
LexicallyDeclaredNames中出現(xiàn)的任何元素在VarDeclaredNames聲明中出現(xiàn),語法錯誤。
規(guī)則1很正常,LexicallyDeclaredNames這個列表里不能有重復(fù)項,即不能重復(fù)聲明。
規(guī)則2這就很有意思了,我們上面說到了在Block中function聲明屬于詞法聲明,于是你會在Block中看到:
{ var foo=1; function foo(){} //Syntax Error,var和function不能聲明同一個標(biāo)識符,腳本和函數(shù)中是不存在這個問題的。 //我大膽推測一下,可能在不久的將來腳本和函數(shù)中var和function也不能聲明同一個標(biāo)識符了。 }
補(bǔ)充規(guī)則1中function聲明
{ function a(){}; function a(){}; //it"s ok,no syntax Error } //----------------------- "use strict"; { function a(){}; function a(){}; //error, syntax Error redeclaration a; }
這里我不得不吐槽一下了,就因為在非嚴(yán)格模式下Block中的function可以重復(fù)聲明害我以為規(guī)范1我理解錯了,導(dǎo)致我把文檔中有關(guān)Block規(guī)范說明部分翻來覆去看了好幾遍,最后我才在規(guī)范文檔的附錄中找到原因:為了實現(xiàn)網(wǎng)頁瀏覽器的兼容性,允許在非嚴(yán)格模式下的Block中的function可以重復(fù)聲明。
這里有個建議,最好永遠(yuǎn)不要在一個作用域內(nèi)同時使用var和let/const聲明,還有不要在Block中使用var聲明,至于Block中的function聲明,除非你確切的知道你需要這個function做什么,否則也不要在Block中使用function。Block中的function是如此的怪異。
1.非嚴(yán)格模式下,block中的function聲明的標(biāo)識符會被提到頂級作用域下,但是只提標(biāo)識符,并賦值undefined,不提函數(shù)體。你可以把它看成是一個var聲明的變量,具體如下:
console.log(foo); //undefined { function foo(){ console.log(1); } } foo(); //1
2.非嚴(yán)格模式下,block中的function聲明的函數(shù)對象對這個block來說形成了一個閉包,我認(rèn)為‘閉包’這個詞是最好的解釋:
var a = "outer a"; { let a = "inner a"; function foo(){ console.log(a); } } console.log(a) //outer a foo(); //inner a, not outer a
3.嚴(yán)格模式下,block中的function聲明只能在block中訪問到,離開這個block無法訪問:
"use strict"; console.log(foo); //Uncaught ReferenceError: foo is not defined { function foo(){ console.log(1); } } foo(); //Uncaught ReferenceError: foo is not defined
出現(xiàn)這種情況是因為ES5之前,block中不能出現(xiàn)function聲明,但是不同的瀏覽器實現(xiàn)不一樣,到了現(xiàn)在只能通過瀏覽器擴(kuò)展進(jìn)行填補(bǔ)。在非嚴(yán)格模式下,編譯器進(jìn)行全局聲明實例化是也就是上篇文章中說道的GlobalDeclarationInstantiation方法時會對block、switch中case和default語句中的function聲明進(jìn)行額外的操作,如果function聲明的標(biāo)識符在全局環(huán)境下沒有找打其它的詞法聲明名稱即在TopLevelLexicallyDeclaredNames列表中不存在function聲明的標(biāo)識符,則在全局環(huán)境記錄下創(chuàng)建function綁定,但是設(shè)置的值不是聲明的函數(shù)體而是是undefined。函數(shù)中有相似的操作。
block中的一些注意點以及和function還有script中的區(qū)別我大致講了一下。那么block是如何做到有塊級作用域的功能的呢?
我在上一篇文章中講到了執(zhí)行上下文,提到執(zhí)行上下文是編譯器用來跟蹤代碼執(zhí)行時評估的一種規(guī)范設(shè)備,每個執(zhí)行上下文都有自己的LexicalEnvironment和VariableEnvironment組件。編譯器在評估Block做了如下操作:
讓oldEnv成為正在運行的執(zhí)行上下文(running execution context)的LexicalEnvironment。
讓blockEnv成為一個新的聲明性環(huán)境,它的外部詞法環(huán)境引用指向oldEnv。
對block中的聲明進(jìn)行實例化。
把正在運行的執(zhí)行上下文(running execution context)的LexicalEnvironment設(shè)為blockEnv。
讓blockValue成為執(zhí)行block中的代碼的結(jié)果。
把正在運行的執(zhí)行上下文(running execution context)的LexicalEnvironment設(shè)為oldEnv。
返回blockValue。
我們看到了執(zhí)行block中代碼時不會新建執(zhí)行上下文,它只是改變了正在運行的執(zhí)行上下文的LexicalEnvironment組件值,block運行完成后又恢復(fù)成以前的LexicalEnvironment組件,這指明了block中聲明的變量只在該block中起作用,這也表示為什么block是塊級作用域。這跟函數(shù)不一樣,執(zhí)行函數(shù)時會創(chuàng)建新的執(zhí)行上下文。
我這再說明一下,步驟3中的聲明進(jìn)行實例化指得是LexicallyScopedDeclarations列表中的聲明,block不會對其中的var聲明進(jìn)行操作。步驟5中的blockValue指得是block中最后一個語句執(zhí)行后的返回值。
知道了這個,我們來看個let和var在Block中的不同:
for(var i = 0;i < 10;i++){ setTimeout(function(){console.log(i)}) } //輸出10個10 for(let i=0;i<10;i++){ setTimeout(function(){console.log(i)}) } //輸出0到9
我這邊做個簡單說明:
把全局環(huán)境記錄記gec,for循環(huán)里的環(huán)境記錄記為bec,匿名函數(shù)的環(huán)境記錄記為fec。
gec的外部環(huán)境null,bec的外部環(huán)境gec,fec的外部環(huán)境bec。
第一個for循環(huán)中函數(shù)輸出i,fec中沒有i的記錄,向外找bec,沒有i的記錄,向外找找gec,發(fā)現(xiàn)i,值為10,所以輸出10個10。
第二個for循環(huán)中函數(shù)輸出i,fec中沒有i的記錄,向外找bec,找到i的記錄,并輸出i,這個i是當(dāng)前bec記錄中i的值,每次循環(huán)都會創(chuàng)建一個新的bec記錄。
變量提升(Hoisting)我們都知道var和function聲明在作用域內(nèi)存在著變量提升,但是let/const或者class呢?究竟有沒有存在變量提升。這個問題存在著爭議,可謂仁者見仁智者見智。
我在上篇文章中提到了全局聲明實例化和block中的block聲明實例化以及沒有提到的function聲明實例化,你會發(fā)現(xiàn)一個關(guān)鍵,就是這些操作都是在執(zhí)行代碼之前做的,全局聲明實例化在腳本執(zhí)行之前進(jìn)行,block聲明實例化在block中的代碼執(zhí)行之前進(jìn)行,包括函數(shù)也是如此。那么聲明實例化究竟是做什么的呢?
具體的操作就是把存在LexicallyScopedDeclarations、VarScopedDeclarations、TopLevelLexicallyScopedDeclarations和TopLevelVarScopedDeclarations的信息進(jìn)行操作,存到環(huán)境記錄中。這些詞都是靜態(tài)語義,也就在在腳本執(zhí)行之前就已經(jīng)存儲了。
var a = 1; let b = 1; //執(zhí)行代碼前環(huán)境記錄(Environment Record)綁定了a,b,并給a賦值為undefined,b不賦值。 //注:let、const和class只綁定(實例化)不初始化,var和function會進(jìn)行初始化,function初始化指的就是整個函數(shù)。 //執(zhí)行代碼時---------------- console.log(a); //undefined 環(huán)境記錄中有a的這個綁定,并且值是undefined,所以輸出undefined var a = 1; //---------------- console.log(a); //Uncaught ReferenceError: a is not defined 環(huán)境記錄中有a的這個綁定,但是沒有值,所以error。 //可能a is not defined改為a is not initialized更能讓人容易理解。 // not defined容易和undefined混淆。 let a = 1; //一個更好的例子 var a = 1; { console.log(a); //Uncaught ReferenceError: a is not defined,not value 1; let a = 2; //let聲明的變量實際上也提升了 }
正是這樣原因?qū)е隆白兞刻嵘贝嬖跔幾h,一部分人認(rèn)為let、const、class和var一樣,在一開始就已經(jīng)提升了,所以let、const、class存在“變量提升”。有的人認(rèn)為所謂“變量提升”,是指代碼不報錯,還能運行,而let、const、class會出現(xiàn)錯誤,所以不能算“變量提升”。
ECMAScript規(guī)范一直沒有給出準(zhǔn)確的說明,甚至不同版本說法不一樣,在最新的ES8規(guī)范中雖然沒有給出準(zhǔn)確的說明,但是規(guī)范定義了一個HoistableDeclaration文法,該文法中包含了FunctionDeclaration、GeneratorDeclaration和AsyncFunctionDeclaration文法。HoistableDeclaration文法又與ClassDeclaration和LexicalDeclaration(let/const的語法規(guī)則)文法組成Declaration文法。
這里是不是可以推斷出ECMAScript規(guī)范認(rèn)為let、const和class不存在“變量提升”呢。當(dāng)然這只是我的一個推測。
結(jié)束語到這里let/const和var的解釋基本就完結(jié)了。我大致的對let/const以及var做了一個區(qū)別介紹,但是還有很多小的細(xì)節(jié)不能涵蓋到,如果感興趣想了解更多的話可以查看官方文檔13.2 Block和13.3 let/const和var。
算上最開始的javascript強(qiáng)制轉(zhuǎn)化,這是我對ES8文檔講解的第三篇文章,之后我會陸續(xù)發(fā)表一些我對ES8文檔的理解,希望能與人一起交流共進(jìn)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/90105.html
摘要:前言和的區(qū)別是老生常談,看到網(wǎng)上一些文章的總結(jié),有的不太全面,甚至有的描述不太準(zhǔn)確,在這里盡量全面的總結(jié)下這三者的區(qū)別。最后以上大概是總結(jié)后的內(nèi)容,看來,還是多用吧。 前言 var 和 let 的區(qū)別是老生常談,看到網(wǎng)上一些文章的總結(jié),有的不太全面,甚至有的描述不太準(zhǔn)確,在這里盡量全面的總結(jié)下這三者的區(qū)別。 let 是 ES6新增的變量類型,用來代替 var 的一些缺陷,跟 var...
摘要:在的閉包中,閉包函數(shù)能夠訪問到包庇函數(shù)中的變量,這些閉包函數(shù)能夠訪問到的變量也因此被稱為自由變量。在之前最常見的兩種作用域,全局作用局和函數(shù)作用域局部作用域。 關(guān)于文章討論請訪問:https://github.com/Jocs/jocs.... 當(dāng)Brendan Eich在1995年設(shè)計JavaScript第一個版本的時候,考慮的不是很周到,以至于最初版本的JavaScript有很多不...
摘要:會出現(xiàn)這樣的情況是因為擁有暫時性死區(qū)。規(guī)定暫時性死區(qū)和語句不出現(xiàn)變量提升,主要是為了減少運行時錯誤,防止在變量聲明前就使用這個變量,從而導(dǎo)致意料之外的行為。 首先我們應(yīng)該知道js引擎在讀取js代碼時會進(jìn)行兩個步驟: 第一個步驟是解釋。 第二個步驟是執(zhí)行。 所謂解釋就是會先通篇掃描所有的Js代碼,然后把所有聲明提升到頂端,第二步是執(zhí)行,執(zhí)行就是操作一類的。 我們先來看個簡單的變量提升...
摘要:概述發(fā)布前,只能通過聲明變量的方式,常量塊級變量函數(shù)變量這些概念的差別都不能很好的體現(xiàn)出來,于此同時,加入你要使用或者提供一個,聲明的變量可隨時被修改和重新分配的問題,會讓你時刻擔(dān)心代碼是否能正常運行。 1. var、let、const概述 ES6發(fā)布前,Javascript只能通過var聲明變量的方式,常量、塊級變量、函數(shù)變量這些概念的差別都不能很好的體現(xiàn)出來,于此同時,加入你要使用...
摘要:變量提升是在預(yù)編譯的過程中發(fā)生的,賦值為被聲明的變量還是在原來的地方,真正被賦值塊級聲明塊級聲明用于聲明在指定塊的作用域之外無法訪問的變量。只有執(zhí)行變量聲明語句后,變量才會從中移出,然后才可以正常訪問。 在代碼中,聲明變量是基礎(chǔ),但是在javascript中,經(jīng)歷了從var到let,const的變化,到底有什么本質(zhì)上的區(qū)別呢? 本文的原文在我的博客中:https://github.co...
閱讀 2219·2021-11-22 13:54
閱讀 3382·2019-08-29 12:25
閱讀 3445·2019-08-28 18:29
閱讀 3591·2019-08-26 13:40
閱讀 3280·2019-08-26 13:32
閱讀 966·2019-08-26 11:44
閱讀 2236·2019-08-23 17:04
閱讀 2978·2019-08-23 17:02