摘要:在的閉包中,閉包函數(shù)能夠訪問(wèn)到包庇函數(shù)中的變量,這些閉包函數(shù)能夠訪問(wèn)到的變量也因此被稱(chēng)為自由變量。在之前最常見(jiàn)的兩種作用域,全局作用局和函數(shù)作用域局部作用域。
關(guān)于文章討論請(qǐng)?jiān)L問(wèn):https://github.com/Jocs/jocs....
當(dāng)Brendan Eich在1995年設(shè)計(jì)JavaScript第一個(gè)版本的時(shí)候,考慮的不是很周到,以至于最初版本的JavaScript有很多不完善的地方,在Douglas Crockford的《JavaScript:The Good Parts》中就總結(jié)了很多JavaScript不好的地方,比如允許!=和==的使用,會(huì)導(dǎo)致隱式的類(lèi)型轉(zhuǎn)換,比如在全局作用域中通過(guò)var聲明變量會(huì)成為全局對(duì)象(在瀏覽器環(huán)境中是window對(duì)象)的一個(gè)屬性,在比如var聲明的變量可以覆蓋window對(duì)象上面原生的方法和屬性等。
但是作為一門(mén)已經(jīng)被廣泛用于web開(kāi)發(fā)的計(jì)算機(jī)語(yǔ)言來(lái)說(shuō),去糾正這些設(shè)計(jì)錯(cuò)誤顯得相當(dāng)困難,因?yàn)槿绻碌恼Z(yǔ)法和老的語(yǔ)法有沖突的話,那么已有的web應(yīng)用無(wú)法運(yùn)行,瀏覽器生產(chǎn)廠商肯定不會(huì)去冒這個(gè)險(xiǎn)去實(shí)現(xiàn)這些和老的語(yǔ)法完全沖突的功能的,因?yàn)檎l(shuí)都不想失去自己的客戶,不是嗎?因此向下兼容便成了解決上述問(wèn)題的唯一途徑,也就是說(shuō)在不改變?cè)姓Z(yǔ)法特性的基礎(chǔ)上,增加一些新的語(yǔ)法或變量聲明方式等,來(lái)把新的語(yǔ)言特性引入到JavaScript語(yǔ)言中。
早在九年前,Brendan Eich在Firefox中就實(shí)現(xiàn)了第一版的let.但是let的功能和現(xiàn)有的ES2015標(biāo)準(zhǔn)規(guī)定有些出入,后來(lái)由Shu-yu Guo將let的實(shí)現(xiàn)升級(jí)到符合現(xiàn)有的ES2015標(biāo)準(zhǔn),現(xiàn)在才有了我們現(xiàn)在在最新的Firefox中使用的let 聲明變量語(yǔ)法。
問(wèn)題一:沒(méi)有塊級(jí)作用域在ES2015之前,在函數(shù)中通過(guò)var聲明的變量,不論其在{}中還是外面,其都可以在整個(gè)函數(shù)范圍內(nèi)訪問(wèn)到,因此在函數(shù)中聲明的變量被稱(chēng)為局部變量,作用域被稱(chēng)為局部作用域,而在全局中聲明的變量存在整個(gè)全局作用域中。但是在很多情境下,我們迫切的需要塊級(jí)作用域的存在,也就是說(shuō)在{}內(nèi)部聲明的變量只能夠在{}內(nèi)部訪問(wèn)到,在{}外部無(wú)法訪問(wèn)到其內(nèi)部聲明的變量,比如下面的例子:
function foo() { var bar = "hello" if (true) { var zar = "world" console.log(zar) } console.log(zar) // 如果存在塊級(jí)作用域那么將報(bào)語(yǔ)法錯(cuò)誤:Uncaught ReferenceError }
在上面的例子中,如果JavaScript在ES2015之前就存在塊級(jí)作用域,那么在{}之外將無(wú)法訪問(wèn)到其內(nèi)部聲明的變量zar,但是實(shí)際上,第二個(gè)console卻打印了zar的賦值,"world"。
問(wèn)題二:for循環(huán)中共享迭代變量值在for循環(huán)初始循環(huán)變量時(shí),如果使用var聲明初始變量i,那么在整個(gè)循環(huán)中,for循環(huán)內(nèi)部將共享i的值。如下代碼:
var funcs = [] for (var i = 0; i < 10; i++) { funcs.push(function() { return i }) } funcs.forEach(function(f) { console.log(f()) // 將在打印10數(shù)字10次 })
上面的代碼并沒(méi)有按著我們希望的方式執(zhí)行,我們本來(lái)希望是最后打印0、1、2...9這10個(gè)數(shù)字。但是最后的結(jié)果卻出乎我們的意料,而是將數(shù)字10打印了10次,究其原因,聲明的變量i在上面的整個(gè)代碼塊能夠訪問(wèn)到,也就是說(shuō),funcs數(shù)組中每一個(gè)函數(shù)返回的i都是全局聲明的變量i。也就說(shuō)在funcs中函數(shù)執(zhí)行時(shí),將返回同一個(gè)值,而變量i初始值為0,當(dāng)?shù)詈笠淮芜M(jìn)行累加,9+1 = 10時(shí),通過(guò)條件語(yǔ)句i < 10判斷為false,循環(huán)運(yùn)行完畢。最后i的值為10.也就是為什么最后所有的函數(shù)都打印為10。那么在ES2015之前能夠使上面的循環(huán)打印0、1、2、… 9嗎?答案是肯定的。
var funcs = [] for (var i = 1; i < 10; i++) { funcs.push((function(value) { return function() { return value } })(i)) } funcs.forEach(function(f) { console.log(f()) })
在這兒我們使用了JavaScript中的兩個(gè)很棒的特性,立即執(zhí)行函數(shù)(IIFEs)和閉包(closure)。在JavaScript的閉包中,閉包函數(shù)能夠訪問(wèn)到包庇函數(shù)中的變量,這些閉包函數(shù)能夠訪問(wèn)到的變量也因此被稱(chēng)為自由變量。只要閉包沒(méi)有被銷(xiāo)毀,那么外部函數(shù)將一直在內(nèi)存中保存著這些變量,在上面的代碼中,形參value就是自由變量,return的函數(shù)是一個(gè)閉包,閉包內(nèi)部能夠訪問(wèn)到自由變量value。同時(shí)這兒我們還使用了立即執(zhí)行函數(shù),立即函數(shù)的作用就是在每次迭代的過(guò)程中,將i的值作為實(shí)參傳入立即執(zhí)行函數(shù),并執(zhí)行返回一個(gè)閉包函數(shù),這個(gè)閉包函數(shù)保存了外部的自由變量,也就是保存了當(dāng)次迭代時(shí)i的值。最后,就能夠達(dá)到我們想要的結(jié)果,調(diào)用funcs中每個(gè)函數(shù),最終返回0、1、2、… 9。
問(wèn)題三:變量提升(Hoisting)我們先來(lái)看看函數(shù)中的變量提升, 在函數(shù)中通過(guò)var定義的變量,不論其在函數(shù)中什么位置定義的,都將被視作在函數(shù)頂部定義,這一特定被稱(chēng)為提升(Hoisting)。想知道變量提升具體是怎樣操作的,我們可以看看下面的代碼:
function foo() { console.log(a) // undefined var a = "hello" console.log(a) // "hello" }
在上面的代碼中,我們可以看到,第一個(gè)console并沒(méi)有報(bào)錯(cuò)(ReferenceError)。說(shuō)明在第一個(gè)console.log(a)的時(shí)候,變量a已經(jīng)被定義了,JavaScript引擎在解析上面的代碼時(shí)實(shí)際上是像下面這樣的:
function foo() { var a console.log(a) a = "hello" console.log(a) }
也就是說(shuō),JavaScript引擎把變量的定義和賦值分開(kāi)了,首先對(duì)變量進(jìn)行提升,將變量提升到函數(shù)的頂部,注意,這兒變量的賦值并沒(méi)有得到提升,也就是說(shuō)a = "hello"依然是在后面賦值的。因此第一次console.log(a)并沒(méi)有打印hello也沒(méi)有報(bào)ReferenceError錯(cuò)誤。而是打印undefined。無(wú)論是函數(shù)內(nèi)部還是外部,變量提升都會(huì)給我們帶來(lái)意想不到的bug。比如下面代碼:
if (!("a" in window)) { var a = "hello" } console.log(a) // undefined
很多公司都把上面的代碼作為面試前端工程師JavaScript基礎(chǔ)的面試題,其考點(diǎn)也就是考察全局環(huán)境下的變量提升,首先,答案是undefined,并不是我們期許的hello。原因就在于變量a被提升到了最上面,上面的代碼JavaScript其實(shí)是這樣解析的:
var a if (!("a" in window)) { a = "hello" } console.log(a) // undefined
現(xiàn)在就很明了了,bianlianga被提升到了全局環(huán)境最頂部,但是變量a的賦值還是在條件語(yǔ)句內(nèi)部,我們知道通過(guò)關(guān)鍵字var在全局作用域中聲明的變量將作為全局對(duì)象(window)的一個(gè)屬性,因此"a" in window為true。所以if語(yǔ)句中的判斷語(yǔ)句就為false。因此條件語(yǔ)句內(nèi)部就根本不會(huì)執(zhí)行,也就是說(shuō)不會(huì)執(zhí)行賦值語(yǔ)句。最后通過(guò)console.log(a)打印也就是undefined,而不是我們想要的hello。
雖然使用關(guān)鍵詞let進(jìn)行變量聲明也會(huì)有變量提升,但是其和通過(guò)var申明的變量帶來(lái)的變量提升是不一樣的,這一點(diǎn)將在后面的let和var的區(qū)別中討論到。
關(guān)于ES2015之前作用域的概念上面提及的一些問(wèn)題,很多都是由于JavaScript中關(guān)于作用域的細(xì)分粒度不夠,這兒我們稍微回顧一下ES2015之前關(guān)于作用域的概念。
Scope: collects and maintains a look-up list of all the declared identifiers (variables), and enforces a strict set of rules as to how these are accessible to currently executing code.
上面是關(guān)于作用域的定義,作用域就是一些規(guī)則的集合,通過(guò)這些規(guī)則我們能夠查找到當(dāng)前執(zhí)行代碼所需變量的值,這就是作用域的概念。在ES2015之前最常見(jiàn)的兩種作用域,全局作用局和函數(shù)作用域(局部作用域)。函數(shù)作用域可以嵌套,這樣就形成了一條作用域鏈,如果我們自頂向下的看,一個(gè)作用域內(nèi)部可以嵌套幾個(gè)子作用域,子作用域又可以嵌套更多的作用域,這就更像一個(gè)‘’作用域樹(shù)‘’而非作用域鏈了,作用域鏈?zhǔn)且粋€(gè)自底向上的概念,在變量查找的過(guò)程中很有用的。在ES3時(shí),引入了try catch語(yǔ)句,在catch語(yǔ)句中形成了新的作用域,外部是訪問(wèn)不到catch語(yǔ)句中的錯(cuò)誤變量。代碼如下:
try { throw new Error() } catch(err) { console.log(err) } console.log(err) //Uncaught ReferenceError
再到ES5的時(shí)候,在嚴(yán)格模式下(use strict),函數(shù)中使用eval函數(shù)并不會(huì)再在原有函數(shù)中的作用域中執(zhí)行代碼或變量賦值了,而是會(huì)動(dòng)態(tài)生成一個(gè)作用域嵌套在原有函數(shù)作用域內(nèi)部。如下面代碼:
"use strict" var a = function() { var b = "123" eval("var c = 456;console.log(c + b)") // "456123" console.log(b) // "123" console.log(c) // 報(bào)錯(cuò) }
在非嚴(yán)格模式下,a函數(shù)內(nèi)部的console.log(c)是不會(huì)報(bào)錯(cuò)的,因?yàn)閑val會(huì)共享a函數(shù)中的作用域,但是在嚴(yán)格模式下,eval將會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)新的子作用域嵌套在a函數(shù)內(nèi)部,而外部是訪問(wèn)不到這個(gè)子作用域的,也就是為什么console.log(c)會(huì)報(bào)錯(cuò)。
通過(guò)let來(lái)聲明變量通過(guò)let關(guān)鍵字來(lái)聲明變量也通過(guò)var來(lái)聲明變量的語(yǔ)法形式相同,在某些場(chǎng)景下你甚至可以直接把var替換成let。但是使用let來(lái)申明變量與使用var來(lái)聲明變量最大的區(qū)別就是作用域的邊界不再是函數(shù),而是包含let變量聲明的代碼塊({})。下面的代碼將說(shuō)明let聲明的變量只在代碼塊內(nèi)部能夠訪問(wèn)到,在代碼塊外部將無(wú)法訪問(wèn)到代碼塊內(nèi)部使用let聲明的變量。
if (true) { let foo = "bar" } console.log(foo) // Uncaught ReferenceError
在上面的代碼中,foo變量在if語(yǔ)句中聲明并賦值。if語(yǔ)句外部卻訪問(wèn)不到foo變量,報(bào)ReferenceError錯(cuò)誤。
let和var的區(qū)別在ECMAScript 2015中,let也會(huì)提升到代碼塊的頂部,在變量聲明之前去訪問(wèn)變量會(huì)導(dǎo)致ReferenceError錯(cuò)誤,也就是說(shuō),變量被提升到了一個(gè)所謂的“temporal dead zone”(以下簡(jiǎn)稱(chēng)TDZ)。TDZ區(qū)域從代碼塊開(kāi)始,直到顯示得變量聲明結(jié)束,在這一區(qū)域訪問(wèn)變量都會(huì)報(bào)ReferenceError錯(cuò)誤。如下代碼:
function do_something() { console.log(foo); // ReferenceError let foo = 2; }
而通過(guò)var聲明的變量不會(huì)形成TDZ,因此在定義變量之前訪問(wèn)變量只會(huì)提示undefined,也就是上文以及討論過(guò)的var的變量提升。
在全局環(huán)境中,通過(guò)var聲明的變量會(huì)成為window對(duì)象的一個(gè)屬性,甚至對(duì)一些原生方法的賦值會(huì)導(dǎo)致原生方法的覆蓋。比如下面對(duì)變量parseInt進(jìn)行賦值,將覆蓋原生parseInt方法。
var parseInt = function(number) { return "hello" } parseInt(123) // "hello" window.parseInt(123) // "hello"
而通過(guò)關(guān)鍵字let在全局環(huán)境中進(jìn)行變量聲明時(shí),新的變量將不會(huì)成為全局對(duì)象的一個(gè)屬性,因此也就不會(huì)覆蓋window對(duì)象上面的一些原生方法了。如下面的例子:
let parseInt = function(number) { return "hello" } parseInt(123) // "hello" window.parseInt(123) // 123
在上面的例子中,我們看到let生命的函數(shù)parsetInt并沒(méi)有覆蓋window對(duì)象上面的parseInt方法,因此我們通過(guò)調(diào)用window.parseInt方法時(shí),返回結(jié)果123。
在ES2015之前,可以通過(guò)var多次聲明同一個(gè)變量而不會(huì)報(bào)錯(cuò)。下面的代碼是不會(huì)報(bào)錯(cuò)的,但是是不推薦的。
var a = "xiaoming" var a = "huangxiaoming"
其實(shí)這一特性不利于我們找出程序中的問(wèn)題,雖然有一些代碼檢測(cè)工具,比如ESLint能夠檢測(cè)到對(duì)同一個(gè)變量進(jìn)行多次聲明賦值,能夠大大減少我們程序出錯(cuò)的可能性,但畢竟不是原生支持的。不用擔(dān)心,ES2015來(lái)了,如果一個(gè)變量已經(jīng)被聲明,不論是通過(guò)var還是let或者const,該變量再次通過(guò)let聲明時(shí)都會(huì)語(yǔ)法報(bào)錯(cuò)(SyntaxError)。如下代碼:
var a = 345 let a = 123 // Uncaught SyntaxError: Identifier "a" has already been declared最好的總是放在最后:const
通過(guò)const生命的變量將會(huì)創(chuàng)建一個(gè)對(duì)該值的一個(gè)只讀引用,也就是說(shuō),通過(guò)const聲明的原始數(shù)據(jù)類(lèi)型(number、string、boolean等),聲明后就不能夠再改變了。通過(guò)const聲明的對(duì)象,也不能改變對(duì)對(duì)象的引用,也就是說(shuō)不能夠再將另外一個(gè)對(duì)象賦值給該const聲明的變量,但是,const聲明的變量并不表示該對(duì)象就是不可變的,依然可以改變對(duì)象的屬性值,只是該變量不能再被賦值了。
const MY_FAV = 7 MY_FAY = 20 // 重復(fù)賦值將會(huì)報(bào)錯(cuò)(Uncaught TypeError: Assignment to constant variable) const foo = {bar: "zar"} foo.bar = "hello world" // 改變對(duì)象的屬性并不會(huì)報(bào)錯(cuò)
通過(guò)const生命的對(duì)象并不是不可變的。但是在很多場(chǎng)景下,比如在函數(shù)式編程中,我們希望聲明的變量是不可變的,不論其是原始數(shù)據(jù)類(lèi)型還是引用數(shù)據(jù)類(lèi)型。顯然現(xiàn)有的變量聲明不能夠滿足我們的需求,如下是一種聲明不可變對(duì)象的一種實(shí)現(xiàn):
const deepFreeze = function(obj) { Object.freeze(obj) for (const key in obj) { if (typeof obj[key] === "object") deepFreeze(obj[key]) } return obj } const foo = deepFreeze({ a: {b: "bar"} }) foo.a.b = "zar" console.log(foo.a.b) // bar最佳實(shí)踐
在ECMAScript 2015成為最新標(biāo)準(zhǔn)之前,很多人都認(rèn)為let是解決本文開(kāi)始羅列的一系列問(wèn)題的最佳方案,對(duì)于很多JavaScript開(kāi)發(fā)者而言,他們認(rèn)為一開(kāi)始var就應(yīng)該像現(xiàn)在let一樣,現(xiàn)在let出來(lái)了,我們只需要根據(jù)現(xiàn)有的語(yǔ)法把以前代碼中的var換成let就好了。然后使用const聲明那些我們永遠(yuǎn)不會(huì)修改的值。
但是,當(dāng)很多開(kāi)發(fā)者開(kāi)始將自己的項(xiàng)目遷移到ECMAScript2015后,他們發(fā)現(xiàn),最佳實(shí)踐應(yīng)該是,盡可能的使用const,在const不能夠滿足需求的時(shí)候才使用let,永遠(yuǎn)不要使用var。為什么要盡可能的使用const呢?在JavaScript中,很多bug都是因?yàn)闊o(wú)意的改變了某值或者對(duì)象而導(dǎo)致的,通過(guò)盡可能使用const,或者上面的deepFreeze能夠很好地規(guī)避這些bug的出現(xiàn),而我的建議是:如果你喜歡函數(shù)式編程,永遠(yuǎn)不改變已經(jīng)聲明的對(duì)象,而是生成一個(gè)新的對(duì)象,那么對(duì)于你來(lái)說(shuō),const就完全夠用了。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/90856.html
摘要:不允許在相同作用域內(nèi),重復(fù)聲明同一個(gè)變量。如但是在中則不再必要了,我們可以通過(guò)塊級(jí)作用域就能夠?qū)崿F(xiàn)本次主要針對(duì)中的變量和塊級(jí)作用域進(jìn)行了梳理學(xué)習(xí),并且通過(guò)與的實(shí)現(xiàn)方式進(jìn)行了對(duì)比,從而看出其變化以及快捷與便利。 ECMAScript 6.0(以下簡(jiǎn)稱(chēng) ES6)是 JavaScript 語(yǔ)言的下一代標(biāo)準(zhǔn),已經(jīng)在 2015 年 6 月正式發(fā)布了。它的目標(biāo),是使得 JavaScript 語(yǔ)言可...
摘要:字面上是生成器的意思,在里是迭代器生成器,用于生成一個(gè)迭代器對(duì)象。當(dāng)執(zhí)行的時(shí)候,并不執(zhí)行函數(shù)體,而是返回一個(gè)迭代器。迭代器具有方法,每次調(diào)用方法,函數(shù)就執(zhí)行到語(yǔ)句的地方。也有觀點(diǎn)極力反對(duì),認(rèn)為隱藏了本身原型鏈的語(yǔ)言特性,使其更難理解。 本文為 ES6 系列的第一篇。旨在給新同學(xué)一些指引,帶大家走近 ES6 新特性。簡(jiǎn)要介紹: 什么是 ES6 它有哪些明星特性 它可以運(yùn)行在哪些環(huán)境 ...
摘要:它是一個(gè)通用標(biāo)準(zhǔn),奠定了的基本語(yǔ)法。年月發(fā)布了的第一個(gè)版本,正式名稱(chēng)就是標(biāo)準(zhǔn)簡(jiǎn)稱(chēng)。結(jié)語(yǔ)的基本擴(kuò)展還有一些沒(méi)有在這里詳細(xì)介紹。 前言 ES6標(biāo)準(zhǔn)以及頒布兩年了,但是,好像還沒(méi)有完全走進(jìn)我們的日常開(kāi)發(fā)。這篇文章從ES6的基本類(lèi)型擴(kuò)展入手,逐步展開(kāi)對(duì)ES6的介紹。 ECMAScript和JavaScript JavaScript是由Netscape創(chuàng)造的,該公司1996年11月將JavaSc...
摘要:采用二八定律,主要涉及常用且重要的部分。對(duì)象是當(dāng)前模塊的導(dǎo)出對(duì)象,用于導(dǎo)出模塊公有方法和屬性。箭頭函數(shù)函數(shù)箭頭函數(shù)把去掉,在與之間加上當(dāng)我們使用箭頭函數(shù)時(shí),函數(shù)體內(nèi)的對(duì)象,就是定義時(shí)所在的對(duì)象,而不是使用時(shí)所在的對(duì)象。 ES6 原文博客地址:https://finget.github.io/2018/05/10/javascript-es6/ 現(xiàn)在基本上開(kāi)發(fā)中都在使用ES6,瀏覽器環(huán)境...
摘要:新增了兩個(gè)變量修飾關(guān)鍵字它們都是塊級(jí)別的,那什么是塊簡(jiǎn)單的來(lái)說(shuō),塊就是一組花括號(hào)中間的部分。全局變量使用基本上可以不用了 ES2015 新增了兩個(gè)變量修飾關(guān)鍵字: let const 它們都是塊級(jí)別的,那什么是塊?簡(jiǎn)單的來(lái)說(shuō),塊就是一組花括號(hào)中間的部分。 Var 為了理解let我們先從var說(shuō)起,如下代碼: function checkStatus(status) { if (...
閱讀 2426·2021-11-25 09:43
閱讀 1202·2021-09-07 10:16
閱讀 2619·2021-08-20 09:38
閱讀 2944·2019-08-30 15:55
閱讀 1464·2019-08-30 13:21
閱讀 895·2019-08-29 15:37
閱讀 1448·2019-08-27 10:56
閱讀 2097·2019-08-26 13:45