一、作用域(what?)
官方解釋是:“一段程序代碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的代碼范圍就是這個名字的作用域。”
單從文字理解比較難懂,舉個栗子:
function outer(){ // 聲明變量 var name = "ukerxi"; // 定義內部函數 function inner() { console.log(name); // 可以訪問到 name 變量 } } console.log(name); // 報錯,undefined
其中變量name聲明在 oute r函數中,當在 outer 中定義一個 inner 函數進行輸出 name,可以得到正確的值,而在 outer 外進行輸出 name 出現 undefined 錯誤;在此可以看出 outer 函數即為 name 變量的作用域(證明過程比較粗略,但結論還是正確的--_--);
二、作用域(why?)
作用域的使用,提高了程序的邏輯的局部性,增強程序的可靠性,以及避免命名沖突;為代碼的模塊化開發提供便利;根據上面提到的函數作用域,name 變量被局限在了 outer 函數中,在其他的函數中也可以定義相同名字的變量,兩者之間不會互相影響;
三、js 中的作用域
先說ES5版本及更低版本的,因為在 ES6 上,重新定義了幾個決定作用域的關鍵字;
- 沒有塊級作用域
在javaScript中,不像C、java等擁有塊級作用域;常見塊級作用域,例如:
// C語言實現 for(int i= 0; i<10; i++){ // 中括號里面就是塊級作用域 } if(ture){ int i = 1; // 這里也是塊級作用域 } printf("%d/n",i); // --“use an undefined variable:i” //這里是訪問不到for語句中的i與if語句中的i變量的
當然這些在javaScript中是沒有的,一般來說只有塊級作用域;所以使用時須注意作用域的影響,例如:
for(var i= 0; i<10; i++){ // do something } console.log(i); // --10
在程序設計過程中可以使用函數作用域,進行模擬塊級作用域;例如:
function loop(){ for(var i= 0; i<10; i++){ // do something } } loop(); console.log(i); // --undefined
javaScript是靈活可變的,同樣上面這個例子,可以使用自執行函數重寫實現,這樣就減少了調用這一步;
(function (){ for(var i= 0; i<10; i++){ // do something } }()); console.log(i); // --undefined
- ES6中的作用域
在ES6中新增加了(let,const)關鍵字,進行定義變量,解決了沒有塊級作用域的限制;
let:let允許你聲明一個作用域被限制在塊級中的變量、語句或者表達式。與var關鍵字不同的是,它聲明的變量只能是全局或者整個函數塊的。let聲明的變量只在其聲明的塊或子塊中可用,這一點,與var相似。二者之間最主要的區別在於var聲明的變量的作用域是整個封閉函數。
具體如下:
function range () { // let 和var 相同的地方,都有函數作用域 var name = 'ukerxi'; let nameOuter = 'outer'; for (var j = 0; j < 1; j++) { console.log(name) } console.log("輸出j變量", j); // ==> 1 for (let i = 0; i < 1; i++) { console.log(nameOuter) } console.log("輸出i變量", i); // 報錯 undefined }
可以看出,使用let 定義的i變量,在for語句外進行輸出時,會進行報錯,說明i不在該作用域內,i的作用域在for包裹的作用內;
- 作用域鏈
每個函數都有自己的執行環境,包含當前環境的變量訪問關系,與之相關聯的就是“變量對象”,如果是當前函數的變量對象,也可稱為“活動對象”;此對象中包含了,當前函數可訪問的變量及函數;變量對象,最開始包含的對象是參數的arguments對象,然后是在函數中定義的其他變量及方法;例如:
function fn(name){ var text = "test"; } // 變量對象中包含:命名函數fn變量、參數name、內部變量test
當然這個變量對象是不可訪問的,只提供后台引擎編譯執行使用;當定義有多個變量對象嵌套,這些變量對象就組成了作用域鏈;例如:var name = "global"; function super() { var name = "super"; function sub(){ var name = "sub"; } }
作用域鏈:
在作用域最前端的是活動對象,而最后端是全局執行環境window(瀏覽器宿主中);變量訪問原則是,根據作用域前端往上進行搜索,如果提前搜索到變量,則停止搜索,例如上面這個例子中,name變量的值是"sub"因為其在最前端的變量對象中已經定義了,就不會往上繼續檢索;
- 延長作用域
有兩種方法可以將作用域進行延長:
①、try-catch 語句的catch塊
②、with 語句
兩個語句都是在原本的作用域最前端進行添加一個變量對象;例如:
var name = "global"; function test(){ var name = "sub"; with(window){ console.log(name); } } test(); // -- "global"
作用域鏈:
所以檢索變量時,會先在最前端的window變量對象中檢索;當然,在嚴格模式下已經禁用了with語句,編程時,最好向后兼容,廢棄使用with語句; - 執行環境只與函數的聲明及定義位置有關
當一個函數定義后其執行環境與作用域鏈就已經確定了,不會因為執行位置改變而改變,具體例子:
var name = "global"; function getName(){ console.log(name); } function test (){ var name = "inner"; getName(); } // 執行test test(); // -- global
運行test 函數,其中test 函數執行的是 getName 進行輸出 name 變量,輸出的是全局變量的信息;即當 getName 定義時就已經確定了自己的作用域及執行環境,因而不會因為執行位置的不同而輸出不同的信息;當然有一種情況不一樣,那就是靈活的 this
- this的動態綁定
與作用域鏈及執行環境不同,this是根據執行時的接受者進行綁定的,改變this的幾種方法:
①、new 關鍵字
②、call / apply 方法
③、直接調用構造函數
具體例子如下:
// 聲明一個類 function Person (){ this.name = "ukerxi"; } // 使用new關鍵字,使this執行新建對象 // 其實是構造函數默認返回this var men1 = new Person(); // this綁定到men1上 // 聲明一個空對象,使用call/apply 進行綁定 var men2 = {}; Person.call(men2); // this綁定到men2上 // 直接執行構造函數 Person(); // this綁定到window上(使用嚴格模式則會報錯,this指向undefined)
【結束語】
系列文章,包括了原創,翻譯,轉載等各類型的文章;一方面是為了自己總結,另一方面頁希望可以共享知識;在技術方面有輸入,也要有所輸出,才能更進一步!文章基於自己的實踐、閱讀及理解,如有不合理及錯誤的地方,煩請各大佬評論指出,以便改正,感謝!