摘要:在及之前版本,只擁有函數作用域,沒有塊作用域和除外。函數表達式分為匿名函數表達式和具名函數表達式。但是,由于這個事件回調函數形成了一個覆蓋當前作用域的閉包,引擎極有可能依然保存著這個數據結構取決于具體實現。總結函數是中最常見的作用域單元。
在 ES5 及之前版本,JavaScript 只擁有函數作用域,沒有塊作用域(with 和 try...catch 除外)。在 ES6 中,JS 引入了塊作用域,{ } 內是多帶帶的一個作用域。采用 let 或者 const 聲明的變量會挾持所在塊的作用域,也就是說,這聲明關鍵字會將變量綁定到所在的任意作用域中(通常是 {...} 內部)。
今天,我們就來深入研究一下函數作用域和塊作用域。
1. 函數中的作用域函數作用域的含義是指,屬于這個函數的任何聲明(變量或函數)都可以在這個函數的范圍內使用及復用(包括這個函數嵌套內的作用域)。
舉個例子:
function foo (a) { var b = 2; // something else function bar () { // something else } var c = 3; } bar(); // 報錯,ReferenceError: bar is not defined console.log(a, b, c); // 報錯,原因同上
在這段代碼中,函數 foo 的作用域包含了標識符a、b、c 和 bar ,函數 bar 的作用域中又包含別的標識符。
由于標識符 a、b、c 和 bar都屬于函數 foo 的作用域,所以在全局作用域中訪問會報錯,因為它們都沒有定義,但是在函數 foo 內部,這些標識符都是可以訪問的,這就是函數作用域。
當我們用作用域把代碼包起來的時候,其實就是對它們進行了“隱藏”,讓我們對其有控制權,想讓誰訪問就可以讓誰訪問,想禁止訪問也很容易。
想像一下,如果所有的變量和函數都在全局作用域中,當然我們可以在內部的嵌套作用域中訪問它們,但是因為暴露了太多的變量或函數,它們可能被有意或者無意的篡改,以非預期的方式使用,這就導致我們的程序會出現各種各樣的問題,嚴重會導致數據泄露,造成無法挽回的后果。
例如:
var obj = { a: 2, getA: function () { return this.a; } }; obj.a = 4; obj.getA(); // 4
這個例子中,我們可以任意修改對象 obj 內部的值,在某種情況下這并不是我們所期望的,采用函數作用域就可以解決這個問題,私有化變量 a 。
var obj = (function () { var a = 2; return { getA: function () { return a; }, setA: function (val) { a = val; } } }()); obj.a = 4; obj.getA(); // 2 obj.setA(8); obj.getA(); // 8
這里通過立即執行函數(IIFE)返回一個對象,只能通過對象內的方法對變量 a 進行操作,其實這里有閉包的存在,這個我們在以后會深入討論。
“隱藏”作用域中的變量和函數所帶來的另一個好處,是可以避免同名標識符之間的沖突,沖突會導致變量的值被意外覆蓋。
例如:
function foo () { function bar (a) { i = 3; // 修改了 for 循環所屬作用域中的 i console.log(a + i); } for (var i = 0; i < 10; i++) { bar(i * 2); // 這里因為 i 總會被設置為 3 ,導致無限循環 } } foo();
bar(...) 內部的賦值表達式 i = 3 意外的覆蓋了聲明在 foo(...) 內部 for 循環中的 i ,在這個例子中因為 i 始終被設置為 3 ,永遠滿足小于 10 這個條件,導致無限循環。
bar(...) 內部的賦值操作需要聲明一個本地變量來使用,采用任何名字都可以,var i = 3; 就可以滿足這個要求。另外一種方法是采用一個完全不同的標識符名稱,比如 var j = 3; 。但是軟件設計在某種情況下可能自然而然的要求使用同樣的標識符名稱,因此在這種情況下使用作用域來“隱藏”內部聲明是唯一的最佳選擇。
總結來說,作用域可以起到兩個作用:
私有化變量或函數
規避同名沖突
如果 function 是聲明中的第一個詞,那么就是一個函數聲明,否則就是一個函數表達式。
函數聲明舉個例子:
function foo () { // something else }
這就是一個函數聲明。
函數表達式分為匿名函數表達式和具名函數表達式。
對于函數表達式來說,最熟悉的場景可能就是回調參數了,例如:
setTimeout(function () { console.log("I wait for one second.") }, 1000);
這個叫作匿名函數表達式,因為 function ()... 沒有名稱標識符。函數表達式可以是匿名的,但是函數聲明不可以省略函數名,在 javascript 中這是非法的。
匿名函數表達式書寫簡便,但是它也有幾個缺點需要注意:
匿名函數在瀏覽器棧追蹤中不會顯示出有意義的函數名,這會加大調試難度。
如果沒有函數名,當函數需要引用自身的時候就只能使用已經不是標準的 arguments.callee 來引用,比如遞歸。在事件觸發后的事件監聽器中也有可能需要通過函數名來解綁自身。
匿名函數對代碼的可讀性和可理解性有一定的影響。一個有意義的函數名可以讓代碼不言自明。
具名函數表達式又叫行內函數表達式,例如:
setTimeout(function timerHandler () { console.log("I wait for one second.") }, 1000);
這樣,在函數內部需要引用自身的時候就可以通過函數名來引用,當然要注意,這個函數名只能在這個函數內部使用,在函數外使用時未定義的。
IIFE 全寫是 Immediately Invoked Function Expression,立即執行函數。
var a = 2; (function foo () { var a = 3; console.log(a); // 3 })(); console.log(a); // 2
由于函數被包含在一對 ( ) 括號內部,因此成為了一個函數表達式,通過在末尾加上另一對 ( ) 括號可以立即執行這個函數,比如 (function () {})() 。第一個 ( ) 將函數變成函數表達式,第二個 ( ) 執行了這個函數。
也有另外一種立即執行函數的寫法,(function () {}()) 也可以立即執行這個函數。
var a = 2; (function foo () { var a = 3; console.log(a); // 3 }()); console.log(a); // 2
這兩種寫法功能是完全一樣的,具體看大家使用。
IIFE 的另一種普遍的進階用法是把它們當做函數調用并傳遞參數進去。
var a = 2; (function (global) { var a = 3; console.log(a); // 3 console.log(global.a) // 2 })(window); console.log(a); // 2
我們將 window 對象的引用傳遞進去,但將參數命名為 global,因此在代碼風格上對全局對象的引用變得比引用一個沒有“全局”字樣的變量更加清晰。當然可以從外部作用域傳遞你需要的任何東西,并將變量命名為任何你覺得合適的文字。這對于改進代碼風格是非常有幫助的。
這個模式的另外一個應用場景是解決 undefined 標識符的默認值被錯誤覆蓋的異常(這并不常見)。將一個參數命名為 undefined ,但是并不傳入任何值,這樣就可以保證在代碼塊中 undefined 的標識符的值就是 undefined 。
undefined = true; (function IIFE (undefined) { var a; if (a === undefined) { console.log("Undefined is safe here.") } }());2. 塊作用域
ES5 及以前 JavaScript 中具有塊作用域的只有 with 和 try...catch 語句,在 ES6 及以后的版本添加了具有塊作用域的變量標識符 let 和 const 。
var obj = { a: 2, b: 3 }; with (obj) { console.log(a); // 2 console.log(b); // 3 } console.log(a); // 報錯,a is not defined console.log(b); // 報錯,a is not defined
用 with 從對象中創建出的作用域僅在 with 聲明中而非外部作用域中有效。
try { undefined(); // 非法操作 } catch (err) { console.log(err); // 正常執行 } console.log(err); // 報錯,err is not defined
try/catch 中的 catch 分句會創建一個塊作用域,其中的變量聲明僅在 catch 內部有效。
let 關鍵字可以將變量綁定到任意作用域中(通常是 {...} 內部)。換句話說,let 為其聲明的變量隱式的劫持了所在的塊作用域。
var foo = true; if (foo) { let a = 2; var b = 2; console.log(a); // 2 console.log(b); // 2 } console.log(b); // 2 console.log(a); // 報錯,a is not defined
用 let 將變量附加在一個已經存在的塊作用域上的行為是隱式的。在開發和修改代碼的過程中,如果沒有密切關注哪些代碼塊作用域中有綁定的變量,并且習慣性的移動這些塊或者將其包含到其他塊中,就會導致代碼混亂。
為塊作用域顯示的創建塊可以部分解決這個問題,使變量的附屬關系變得更加清晰。
var foo = true; if (foo) { { let a = 2; console.log(a); // 2 } }
在代碼的任意位置都可以使用 {...} 括號來為 let 創建一個用于綁定的塊。
還有一點要注意的是,在使用 var 進行變量聲明的時候會存在變量提升,提升是指聲明會被視為存在于其所出現的作用域的整個范圍內。但是使用 let 進行的聲明不會存在作用域提升,聲明的變量在被運行之前,并不存在。
console.log(a); // undefined console.log(b); // 報錯, b is not defined // 在瀏覽器中運行這段代碼時,因為前面報錯了,所以不會看到接下來打印的結果,但是理論上就是這樣的結果 var a = 2; console.log(a); // 2 let b = 4; console.log(b); // 4
2.3.1 垃圾收集
另一個塊作用域非常有用的原因和閉包及垃圾內存的回收機制有關。
舉個例子:
function processData (data) { // do something } var bigData = {...}; processData(bigData); var btn = document.getElementById("my_button"); btn.addEventListener("click", function () { console.log("button clicked"); }, false);
這個按鈕點擊事件的回調函數中并不需要 bigData 這個非常占內存的數據,理論上來說,當 processData 函數處理完之后,這個占有大量空間的數據結構就可以被垃圾回收了。但是,由于這個事件回調函數形成了一個覆蓋當前作用域的閉包,JavaScript 引擎極有可能依然保存著這個數據結構(取決于具體實現)。
使用塊作用域可以解決這個問題,可以讓引擎清楚的知道沒有必要繼續保存這個 bigData 。
function processData (data) { // do something } { let bigData = {...}; processData(bigData); } var btn = document.getElementById("my_button"); btn.addEventListener("click", function () { console.log("button clicked"); }, false);
2.3.2 let 循環
一個 let 可以發揮優勢的典型例子就是 for 循環。
var lists = document.getElementsByTagName("li"); for (let i = 0, length = lists.length; i < length; i++) { console.log(i); lists[i].onclick = function () { console.log(i); // 點擊每個 li 元素的時候,都是相對應的 i 值,而不像用 var 聲明 i 的時候,因為沒有塊作用域,所以在回調函數通過閉包查找 i 的時候找到的都是最后的 i 值 }; }; console.log(i); // 報錯,i is not defined
for 循環頭部的 let 不僅將 i 綁定到 fir 循環的塊中,事實上它將其重新綁定到了循環的每一個迭代中,確保上一個循環迭代結束時的值重新進行賦值。
當然,我們在 for 循環中使用 var 時也可以通過立即執行函數形成一個新的閉包來解決這個問題。
var lists = document.getElementsByTagName("li"); for (var i = 0, length = lists.length; i < length; i++) { lists[i].onclick = (function (j) { return function () { console.log(j); } }(i)); }
或者
var lists = document.getElementsByTagName("li"); for (var i = 0, length = lists.length; i < length; i++) { (function (i) { lists[i].onclick = function () { console.log(i); } }(i)); }
其實原理無非就是,為每個迭代創建新的閉包,立即執行函數執行完后本來應該銷毀變量,釋放內存,但是因為這里有回調函數的存在,所以形成了閉包,然后通過形參進行同名變量覆蓋,所以找到的 i 值就是每個迭代新閉包中的形參 i 。
除了 let 以外,ES6 還引入了 const ,同樣可以用來創建作用域變量,但其值是固定的(常亮)。之后任何試圖修改值的操作都會引起錯誤。
var foo = true; if (foo) { var a = 2; const b = 3; // 包含在 if 中的塊作用域常亮 a = 3; // 正常 b = 4; // 報錯,TypeError: Assignment to constant variable } console.log(a); // 3 console.log(b); // 報錯, b is not defined
和 let 一樣,const 聲明的變量也不存在“變量提升”。
3. 總結函數是 JavaScript 中最常見的作用域單元。塊作用域指的是變量和函數不僅可以屬于所處的函數作用域,也可以屬于某個代碼塊。
本質上,聲明在一個函數內部的變量或函數會在所處的作用域中“隱藏”起來,這是有意為之的良好軟件的設計原則。
有些人認為塊作用域不應該完全作為函數作用域的替代方案。兩種功能應該同時存在,開發者可以并且也應該根據需要選擇使用哪種作用域,創造可讀、可維護的優良代碼。
歡迎關注我的公眾號文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81663.html
摘要:提供了同時解決這兩個問題的方案以上這種模式稱為立即執行函數表達式。塊作用域不應該完全作為函數作用域的替代方案,兩種功能應該同時存在。 函數作用域 為了隱藏內部實現,可以通過在任意代碼片段外部添加包裝函數,但是這并不理想,因為必須聲明一個具名函數,意味著這個函數名稱本身會污染函數所在的作用域;同時必須通過顯式地調用函數才能運行其中的代碼。 *:區分函數聲明和表達式最簡單的方法是看func...
摘要:說到這里我們需要理解兩個概念塊級作用域與函數作用域。大多數類語言都擁有塊級作用域,卻沒有。塊級作用域任何一對花括號中的語句集都屬于一個塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級作用域。 作用域 作用域永遠都是任何一門編程語言中的重中之重,因為它控制著變量與參數的可見性與生命周期。說到這里我們需要理解兩個概念:塊級作用域與函數作用域。 函數作用域 這個應該好理解...
摘要:說到這里我們需要理解兩個概念塊級作用域與函數作用域。大多數類語言都擁有塊級作用域,卻沒有。塊級作用域任何一對花括號中的語句集都屬于一個塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級作用域。 作用域 作用域永遠都是任何一門編程語言中的重中之重,因為它控制著變量與參數的可見性與生命周期。說到這里我們需要理解兩個概念:塊級作用域與函數作用域。 函數作用域 這個應該好理解...
摘要:說到這里我們需要理解兩個概念塊級作用域與函數作用域。大多數類語言都擁有塊級作用域,卻沒有。塊級作用域任何一對花括號中的語句集都屬于一個塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級作用域。 作用域 作用域永遠都是任何一門編程語言中的重中之重,因為它控制著變量與參數的可見性與生命周期。說到這里我們需要理解兩個概念:塊級作用域與函數作用域。 函數作用域 這個應該好理解...
摘要:如果是聲明中的第一個詞,那么就是一個函數聲明,否則就是一個函數表達式。給函數表達式指定一個函數名可以有效的解決以上問題。始終給函數表達式命名是一個最佳實踐。也有開發者干脆關閉了靜態檢查工具對重復變量名的檢查。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復雜微妙技術的語言,即使是經驗豐富的 Ja...
閱讀 3166·2021-11-23 09:51
閱讀 678·2021-10-14 09:43
閱讀 3200·2021-09-06 15:00
閱讀 2403·2019-08-30 15:54
閱讀 2557·2019-08-30 13:58
閱讀 1840·2019-08-29 13:18
閱讀 1372·2019-08-27 10:58
閱讀 506·2019-08-27 10:53