## 1.3 函數作用域與塊作用域
### 1.3.1 函數作用域
~~~
function foo(a) {
var b = 2;
// 一些代碼
function bar() {
// ...
}
// 更多的代碼
var c = 3;
}
~~~
在這個代碼片段中,foo(..) 的作用域氣泡中包含了標識符a、b、c 和bar。無論標識符聲明出現在作用域中的何處,這個標識符所代表的變量或函數都將附屬于所處作用域的氣泡。
**函數作用域**的含義是指,屬于這個函數的全部變量都可以在整個函數的范圍內使用及復用(事實上在嵌套的作用域中也可以使用)。
### 1.3.2 隱藏內部實現
**最小授權或最小暴露原則**,指在軟件設計中,應該最小限度地暴露必要內容,而將其他內容都“隱藏”起來,比如某個模塊或對象的API 設計。
~~~
function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething( 2 ); // 15
~~~
在這個代碼片段中,變量b 和函數doSomethingElse(..) 應該是doSomething(..) 內部具體實現的“私有”內容。給予外部作用域對b 和doSomethingElse(..) 的“訪問權限”不僅沒有必要,而且可能是“危險”的,因為它們可能被有意或無意地以非預期的方式使用,從而導致超出了 doSomething(..) 的適用條件。
改進:
~~~
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
~~~
#### 規避沖突
“隱藏”作用域中的變量和函數所帶來的另一個好處,是可以避免同名標識符之間的沖突,兩個標識符可能具有相同的名字但用途卻不一樣,無意間可能造成命名沖突。沖突會導致變量的值被意外覆蓋。
~~~
function foo() {
function bar(a) {
i = 3; // 修改for 循環所屬作用域中的i
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); // 糟糕,無限循環了!
}
}
foo();
~~~
**1. 全局命名空間**
變量沖突的一個典型例子存在于全局作用域中。當程序中加載了多個第三方庫時,內部私有的函數或變量如果沒有妥善隱藏起來,就會很容易引發沖突。
這些庫通常會在全局作用域中聲明一個名字足夠獨特的變量,通常是一個對象。這個對象被用作庫的**命名空間**,所有需要暴露給外界的功能都會成為這個對象(命名空間)的屬性,而不是將自己的標識符暴漏在頂級的詞法作用域中。
~~~
var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
doAnotherThing: function() {
// ...
}
};
~~~
**2. 模塊管理**
另外一種避免沖突的辦法和現代的模塊機制很接近,就是從眾多模塊管理器中挑選一個來使用。使用這些工具,任何庫都無需將標識符加入到全局作用域中,而是**通過依賴管理器的機制將庫的標識符顯式地導入到另外一個特定的作用域中**。
這些工具只是利用作用域的規則強制所有標識符都不能注入到共享作用域中,而是保持在私有、無沖突的作用域中,這樣可以有效規避掉所有的意外沖突。
### 1.3.3 函數作用域
在任意代碼片段外部添加包裝函數,可以將內部的變量和函數定義“隱藏”起來,外部作用域無法訪問包裝函數內部的任何內容。
但這樣必須聲明一個函數(污染了作用域),并且需要顯示地調用才能運行函數里的代碼。
**區分函數聲明和表達式最簡單的方法是看function 關鍵字出現在聲明中的位置(不僅僅是一行代碼,而是整個聲明中的位置)。如果function 是聲明中的第一個詞,那么就是一個函數聲明,否則就是一個函數表達式。**
**1. 匿名和具名**
~~~
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );
~~~
匿名函數的缺點:
* 匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
* 如果沒有函數名,當函數需要引用自身時只能使用已經過期的`arguments.callee` 引用,比如在遞歸中。另一個函數需要引用自身的例子,是在事件觸發后事件監聽器需要解綁自身。
* 匿名函數省略了對于代碼可讀性/ 可理解性很重要的函數名。一個描述性的名稱可以讓代碼不言自明。
**行內函數表達式**非常強大且有用——匿名和具名之間的區別并不會對這點有任何影響。給函數表達式指定一個函數名可以有效解決以上問題。始終給函數表達式命名是一個**最佳實踐**:
~~~
setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
console.log( "I waited 1 second!" );
}, 1000 );
~~~
**2. 立即執行函數表達式**
~~~
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
~~~
第一個( ) 將函數變成表達式,第二個( ) 執行了這個函數。*第二個( )也能寫在第一個( )末尾里*
這種模式稱為:**IIFE**,代表**立即執行函數表達式**(Immediately Invoked Function Expression),第一個( )常用一個匿名函數表達式。
#### IIFE進階用法:把IIFE當作函數調用并傳遞參數進去。
* 傳遞外部作用域的變量
~~~
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
~~~
* 解決undefined 標識符的默認值被錯誤覆蓋導致的異常
將一個參數命名為undefined,但是在對應的位置不傳入任何值,這樣就可以保證在代碼塊中undefined 標識符的值真的是undefined。
~~~
undefined = true; // 給其他代碼挖了一個大坑!絕對不要這樣做!
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
~~~
* 倒置代碼的運行順序
將需要運行的函數放在第二位,在IIFE執行之后當作參數傳遞進去。
~~~
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
~~~
函數表達式def 定義在片段的第二部分,然后當作參數(這個參數也叫作def)被傳遞進IIFE 函數定義的第一部分中。最后,參數def(也就是傳遞進去的函數)被調用,并將window 傳入當作global 參數的值。
### 1.3.4 塊作用域
**塊作用域**,指的是變量和函數不僅可以屬于所處的作用域,也可以屬于某個代碼塊(通常指{ .. } 內部)。
~~~
for (var i=0; i<10; i++) {
console.log( i );
}
~~~
在for 循環的頭部直接定義了變量i,通常是因為只想在for 循環內部的上下文中使用i,而忽略了i 會被綁定在外部作用域(函數或全局)中的事實。
變量的聲明應該距離使用的地方越近越好,并最大限度地本地化。
~~~
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something( bar );
console.log( bar );
}
~~~
bar 變量僅在if 聲明的上下文中使用,因此如果能將它聲明在if 塊內部中會是一個很有意義的事情。但是,當使用var 聲明變量時,它寫在哪里都是一樣的,因為它們最終都會屬于外部作用域。這段代碼是為了風格更易讀而偽裝出的形式上的塊作用域,如果使用這種形式,要確保沒在作用域其他地方意外地使用bar 只能依靠自覺性。
塊作用域是一個用來對之前的**最小授權原則**進行擴展的工具,將代碼從在函數中隱藏信息擴展為在塊中隱藏信息。
**表面上看JavaScript 并沒有塊作用域的相關功能。**
**1. with**
`with關鍵字`是塊級作用域的一個形式,用with 從對象中創建出的作用域僅在with 聲明中而非外部作用域中有效。
**2. try/catch**
JavaScript 的ES3 規范中規定try/catch 的catch 分句會創建一個塊作用域,其中聲明的變量僅在catch 內部有效。
~~~
try {
undefined(); // 執行一個非法操作來強制制造一個異常
}
catch (err) {
console.log( err ); // 能夠正常執行!
}
console.log( err ); // ReferenceError: err not found
~~~
**3. let**
`let 關鍵字`可以將變量綁定到所在的任意作用域中(通常是{ .. } 內部)。換句話說,`let`為其聲明的變量隱式地附加了所在的塊作用域。
~~~
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
~~~
使用`let `進行的聲明不會在塊作用域中進行提升。聲明的代碼被運行之前,聲明并不“存在”。
~~~
{
console.log( bar ); // ReferenceError!
let bar = 2;
}
~~~
* 垃圾收集
考慮以下代碼:
~~~
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 );
~~~
* let循環
~~~
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
~~~
for 循環頭部的let 不僅將i 綁定到了for 循環的塊中,事實上它將其重新綁定到了循環的每一個迭代中,確保使用上一個循環迭代結束時的值重新進行賦值。
下面通過另一種方式來說明每次迭代時進行重新綁定的行為:
~~~
{
let j;
for (j=0; j<10; j++) {
let i = j; // 每個迭代重新綁定!
console.log( i );
}
}
~~~
**當代碼中存在對于函數作用域中var 聲明的隱式依賴時,就會有很多隱藏的陷阱,如果用let 來替代var 則需要在代碼重構的過程中付出額外的精力。**
考慮以下代碼:
~~~
var foo = true, baz = 10;
if (foo) {
var bar = 3;
if (baz > bar) {
console.log( baz );
}
// ...
}
~~~
這段代碼可以簡單地被重構成下面的同等形式:
~~~
var foo = true, baz = 10;
if (foo) {
var bar = 3;
// ...
}
if (baz > bar) {
console.log( baz );
}
~~~
但是在使用塊級作用域的變量時需要注意以下變化:
~~~
var foo = true, baz = 10;
if (foo) {
let bar = 3;
if (baz > bar) { // <-- 移動代碼時不要忘了bar!
console.log( baz );
}
}
~~~
### 1.3.4 const
除了let 以外,ES6 還引入了const,同樣可以用來創建塊作用域變量,但其值是固定的(常量)。之后任何試圖修改值的操作都會引起錯誤。
~~~
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在if 中的塊作用域常量
a = 3; // 正常!
b = 4; // 錯誤!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
~~~
- 前言
- 第一章 JavaScript簡介
- 第三章 基本概念
- 3.1-3.3 語法、關鍵字和變量
- 3.4 數據類型
- 3.5-3.6 操作符、流控制語句(暫略)
- 3.7函數
- 第四章 變量的值、作用域與內存問題
- 第五章 引用類型
- 5.1 Object類型
- 5.2 Array類型
- 5.3 Date類型
- 5.4 基本包裝類型
- 5.5 單體內置對象
- 第六章 面向對象的程序設計
- 6.1 理解對象
- 6.2 創建對象
- 6.3 繼承
- 第七章 函數
- 7.1 函數概述
- 7.2 閉包
- 7.3 私有變量
- 第八章 BOM
- 8.1 window對象
- 8.2 location對象
- 8.3 navigator、screen與history對象
- 第九章 DOM
- 9.1 節點層次
- 9.2 DOM操作技術
- 9.3 DOM擴展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件處理程序
- 10.3 事件對象
- 10.4 事件類型
- 第十一章 JSON
- 11.1-11.2 語法與序列化選項
- 第十二章 正則表達式
- 12.1 創建正則表達式
- 12.2-12.3 模式匹配與RegExp對象
- 第十三章 Ajax
- 13.1 XMLHttpRequest對象
- 你不知道的JavaScript
- 一、作用域與閉包
- 1.1 作用域
- 1.2 詞法作用域
- 1.3 函數作用域與塊作用域
- 1.4 提升
- 1.5 作用域閉包
- 二、this與對象原型
- 2.1 關于this
- 2.2 全面解析this
- 2.3 對象
- 2.4 混合對象“類”
- 2.5 原型
- 2.6 行為委托
- 三、類型與語法
- 3.1 類型
- 3.2 值
- 3.3 原生函數