摘要:如果是聲明中的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式。給函數(shù)表達(dá)式指定一個(gè)函數(shù)名可以有效的解決以上問題。始終給函數(shù)表達(dá)式命名是一個(gè)最佳實(shí)踐。也有開發(fā)者干脆關(guān)閉了靜態(tài)檢查工具對(duì)重復(fù)變量名的檢查。
你不知道的JS(上卷)筆記
你不知道的 JavaScript
JavaScript 既是一門充滿吸引力、簡(jiǎn)單易用的語言,又是一門具有許多復(fù)雜微妙技術(shù)的語言,即使是經(jīng)驗(yàn)豐富的 JavaScript 開發(fā)者,如果沒有認(rèn)真學(xué)習(xí)的話也無法真正理解它們.
上卷包括倆節(jié):
作用域和閉包
this 和對(duì)象原型
作用域和閉包希望 Kyle 對(duì) JavaScript 工作原理每一個(gè)細(xì)節(jié)的批判性思 考會(huì)滲透到你的思考過程和日常工作中。知其然,也要知其所以然。
函數(shù)作用域和塊作用域正如我們?cè)诘?2 章中討論的那樣,作用域包含了一系列的“氣泡”,每一個(gè)都可以作為容 器,其中包含了標(biāo)識(shí)符(變量、函數(shù))的定義。這些氣泡互相嵌套并且整齊地排列成蜂窩 型,排列的結(jié)構(gòu)是在寫代碼時(shí)定義的。
但是,究竟是什么生成了一個(gè)新的氣泡?只有函數(shù)會(huì)生成新的氣泡嗎? JavaScript 中的其 他結(jié)構(gòu)能生成作用域氣泡嗎?
函數(shù)中的作用域JavaScript 具有基于函數(shù)的作用域;
無論標(biāo)識(shí)符 聲明出現(xiàn)在作用域中的何處,這個(gè)標(biāo)識(shí)符所代表的變量或函數(shù)都將附屬于所處作用域的氣 泡。
函數(shù)作用域的含義是指,屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)使用及復(fù) 用(事實(shí)上在嵌套的作用域中也可以使用)。
這種設(shè)計(jì)方案是非常有用的,能充分利用 JavaScript 變量可以根據(jù)需要改變值類型的“動(dòng)態(tài)”特性。這是什么意思?
隱藏的內(nèi)部實(shí)現(xiàn)可以把變量和函數(shù)包裹在一個(gè)函數(shù)的作用域中,然后用這個(gè)作用域 來“隱藏”它們。
Q: 為什么“隱藏”變量和函數(shù)是一個(gè)有用的技術(shù)?
A: 大都是從最小特權(quán)原則中引申出來 的,也叫最小授權(quán)或最小暴露原則。這個(gè)原則是指在軟件設(shè)計(jì)中,應(yīng)該最小限度地暴露必 要內(nèi)容,而將其他內(nèi)容都“隱藏”起來,比如某個(gè)模塊或?qū)ο蟮?API 設(shè)計(jì)。
設(shè)計(jì)上將具體內(nèi)容私有化了,設(shè)計(jì)良好的軟件都會(huì) 依此進(jìn)行實(shí)現(xiàn)。
全局命名空間
通常會(huì)在全局作用域中聲明一個(gè)名字足夠獨(dú)特的變量,通常是一個(gè)對(duì)象。這個(gè)對(duì)象 被用作庫的命名空間,所有需要暴露給外界的功能都會(huì)成為這個(gè)對(duì)象(命名空間)的屬 性,而不是將自己的標(biāo)識(shí)符暴漏在頂級(jí)的詞法作用域中。
模塊管理
任何庫都無需將標(biāo)識(shí)符加入到全局作用域中,而是通過依賴管理器 的機(jī)制將庫的標(biāo)識(shí)符顯式地導(dǎo)入到另外一個(gè)特定的作用域中。
避免同名標(biāo)識(shí)符之間的沖突
函數(shù)作用域在任意代碼片段外部添加包裝函數(shù),可以將內(nèi)部的變量和函數(shù)定義“隱
藏”起來,外部作用域無法訪問包裝函數(shù)內(nèi)部的任何內(nèi)容。
這種技術(shù)可以解決一些問題,但是它并不理想,因?yàn)闀?huì)導(dǎo)致一些額外的問題:
必須聲明一個(gè)具名函數(shù) foo(),意味著 foo 這個(gè)名稱本身“污染”了所在作用域(在這個(gè) 例子中是全局作用域)
必須顯式地通過函數(shù)名(foo())調(diào)用這個(gè)函數(shù)才能運(yùn)行其 中的代碼。
如果函數(shù)不需要函數(shù)名(或者至少函數(shù)名可以不污染所在作用域),并且能夠自動(dòng)運(yùn)行, 這將會(huì)更加理想。
(function foo(){ // <-- 添加這一行 var a = 3; console.log( a ); // 3 })(); // <-- 以及這一行 console.log( a ); // 2
函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別是它們的名稱標(biāo)識(shí)符將會(huì)綁定在何處。
注意:區(qū)分函數(shù)聲明和表達(dá)式最簡(jiǎn)單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明中的位 置(不僅僅是一行代碼,而是整個(gè)聲明中的位置)。如果 function 是聲明中 的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式。
片段中 foo 被綁定在函數(shù)表達(dá)式自身的函數(shù)中而不是所在作用域中。
類似的還有于 +function foo() {}() 對(duì)函數(shù)求值的操作,都能做到避免泄露
換句話說,(function foo(){ .. })作為函數(shù)表達(dá)式意味著foo只能在..所代表的位置中 被訪問,外部作用域則不行。foo 變量名被隱藏在自身中意味著不會(huì)非必要地污染外部作 用域。
setTimeout( function() { console.log("I waited 1 second!"); }, 1000 );
這叫做匿名函數(shù)表達(dá)式, 因?yàn)閒unction()沒有名稱標(biāo)識(shí)符。函數(shù)表達(dá)式可以是匿名的,而函數(shù)聲明則不可以省略函數(shù)名.
匿名函數(shù)表達(dá)式寫起來簡(jiǎn)單快捷,但是它有幾個(gè)缺點(diǎn)需要考慮:
匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難。
如果沒有函數(shù)名,當(dāng)函數(shù)需要引用自身時(shí),只能使用已經(jīng)過期的arguments.callee引用,比如在遞歸中。另一個(gè)函數(shù)需要引用自身的例子是在事件觸發(fā)后事件監(jiān)聽器需要解綁自身。
匿名函數(shù)省略了對(duì)于代碼可讀性/可理解性很重要的函數(shù)名。一個(gè)描述性的名詞可以讓代碼不言自明。
行內(nèi)函數(shù)表達(dá)式非常強(qiáng)大且有用——匿名和具名之間的區(qū)別并會(huì)有對(duì)這點(diǎn)有任何影響。 給函數(shù)表達(dá)式指定一個(gè)函數(shù)名可以有效的解決以上問題。
始終給函數(shù)表達(dá)式命名是一個(gè)最佳實(shí)踐。
setTimeout( function timeoutHandler() { // <-- 快看,我有名字了! console.log( "I waited 1 second!" ); }, 1000 );
幾年前社區(qū)給它規(guī)定了一個(gè)術(shù)語:IIFE,代表立即執(zhí)行函數(shù)表達(dá)式 (Immediately Invoked Function Expression);
IIFE的形式有下面?zhèn)z種:
(function(){ .. })()
(function(){ .. }())
用法1, 把它們當(dāng)作函數(shù)調(diào)用并傳遞參數(shù)進(jìn)去
例如:
var a = 2; (function IIFE( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 })( window ); console.log( a ); // 2
我們將 window 對(duì)象的引用傳遞進(jìn)去,但將參數(shù)命名為 global,因此在代碼風(fēng)格上對(duì)全局 對(duì)象的引用變得比引用一個(gè)沒有“全局”字樣的變量更加清晰。當(dāng)然可以從外部作用域傳 遞任何你需要的東西,并將變量命名為任何你覺得合適的名字。這對(duì)于改進(jìn)代碼風(fēng)格是非 常有幫助的。
用法2,解決 undefined 標(biāo)識(shí)符的默認(rèn)值被錯(cuò)誤覆蓋導(dǎo)致的異常(雖 然不常見)。
例如:將一個(gè)參數(shù)命名為 undefined,但是在對(duì)應(yīng)的位置不傳入任何值,這樣就可以 保證在代碼塊中 undefined 標(biāo)識(shí)符的值真的是 undefined:
undefined = true; // 給其他代碼挖了一個(gè)大坑!絕對(duì)不要這樣做!
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
用法3:倒置代碼的運(yùn)行順序
例如:將需要運(yùn)行的函數(shù)放在第二位,在 IIFE 執(zhí)行之后當(dāng)作參數(shù)傳遞進(jìn)去。這種模式在 UMD(Universal Module Definition)項(xiàng)目中被廣 泛使用。盡管這種模式略顯冗長(zhǎng),但有些人認(rèn)為它更易理解。
var a = 2; (function IIFE( def ) { def( window ); })(function def( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 });塊作用域
塊作用域的用處:變量的聲明應(yīng)該距離使用的地方越近越好,并最大限度地本地化。
塊作用域是一個(gè)用來對(duì)之前的最小授權(quán)原則進(jìn)行擴(kuò)展的工具,將代碼從在函數(shù)中隱藏信息 擴(kuò)展為在塊中隱藏信息。
為什么要把一個(gè)只在 for 循環(huán)內(nèi)部使用(至少是應(yīng)該只在內(nèi)部使用)的變量 i 污染到整個(gè)
函數(shù)作用域中呢?
可惜,表面上看 JavaScript 并沒有塊作用域的相關(guān)功能。
with 關(guān)鍵字。它不僅是一個(gè)難于理解的結(jié)構(gòu),同時(shí)也是塊作用域的一 個(gè)例子(塊作用域的一種形式),用 with 從對(duì)象中創(chuàng)建出的作用域僅在 with 聲明中而非外 部作用域中有效。
非常少有人會(huì)注意到 JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會(huì)創(chuàng)建一個(gè)塊作
用域,其中聲明的變量?jī)H在 catch 內(nèi)部有效。
例如:
try { undefined(); // 執(zhí)行一個(gè)非法操作來強(qiáng)制制造一個(gè)異常 } catch (err) { console.log( err ); // 能夠正常執(zhí)行! } console.log( err ); // ReferenceError: err not found
盡管這個(gè)行為已經(jīng)被標(biāo)準(zhǔn)化,并且被大部分的標(biāo)準(zhǔn) JavaScript 環(huán)境(除了老 版本的 IE 瀏覽器)所支持,但是當(dāng)同一個(gè)作用域中的兩個(gè)或多個(gè) catch 分句 用同樣的標(biāo)識(shí)符名稱聲明錯(cuò)誤變量時(shí),很多靜態(tài)檢查工具還是會(huì)發(fā)出警告。 實(shí)際上這并不是重復(fù)定義,因?yàn)樗凶兞慷急话踩叵拗圃趬K作用域內(nèi)部, 但是靜態(tài)檢查工具還是會(huì)很煩人地發(fā)出警告。為了避免這個(gè)不必要的警告,很多開發(fā)者會(huì)將 catch 的參數(shù)命名為 err1、 err2 等。也有開發(fā)者干脆關(guān)閉了靜態(tài)檢查工具對(duì)重復(fù)變量名的檢查。
ES6 改變了現(xiàn)狀,引入了新的 let 關(guān)鍵字,提供了除 var 以外的另一種變量聲明方式。
var foo = true; if (foo) { let bar = foo * 2; bar = something( bar ); console.log( bar ); } console.log( bar ); // ReferenceError
ES6中的if表達(dá)式中的{}并不具備塊級(jí)作用域的劃分,僅僅只能表明一個(gè)語句塊,因?yàn)橐谄渲新暶鲏K級(jí)作用域變量還需要let來輔助。
let 關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內(nèi)部)。換句話說,let為其聲明的變量隱式地了所在的塊作用域。
在開發(fā)和修改代碼的過 程中,如果沒有密切關(guān)注哪些塊作用域中有綁定的變量,并且習(xí)慣性地移動(dòng)這些塊或者將 其包含在其他的塊中,就會(huì)導(dǎo)致代碼變得混亂。
為塊作用域顯式地創(chuàng)建塊可以部分解決這個(gè)問題,使變量的附屬關(guān)系變得更加清晰。通常 來講,顯式的代碼優(yōu)于隱式或一些精巧但不清晰的代碼。顯式的塊作用域風(fēng)格非常容易書 寫,并且和其他語言中塊作用域的工作原理一致:
var foo = true; if (foo) { { // <-- 顯式的快 let bar = foo * 2; bar = something( bar ); console.log( bar ); } } console.log( bar ); // ReferenceError
只要聲明是有效的,在聲明中的任意位置都可以使用 { .. } 括號(hào)來為 let 創(chuàng)建一個(gè)用于綁 定的塊。在這個(gè)例子中,我們?cè)?if 聲明內(nèi)部顯式地創(chuàng)建了一個(gè)塊,如果需要對(duì)其進(jìn)行重 構(gòu),整個(gè)塊都可以被方便地移動(dòng)而不會(huì)對(duì)外部 if 聲明的位置和語義產(chǎn)生任何影響。
另一個(gè)塊作用域非常有用的原因和閉包及回收內(nèi)存垃圾的回收機(jī)制相關(guān)。
function process(data) { // 在這里做點(diǎn)有趣的事情 } var someReallyBigData = { .. }; process( someReallyBigData ); var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt) { console.log("button clicked"); }, /*capturingPhase=*/false );
click 函數(shù)的點(diǎn)擊回調(diào)并不需要 someReallyBigData 變量。理論上這意味著當(dāng) process(..) 執(zhí) 行后,在內(nèi)存中占用大量空間的數(shù)據(jù)結(jié)構(gòu)就可以被垃圾回收了。但是,由于 click 函數(shù)形成 了一個(gè)覆蓋整個(gè)作用域的閉包,JavaScript 引擎極有可能依然保存著這個(gè)結(jié)構(gòu)(取決于具體 實(shí)現(xiàn))。
塊作用域可以打消這種顧慮,可以讓引擎清楚地知道沒有必要繼續(xù)保存 someReallyBigData 了:
function process(data) {
// 在這里做點(diǎn)有趣的事情
}
// 在這個(gè)塊中定義的內(nèi)容可以銷毀了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /capturingPhase=/false );
為變量顯式聲明塊作用域,并對(duì)變量進(jìn)行本地綁定是非常有用的工具,可以把它添加到你
的代碼工具箱中了。
for 循環(huán)頭部的 let 不僅將 i 綁定到了 for 循環(huán)的塊中,事實(shí)上它將其重新綁定到了循環(huán) 的每一個(gè)迭代中,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值。
每個(gè)迭代進(jìn)行重新綁定的原因非常有趣,我們會(huì)在第 5 章討論閉包時(shí)進(jìn)行說明。
除了 let 以外,ES6 還引入了 const,同樣可以用來創(chuàng)建塊作用域變量,但其值是固定的 (常量)。之后任何試圖修改值的操作都會(huì)引起錯(cuò)誤。
小結(jié)函數(shù)是 JavaScript 中最常見的作用域單元。本質(zhì)上,聲明在一個(gè)函數(shù)內(nèi)部的變量或函數(shù)會(huì)在所處的作用域中“隱藏”起來,這是有意為之的良好軟件的設(shè)計(jì)原則。 但函數(shù)不是唯一的作用域單元。塊作用域指的是變量和函數(shù)不僅可以屬于所處的作用域,也可以屬于某個(gè)代碼塊(通常指 { .. } 內(nèi)部)。
從 ES3 開始,try/catch 結(jié)構(gòu)在 catch 分句中具有塊作用域。
在 ES6 中引入了 let 關(guān)鍵字(var 關(guān)鍵字的表親),用來在任意代碼塊中聲明變量。if (..) { let a = 2; } 會(huì)聲明一個(gè)劫持了 if 的 { .. } 塊的變量,并且將變量添加到這個(gè)塊 中。
有些人認(rèn)為塊作用域不應(yīng)該完全作為函數(shù)作用域的替代方案。兩種功能應(yīng)該同時(shí)存在,開 發(fā)者可以并且也應(yīng)該根據(jù)需要選擇使用何種作用域,創(chuàng)造可讀、可維護(hù)的優(yōu)良代碼。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/101464.html
摘要:如果提升改變了代碼執(zhí)行的順序,會(huì)造成非常嚴(yán)重的破壞。聲明本身會(huì)被提升,而包括函數(shù)表達(dá)式的賦值在內(nèi)的賦值操作并不會(huì)提升。要注意避免重復(fù)聲明,特別是當(dāng)普通的聲明和函數(shù)聲明混合在一起的時(shí)候,否則會(huì)引起很多危險(xiǎn)的問題 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡(jiǎn)單易用的語言,又是一門具有許多復(fù)雜微妙技術(shù)的語言,即使是經(jīng)驗(yàn)豐富的 Ja...
摘要:詞法作用域的查找規(guī)則是閉包的一部分。因此的確同閉包息息相關(guān),即使本身并不會(huì)真的使用閉包。而上面的創(chuàng)建一個(gè)閉包,本質(zhì)上這是將一個(gè)塊轉(zhuǎn)換成一個(gè)可以被關(guān)閉的作用域。結(jié)合塊級(jí)作用域與閉包模塊這個(gè)模式在中被稱為模塊。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡(jiǎn)單易用的語言,又是一門具有許多復(fù)雜微妙技術(shù)的語言,即使是經(jīng)驗(yàn)豐富的 Jav...
摘要:詞法作用域定義在詞法階段的作用域由你在寫代碼時(shí)將變量和塊作用域?qū)懺谀膩頉Q定的,因此當(dāng)詞法分析器處理代碼時(shí)會(huì)保持作用域不變。欺騙詞法作用域在詞法分析器處理過后依然可以修改作用域。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡(jiǎn)單易用的語言,又是一門具有許多復(fù)雜微妙技術(shù)的語言,即使是經(jīng)驗(yàn)豐富的 JavaScript 開發(fā)者,如果沒...
摘要:的抽象語法樹中可能會(huì)有一個(gè)叫作的頂級(jí)節(jié)點(diǎn),接下來是一個(gè)叫作它的值是的子節(jié)點(diǎn),以及一個(gè)叫作的子節(jié)點(diǎn)。值得注意的是,是非常重要的異常類型。嚴(yán)格模式下,未聲明的和倆者行為相同,都會(huì)是。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡(jiǎn)單易用的語言,又是一門具有許多復(fù)雜微妙技術(shù)的語言,即使是經(jīng)驗(yàn)豐富的 JavaScript 開發(fā)者,如果...
摘要:上一篇文章第二章實(shí)戰(zhàn)演練開發(fā)網(wǎng)站第九節(jié)防止跨站攻擊下一篇文章第三章概念及應(yīng)用第二節(jié)服務(wù)端編程的異步特性使得其非常適合服務(wù)器的高并發(fā)處理,客戶端與服務(wù)器的持久連接應(yīng)用框架就是高并發(fā)的典型應(yīng)用。因?yàn)槭堑臉?biāo)準(zhǔn)協(xié)議,所以不受企業(yè)防火墻的攔截。 上一篇文章:Python:Tornado 第二章:實(shí)戰(zhàn)演練:開發(fā)Tornado網(wǎng)站:第九節(jié):防止跨站攻擊下一篇文章:Python:Tornado 第三章...
閱讀 972·2021-10-25 09:48
閱讀 680·2021-08-23 09:45
閱讀 2538·2019-08-30 15:53
閱讀 1793·2019-08-30 12:45
閱讀 714·2019-08-29 17:21
閱讀 3500·2019-08-27 10:56
閱讀 2592·2019-08-26 13:48
閱讀 743·2019-08-26 12:24