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