摘要:如果是聲明中的第一個詞,那么就是一個函數聲明,否則就是一個函數表達式。給函數表達式指定一個函數名可以有效的解決以上問題。始終給函數表達式命名是一個最佳實踐。也有開發者干脆關閉了靜態檢查工具對重復變量名的檢查。
你不知道的JS(上卷)筆記
你不知道的 JavaScript
JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復雜微妙技術的語言,即使是經驗豐富的 JavaScript 開發者,如果沒有認真學習的話也無法真正理解它們.
上卷包括倆節:
作用域和閉包
this 和對象原型
作用域和閉包希望 Kyle 對 JavaScript 工作原理每一個細節的批判性思 考會滲透到你的思考過程和日常工作中。知其然,也要知其所以然。
函數作用域和塊作用域正如我們在第 2 章中討論的那樣,作用域包含了一系列的“氣泡”,每一個都可以作為容 器,其中包含了標識符(變量、函數)的定義。這些氣泡互相嵌套并且整齊地排列成蜂窩 型,排列的結構是在寫代碼時定義的。
但是,究竟是什么生成了一個新的氣泡?只有函數會生成新的氣泡嗎? JavaScript 中的其 他結構能生成作用域氣泡嗎?
函數中的作用域JavaScript 具有基于函數的作用域;
無論標識符 聲明出現在作用域中的何處,這個標識符所代表的變量或函數都將附屬于所處作用域的氣 泡。
函數作用域的含義是指,屬于這個函數的全部變量都可以在整個函數的范圍內使用及復 用(事實上在嵌套的作用域中也可以使用)。
這種設計方案是非常有用的,能充分利用 JavaScript 變量可以根據需要改變值類型的“動態”特性。這是什么意思?
隱藏的內部實現可以把變量和函數包裹在一個函數的作用域中,然后用這個作用域 來“隱藏”它們。
Q: 為什么“隱藏”變量和函數是一個有用的技術?
A: 大都是從最小特權原則中引申出來 的,也叫最小授權或最小暴露原則。這個原則是指在軟件設計中,應該最小限度地暴露必 要內容,而將其他內容都“隱藏”起來,比如某個模塊或對象的 API 設計。
設計上將具體內容私有化了,設計良好的軟件都會 依此進行實現。
全局命名空間
通常會在全局作用域中聲明一個名字足夠獨特的變量,通常是一個對象。這個對象 被用作庫的命名空間,所有需要暴露給外界的功能都會成為這個對象(命名空間)的屬 性,而不是將自己的標識符暴漏在頂級的詞法作用域中。
模塊管理
任何庫都無需將標識符加入到全局作用域中,而是通過依賴管理器 的機制將庫的標識符顯式地導入到另外一個特定的作用域中。
避免同名標識符之間的沖突
函數作用域在任意代碼片段外部添加包裝函數,可以將內部的變量和函數定義“隱
藏”起來,外部作用域無法訪問包裝函數內部的任何內容。
這種技術可以解決一些問題,但是它并不理想,因為會導致一些額外的問題:
必須聲明一個具名函數 foo(),意味著 foo 這個名稱本身“污染”了所在作用域(在這個 例子中是全局作用域)
必須顯式地通過函數名(foo())調用這個函數才能運行其 中的代碼。
如果函數不需要函數名(或者至少函數名可以不污染所在作用域),并且能夠自動運行, 這將會更加理想。
(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()沒有名稱標識符。函數表達式可以是匿名的,而函數聲明則不可以省略函數名.
匿名函數表達式寫起來簡單快捷,但是它有幾個缺點需要考慮:
匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
如果沒有函數名,當函數需要引用自身時,只能使用已經過期的arguments.callee引用,比如在遞歸中。另一個函數需要引用自身的例子是在事件觸發后事件監聽器需要解綁自身。
匿名函數省略了對于代碼可讀性/可理解性很重要的函數名。一個描述性的名詞可以讓代碼不言自明。
行內函數表達式非常強大且有用——匿名和具名之間的區別并會有對這點有任何影響。 給函數表達式指定一個函數名可以有效的解決以上問題。
始終給函數表達式命名是一個最佳實踐。
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 的參數命名為 err1、 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 的 { .. } 塊的變量,并且將變量添加到這個塊 中。
有些人認為塊作用域不應該完全作為函數作用域的替代方案。兩種功能應該同時存在,開 發者可以并且也應該根據需要選擇使用何種作用域,創造可讀、可維護的優良代碼。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101464.html
摘要:如果提升改變了代碼執行的順序,會造成非常嚴重的破壞。聲明本身會被提升,而包括函數表達式的賦值在內的賦值操作并不會提升。要注意避免重復聲明,特別是當普通的聲明和函數聲明混合在一起的時候,否則會引起很多危險的問題 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復雜微妙技術的語言,即使是經驗豐富的 Ja...
摘要:詞法作用域的查找規則是閉包的一部分。因此的確同閉包息息相關,即使本身并不會真的使用閉包。而上面的創建一個閉包,本質上這是將一個塊轉換成一個可以被關閉的作用域。結合塊級作用域與閉包模塊這個模式在中被稱為模塊。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復雜微妙技術的語言,即使是經驗豐富的 Jav...
摘要:詞法作用域定義在詞法階段的作用域由你在寫代碼時將變量和塊作用域寫在哪來決定的,因此當詞法分析器處理代碼時會保持作用域不變。欺騙詞法作用域在詞法分析器處理過后依然可以修改作用域。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復雜微妙技術的語言,即使是經驗豐富的 JavaScript 開發者,如果沒...
摘要:的抽象語法樹中可能會有一個叫作的頂級節點,接下來是一個叫作它的值是的子節點,以及一個叫作的子節點。值得注意的是,是非常重要的異常類型。嚴格模式下,未聲明的和倆者行為相同,都會是。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復雜微妙技術的語言,即使是經驗豐富的 JavaScript 開發者,如果...
摘要:上一篇文章第二章實戰演練開發網站第九節防止跨站攻擊下一篇文章第三章概念及應用第二節服務端編程的異步特性使得其非常適合服務器的高并發處理,客戶端與服務器的持久連接應用框架就是高并發的典型應用。因為是的標準協議,所以不受企業防火墻的攔截。 上一篇文章:Python:Tornado 第二章:實戰演練:開發Tornado網站:第九節:防止跨站攻擊下一篇文章:Python:Tornado 第三章...
閱讀 819·2021-10-25 09:48
閱讀 611·2021-08-23 09:45
閱讀 2496·2019-08-30 15:53
閱讀 1759·2019-08-30 12:45
閱讀 586·2019-08-29 17:21
閱讀 3407·2019-08-27 10:56
閱讀 2547·2019-08-26 13:48
閱讀 691·2019-08-26 12:24