摘要:寫在前面對于一個前端開發(fā)者,應(yīng)該沒有不知道作用域的。欺騙詞法作用域有兩個機制可以欺騙詞法作用域和。關(guān)于你不知道的的第一部分作用域和閉包已經(jīng)結(jié)束了,但是,更新不會就此止住未完待續(xù)
這是《你不知道的JavaScript》的第一部分。
本系列持續(xù)更新中,Github 地址請查閱這里。
寫在前面對于一個前端開發(fā)者,應(yīng)該沒有不知道作用域的。它是一個既簡單有復(fù)雜的概念,簡單到每行代碼都有它的影子,復(fù)雜到寫過很多的代碼依然不一定能完全理解。
最近在看《你不知道的JavaScript》,看完之后不寫點什么,好像看的意義就不大了。今天就花點時間,從最簡單又復(fù)雜的作用域開始縷一縷。
先來份關(guān)于《你不知道的JavaScript》第一部分作用域和閉包的目錄感受一下:
第1章 作用域是什么
第2章 詞法作用域
第3章 函數(shù)作用域和塊作用域
第4章 提升
第5章 作用域閉包
真正開始寫的時候發(fā)現(xiàn)好像并沒有什么好寫了,因為這并不是很難的概念,但是要問自己閉包到底是什么,到底有什么用,什么時候場景用的時候,好像又有點模糊了?閉包這個經(jīng)常寫的代碼,居然好像并沒有徹底地理解。
開始之前先問自己幾個問題
你真的理解了作用域嗎?
什么是動態(tài)作用域?什么是詞法作用域?
閉包到底是什么?閉包的應(yīng)用場景有哪些?
怎么實現(xiàn)一個簡單的模塊依賴加載器?
JS是一門解釋型語言,它的編譯過程不是發(fā)生在構(gòu)建之前,那么聲明提升為什么會發(fā)生呢?
如果這些你都知道的話,或許這篇內(nèi)容并不是很需要看了,如果還有一點困惑,希望它能對你有點幫助。
作用域是什么在講作用域之前,我們先來看看什么是編譯。在傳統(tǒng)編譯語言的流程中,編譯分為3步:
分詞/詞法分析:將由字符組成的字符串分解成有意義的代碼塊(詞法單元)
解析/語法分析:將詞法單元流轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表程序語法機構(gòu)的樹(抽象語法樹,AST)
代碼生成:將AST轉(zhuǎn)換成可執(zhí)行代碼
JS是一門解釋型語言,它的編譯過程不是發(fā)生在構(gòu)建之前,大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒(甚至更短)的時間內(nèi),所以在作用域的背后,JS引擎用盡了各種辦法來保證性能最佳。
簡單了解了編譯后,我們再來看看作用域是什么:
作用域負責(zé)收集并維護有所有聲明的標(biāo)識符(變量)組成的一系列查詢,并實施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符的訪問權(quán)限
上面的話不是很好理解,我們來分解一下下面的代碼:
var a = 2; var b = a;
熟悉JS的同學(xué)都知道,包括變量和函數(shù)在內(nèi)的所有聲明都會有一個提升的過程,即首先被處理,但是賦值不會提升。所以上面的代碼會被如下處理:
var a; var b; a = 2; b = a;
當(dāng)編譯器遇到var a時,編譯器會詢問作用域是否存在名稱為a的變量。如果存在,則忽略聲明,繼續(xù)編譯;如果不存在,則聲明一個新變量,命名a。(var b;同理)
當(dāng)遇到a = 2時,引擎會詢問作用域,當(dāng)前作用域中是否存在該變量,如果找不到,就向上一級查找,當(dāng)?shù)诌_最外層的全局作用域時,無論找到還是沒找到,查找都會停止,找到了就賦值2給它,沒找到就拋出異常。
其中,a = 2這一過程中對a的查找被稱為LHS查詢,在b = a這一句中,引擎會查找變量a的值和變量b的容器,并將a的值賦值給b,這一查找變量a的值被稱為RHS查詢。
詞法作用域作用域有兩種工作模式,一種詞法作用域,一種動態(tài)作用域。詞法作用域(也叫靜態(tài)作用域)就是在詞法階段的作用域,詞法分析階段就確定了,不會改變。JS采用的就是詞法作用域,但是可以通過一些欺騙詞法作用域的方法,在詞法分析過后依然可以修改作用域。
詞法作用域與動態(tài)作用域我們先來看看詞法作用域與動態(tài)作用域的區(qū)別(因為JS采用的是詞法作用域,所以對動態(tài)作用域不做過多介紹):
var a = 2; function foo () { console.log(a); // 會輸出2還是3? } function bar () { var a = 3; foo(); } bar();
熟悉JS的同學(xué)應(yīng)該都知道通過RHS引用到了全局作用域中的a,所以輸出2。但是如果JS是動態(tài)作用域,情況就不一樣了,當(dāng)foo()無法找到a的變量引用時,會在調(diào)用foo()的地方查找a,而不是在嵌套的詞法作用域上查找,所以會輸出3。
下面我們來看看什么是欺騙詞法。
欺騙詞法作用域JS有兩個機制可以欺騙詞法作用域:eval()和with。大多數(shù)情況下,它們是不被推薦使用的,因為欺騙詞法作用域?qū)е乱鏌o法在編譯時對作用域查找進行優(yōu)化,所以會導(dǎo)致性能下降;另外,在嚴(yán)格模式下,with被完全禁止使用,間接或非安全的使用eval也被禁止。
eval()這個方法接受一個字符串為參數(shù),并將其中的內(nèi)容視為好像在書寫時就存在于程序中這個位置的代碼。
function foo (str, a) { eval(str); console.log(a, b); } var b = 2; foo("var b =3;", 1); // 1, 3
可以看到 eval() 調(diào)用了 var b =3; 導(dǎo)致修改了原本的作用域。
with 通常被當(dāng)作重復(fù)引用同一個對象中的多個屬性的快捷方式。
function foo (obj) { with (obj) { a = 2; } } var o1 = { a: 3 }; var o2 = { b: 3 }; foo(o1); console.log(o1.a); // 2 foo(o2); console.log(o2.a); // undefined console.log(a); // 2——不好,a被泄漏到全局作用域上了!
with 聲明實際上是根據(jù)你傳遞給它的對象憑空創(chuàng)建了一個全新的詞法作用域
聲明提升前面有個例子說到了聲明提升,我們已經(jīng)知道引擎會在解釋 JavaScript 代碼之前首先對其進行編譯。編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來,這也正詞法作用域的核心內(nèi)容。聲明提升在代碼中比較常見,相信面試過的朋友肯定對它非常熟悉了,但是它也有幾個必須要注意的點:
每個作用域都會進行提升操作,聲明會被提升到所在作用域的頂部(只有聲明會被提升,賦值或其他運行邏輯會留在原地)
并非所有的聲明都會被提升,不同聲明提升的權(quán)重也不同,具體來說函數(shù)聲明會被提升,函數(shù)表達式不會被提升(就算是有名稱的函數(shù)表達式也不會提升),通過var定義的變量會提升,而let和const進行的聲明不會提升
函數(shù)聲明和變量聲明都會被提升。但是一個值得注意的細節(jié)也就是函數(shù)會首先被提升,然后才是變量,也就是說如果一個變量聲明和一個函數(shù)聲明同名,那么就算在語句順序上變量聲明在前,該標(biāo)識符還是會指向相關(guān)函數(shù)
如果變量或函數(shù)有重復(fù)聲明以會第一次聲明為主,但是后面的函數(shù)聲明還是可以覆蓋前面的
閉包閉包到底是什么?
閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)
內(nèi)部函數(shù)總是可以訪問其所在的外部函數(shù)中聲明的參數(shù)和變量,即使在其外部函數(shù)被返回(壽命終結(jié))了之后
閉包這個詞的意思是封閉,將外部作用域中的局部變量封閉起來的函數(shù)對象稱為閉包。被封閉起來的變量與封閉它的函數(shù)對象有相同的生命周期
關(guān)于閉包的解釋,網(wǎng)上有很多,《你不知道的JavaScript》的作者也給出了他的定義:
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時,就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行
看起來不是那么生動的解釋,仔細看看好像也不是很難理解,不過,作為一個程序員,代碼才是王道
function foo () { var a = 2; function bar () { console.log(a); } return bar; } var baz = foo(); baz();
函數(shù)bar()的詞法作用域可以訪問foo()的內(nèi)部作用域,foo()執(zhí)行之后,bar()依然持有對foo()內(nèi)部作用域的引用(也就不會被垃圾回收機制回收),bar()對該作用域的這個引用就被叫做閉包。
再來看看下面這段代碼,有沒有很熟悉的感覺,看過一些面試題的朋友應(yīng)該都不會陌生吧,答案是每隔一秒的頻率輸出五次6
for (var i = 1; i <= 5; i++) { setTimeout(function timer () { console.log(i) }, i * 1000) }
瀏覽器運行機制,任務(wù)隊列之類的我們就不討論了,我們來看看怎么改進,從閉包的角度出現(xiàn)讓它輸出1~5
for (var i = 1; i <= 5; i++) { (function () { setTimeout(function timer () { console.log(i) }, i * 1000) })() }
上面的代碼可以嗎?是的,不行,我們只是封閉了什么都沒有的空作用域中,依然會向上查找全局的i。怎么實現(xiàn)?寫了這么多,我的任務(wù)完成了,輪到你動一下腦瓜子了。
模塊依賴加載器require(["a", "b"], callback)這樣的模塊加載方式有沒有勾起你老人家什么回憶呢?作為一個年輕人,ES6的import大法還是比較適合我,不過前輩當(dāng)時的先進經(jīng)驗還是有很多可以學(xué)習(xí)的地方的,直接貼代碼了
var MyModules = (function Manager () { var modules = {}; function define (name, deps, impl) { for (var i = 0; i < deps.length; i++) { deps[i] = modules[deps[i]]; } modules[name] = impl.apply(impl, deps); } function get (name) { return modules[name]; } return { define: define, get: get } })();
上面實現(xiàn)了一個簡單的模塊加載器,下面是使用它來定義模塊
MyModules.define("bar", [], function () { function hello (who) { return "Let me introduce: " + who; } return { hello: hello }; }); MyModules.define("foo", ["bar"], function (bar) { var hungry = "hippo"; function awesome () { console.log(bar.hello(hungry).toUpperCase()) } return { awesome: awesome }; }); var bar = MyModules.get("bar"); var foo = MyModules.get("foo"); console.log(bar.hello("hippo")); // Let me introduce: hippo foo.awesome(); // LET ME INTRODUCE: HIPPO寫在最后
不知不覺,篇幅已經(jīng)不短,怎么總結(jié)的更精煉確實是一個技術(shù)活,我得好好學(xué)學(xué)才行。本篇主要從編譯、詞法作用域、聲明提升的角度對JS的作用域進行了介紹,并慢慢打開了閉包的大門,最后展示了一個簡單的模塊加載器的代碼。
關(guān)于《你不知道的JavaScript》的第一部分——作用域和閉包已經(jīng)結(jié)束了,但是,更新不會就此止住
未完待續(xù)...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/94791.html
摘要:關(guān)于本書,我會寫好幾篇讀書筆記用以記錄那些讓我恍然大悟的瞬間,本文是第一篇弄懂的作用域和閉包。作用域也可以看做是一套依據(jù)名稱查找變量的規(guī)則。聲明實際上是根據(jù)你傳遞給它的對象憑空創(chuàng)建了一個全新的詞法作用域。 《你不知道的JavaScript》真的是一本好書,閱讀這本書,我有多次哦,原來是這樣的感覺,以前自以為理解了(其實并非真的理解)的概念,這一次真的理解得更加透徹了。關(guān)于本書,我會寫好...
摘要:查詢是在作用域鏈中,一級級的往上查找該變量的引用。作用域和作用域鏈作用域的概念,應(yīng)該兩張圖幾句話就能解釋吧。這個建筑代表程序中的嵌套作用域鏈。一層嵌一層的作用域形成了作用域鏈,變量在作用域鏈中的函數(shù)內(nèi)得到了自己的定義。 javascript作用域和閉包之我見 看了《你不知道的JavaScript(上卷)》的第一部分——作用域和閉包,感受頗深,遂寫一篇讀書筆記加深印象。路過的大牛歡迎指點...
摘要:大名鼎鼎的作用域和閉包,面試經(jīng)常會問到。聲明理解閉包,先理解函數(shù)的執(zhí)行過程。閉包的基本結(jié)構(gòu)因為閉包不允許外界直接訪問,所以只能間接訪問函數(shù)內(nèi)部的數(shù)據(jù),獲得函數(shù)內(nèi)部數(shù)據(jù)的使用權(quán)。 大名鼎鼎的作用域和閉包,面試經(jīng)常會問到。閉包(closure)是Javascript語言的一個難點,也是它的特色。 聲明 理解閉包,先理解函數(shù)的執(zhí)行過程。 代碼在執(zhí)行的過程中會有一個預(yù)解析的過程,也就是在代碼的...
摘要:吐槽一下,閉包這個詞的翻譯真是有很大的誤解性啊要說閉包,要先說下詞法作用域。閉包兩個作用通過閉包,在外部環(huán)境訪問內(nèi)部環(huán)境的變量。閉包使得函數(shù)可以繼續(xù)訪問定義時的詞法作用域。 閉包是真的讓人頭暈啊,看了很久還是覺得很模糊。只能把目前自己的一些理解先寫下來,這其中必定包含著一些錯誤,待日后有更深刻的理解時再作更改。 吐槽一下,閉包這個詞的翻譯真是有很大的誤解性啊…… 要說閉包,要先說下詞法...
摘要:如果是聲明中的第一個詞,那么就是一個函數(shù)聲明,否則就是一個函數(shù)表達式。給函數(shù)表達式指定一個函數(shù)名可以有效的解決以上問題。始終給函數(shù)表達式命名是一個最佳實踐。也有開發(fā)者干脆關(guān)閉了靜態(tài)檢查工具對重復(fù)變量名的檢查。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復(fù)雜微妙技術(shù)的語言,即使是經(jīng)驗豐富的 Ja...
閱讀 1387·2021-10-08 10:04
閱讀 2710·2021-09-22 15:23
閱讀 2733·2021-09-04 16:40
閱讀 1185·2019-08-29 17:29
閱讀 1504·2019-08-29 17:28
閱讀 3001·2019-08-29 14:02
閱讀 2230·2019-08-29 13:18
閱讀 856·2019-08-23 18:35