JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具備許多複雜微妙技術的語言,即便是經驗豐富的 JavaScript 開發者,若是沒有認真學習的話也沒法真正理解它們.安全
上捲包括倆節:數據結構
但願 Kyle 對 JavaScript 工做原理每個細節的批判性思 考會滲透到你的思考過程和平常工做中。知其然,也要知其因此然。閉包
正如咱們在第 2 章中討論的那樣,做用域包含了一系列的「氣泡」,每個均可以做爲容 器,其中包含了標識符(變量、函數)的定義。這些氣泡互相嵌套而且整齊地排列成蜂窩 型,排列的結構是在寫代碼時定義的。函數
可是,到底是什麼生成了一個新的氣泡?只有函數會生成新的氣泡嗎? JavaScript 中的其 他結構能生成做用域氣泡嗎?工具
函數做用域的含義是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用及復 用(事實上在嵌套的做用域中也可使用)。學習
這種設計方案是很是有用的,能充分利用 JavaScript 變量能夠根據須要改變值類型的「動態」特性。這是什麼意思?this
能夠把變量和函數包裹在一個函數的做用域中,而後用這個做用域 來「隱藏」它們。設計
Q: 爲何「隱藏」變量和函數是一個有用的技術?調試
A: 大都是從最小特權原則中引伸出來 的,也叫最小受權或最小暴露原則。這個原則是指在軟件設計中,應該最小限度地暴露必 要內容,而將其餘內容都「隱藏」起來,好比某個模塊或對象的 API 設計。
在任意代碼片斷外部添加包裝函數,能夠將內部的變量和函數定義「隱
藏」起來,外部做用域沒法訪問包裝函數內部的任何內容。
這種技術能夠解決一些問題,可是它並不理想,由於會致使一些額外的問題:
若是函數不須要函數名(或者至少函數名能夠不污染所在做用域),而且可以自動運行, 這將會更加理想。
(function foo(){ // <-- 添加這一行 var a = 3; console.log( a ); // 3 })(); // <-- 以及這一行 console.log( a ); // 2
函數聲明和函數表達式之間最重要的區別是它們的名稱標識符將會綁定在何處。
注意:區分函數聲明和表達式最簡單的方法是看 function 關鍵字出如今聲明中的位 置(不只僅是一行代碼,而是整個聲明中的位置)。若是 function 是聲明中 的第一個詞,那麼就是一個函數聲明,不然就是一個函數表達式。
片斷中 foo 被綁定在函數表達式自身的函數中而不是所在做用域中。
相似的還有於 +function foo() {}()
對函數求值的操做,都能作到避免泄露
換句話說,(function foo(){ .. })做爲函數表達式意味着foo只能在..所表明的位置中 被訪問,外部做用域則不行。foo 變量名被隱藏在自身中意味着不會非必要地污染外部做 用域。
setTimeout( function() { console.log("I waited 1 second!"); }, 1000 );
這叫作匿名函數表達式, 由於function()沒有名稱標識符。函數表達式能夠是匿名的,而函數聲明則不能夠省略函數名.
匿名函數表達式寫起來簡單快捷,可是它有幾個缺點須要考慮:
行內函數表達式很是強大且有用——匿名和具名之間的區別並會有對這點有任何影響。 給函數表達式指定一個函數名能夠有效的解決以上問題。
始終給函數表達式命名是一個最佳實踐。
setTimeout( function timeoutHandler() { // <-- 快看,我有名字了! console.log( "I waited 1 second!" ); }, 1000 );
幾年前社區給它規定了一個術語:IIFE,表明當即執行函數表達式 (Immediately Invoked Function Expression);
IIFE的形式有下面倆種:
(function(){ .. })()
(function(){ .. }())
用法1, 把它們看成函數調用並傳遞參數進去
例如:
var a = 2; (function IIFE( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 })( window ); console.log( a ); // 2
咱們將 window 對象的引用傳遞進去,但將參數命名爲 global,所以在代碼風格上對全局 對象的引用變得比引用一個沒有「全局」字樣的變量更加清晰。固然能夠從外部做用域傳 遞任何你須要的東西,並將變量命名爲任何你以爲合適的名字。這對於改進代碼風格是非 常有幫助的。
用法2,解決 undefined 標識符的默認值被錯誤覆蓋致使的異常(雖 然不常見)。
例如:將一個參數命名爲 undefined,可是在對應的位置不傳入任何值,這樣就能夠 保證在代碼塊中 undefined 標識符的值真的是 undefined:
undefined = true; // 給其餘代碼挖了一個大坑!絕對不要這樣作!
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
用法3:倒置代碼的運行順序
例如:將須要運行的函數放在第二位,在 IIFE 執行以後看成參數傳遞進去。這種模式在 UMD(Universal Module Definition)項目中被廣 泛使用。儘管這種模式略顯冗長,但有些人認爲它更易理解。
var a = 2; (function IIFE( def ) { def( window ); })(function def( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 });
塊做用域的用處:變量的聲明應該距離使用的地方越近越好,並最大限度地本地化。
塊做用域是一個用來對以前的最小受權原則進行擴展的工具,將代碼從在函數中隱藏信息 擴展爲在塊中隱藏信息。
爲何要把一個只在 for 循環內部使用(至少是應該只在內部使用)的變量 i 污染到整個
函數做用域中呢?
惋惜,表面上看 JavaScript 並無塊做用域的相關功能。
with 關鍵字。它不只是一個難於理解的結構,同時也是塊做用域的一 個例子(塊做用域的一種形式),用 with 從對象中建立出的做用域僅在 with 聲明中而非外 部做用域中有效。
很是少有人會注意到 JavaScript 的 ES3 規範中規定 try/catch 的 catch 分句會建立一個塊做
用域,其中聲明的變量僅在 catch 內部有效。
例如:
try { undefined(); // 執行一個非法操做來強制製造一個異常 } catch (err) { console.log( err ); // 可以正常執行! } console.log( err ); // ReferenceError: err not found
儘管這個行爲已經被標準化,而且被大部分的標準 JavaScript 環境(除了老 版本的 IE 瀏覽器)所支持,可是當同一個做用域中的兩個或多個 catch 分句 用一樣的標識符名稱聲明錯誤變量時,不少靜態檢查工具仍是會發出警告。 實際上這並非重複定義,由於全部變量都被安全地限制在塊做用域內部, 可是靜態檢查工具仍是會很煩人地發出警告。爲了不這個沒必要要的警告,不少開發者會將 catch 的參數命名爲 err一、 err2 等。也有開發者乾脆關閉了靜態檢查工具對重複變量名的檢查。
ES6 改變了現狀,引入了新的 let 關鍵字,提供了除 var 之外的另外一種變量聲明方式。
var foo = true; if (foo) { let bar = foo * 2; bar = something( bar ); console.log( bar ); } console.log( bar ); // ReferenceError
ES6中的if表達式中的{}並不具有塊級做用域的劃分,僅僅只能代表一個語句塊,由於要在其中聲明塊級做用域變量還須要let來輔助。
let 關鍵字能夠將變量綁定到所在的任意做用域中(一般是 { .. } 內部)。換句話說,let爲其聲明的變量隱式地了所在的塊做用域。
在開發和修改代碼的過 程中,若是沒有密切關注哪些塊做用域中有綁定的變量,而且習慣性地移動這些塊或者將 其包含在其餘的塊中,就會致使代碼變得混亂。
爲塊做用域顯式地建立塊能夠部分解決這個問題,使變量的附屬關係變得更加清晰。一般 來說,顯式的代碼優於隱式或一些精巧但不清晰的代碼。顯式的塊做用域風格很是容易書 寫,而且和其餘語言中塊做用域的工做原理一致:
var foo = true; if (foo) { { // <-- 顯式的快 let bar = foo * 2; bar = something( bar ); console.log( bar ); } } console.log( bar ); // ReferenceError
只要聲明是有效的,在聲明中的任意位置均可以使用 { .. } 括號來爲 let 建立一個用於綁 定的塊。在這個例子中,咱們在 if 聲明內部顯式地建立了一個塊,若是須要對其進行重 構,整個塊均可以被方便地移動而不會對外部 if 聲明的位置和語義產生任何影響。
另外一個塊做用域很是有用的緣由和閉包及回收內存垃圾的回收機制相關。
function process(data) { // 在這裏作點有趣的事情 } var someReallyBigData = { .. }; process( someReallyBigData ); var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt) { console.log("button clicked"); }, /*capturingPhase=*/false );
click 函數的點擊回調並不須要 someReallyBigData 變量。理論上這意味着當 process(..) 執 行後,在內存中佔用大量空間的數據結構就能夠被垃圾回收了。可是,因爲 click 函數造成 了一個覆蓋整個做用域的閉包,JavaScript 引擎極有可能依然保存着這個結構(取決於具體 實現)。
塊做用域能夠打消這種顧慮,可讓引擎清楚地知道沒有必要繼續保存 someReallyBigData 了:
function process(data) {
// 在這裏作點有趣的事情
}
// 在這個塊中定義的內容能夠銷燬了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /capturingPhase=/false );
爲變量顯式聲明塊做用域,並對變量進行本地綁定是很是有用的工具,能夠把它添加到你
的代碼工具箱中了。
for 循環頭部的 let 不只將 i 綁定到了 for 循環的塊中,事實上它將其從新綁定到了循環 的每個迭代中,確保使用上一個循環迭代結束時的值從新進行賦值。
每一個迭代進行從新綁定的緣由很是有趣,咱們會在第 5 章討論閉包時進行說明。
除了 let 之外,ES6 還引入了 const,一樣能夠用來建立塊做用域變量,但其值是固定的 (常量)。以後任何試圖修改值的操做都會引發錯誤。
函數是 JavaScript 中最多見的做用域單元。本質上,聲明在一個函數內部的變量或函數會在所處的做用域中「隱藏」起來,這是有意爲之的良好軟件的設計原則。 但函數不是惟一的做用域單元。塊做用域指的是變量和函數不只能夠屬於所處的做用域,也能夠屬於某個代碼塊(一般指 { .. } 內部)。
從 ES3 開始,try/catch 結構在 catch 分句中具備塊做用域。
在 ES6 中引入了 let 關鍵字(var 關鍵字的表親),用來在任意代碼塊中聲明變量。if (..) { let a = 2; } 會聲明一個劫持了 if 的 { .. } 塊的變量,而且將變量添加到這個塊 中。 有些人認爲塊做用域不該該徹底做爲函數做用域的替代方案。兩種功能應該同時存在,開 發者能夠而且也應該根據須要選擇使用何種做用域,創造可讀、可維護的優良代碼。