JavaScript執行環境、作用域及this值

執行環境


JavaScript的執行環境定義了其中的變量和函數有權訪問的其他數據,即規定了在其內部能夠訪問什麼數據。每個執行環境都有一個與之相關聯的“變量對象”,環境中定義的變量和函數都保存在這個對象之中,可以理解爲環境內的變量和函數都是這個變量對象的屬性和方法,但是這個變量對象我們無法通過js訪問到。

可能這個概念有點難以理解,我們來看看一個例子:全局執行環境是最外圍的一個執行環境,在web瀏覽器中,全局執行環境被認爲是window對象,也即此時 的變量對象可以認爲是window對象:

var name = 'paper_crane';
function sayName() {
  alert('paper_crane');
}
alert(name)          // paper_crane
sayName();           // paper_crane
window.sayName();    // paper_crane
alert(window.name);  // paper_crane

在上面的例子中,聲明瞭一個全局變量name和一個全局函數sayName,無論是直接調用它們還是當window對象的一個屬性或方法使用都能正確的執行並得到我們預期的結果,所以執行環境裏面定義的所有變量和函數都會被當成其變量對象的屬性和方法,此對象內部的所有的屬性和方法都能訪問到此對象內部的其他屬性和方法。這樣就能用一個具體的變量對象來描述一個抽象的執行環境,window是唯一一個可以通過js獲取的變量對象,即使如此,在非必須的情況下不建議使用這種方式使用自定義的全局變量和全局函數。

每個函數都有自己的執行環境。當執行環境進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行完成之後,棧將其環境彈出,把控制權返回給之前的執行環境。

作用域和作用域鏈


看完執行環境的定義與理解,我們發現,執行環境不就是我們所說的作用域嗎?是的,可以認爲作用域就是執行環境,就如上面的代碼例子,存在兩個執行環境:全局執行環境和sayName函數內部執行環境,也即爲全局作用域和sayName函數局部作用域。我們可以推測:全局作用域裏面無法訪問到局部作用域裏面的變量,而在局部作用域裏面可以訪問到全局作用域裏面的變量。來看一下例子:

var name = 'paper_crane';
function sayName() {
  alert(name);
  var age = 22;
}
sayName();           // paper_crane
alert(age);          // throw an error

在這個例子中,變量name是全局變量,變量age是sayName函數局部作用域的變量,在全局作用域裏面使用age會報錯,而在局部作用域裏面使用全局作用域裏面的name變量卻不會報錯。這個結果符合我們的預期,而JavaScript就是通過作用域鏈實現這種變量訪問權限的。

當js代碼在一個環境中執行時,會創建變量對象的一個作用域鏈,作用域鏈的用途是保證對執行環境有權訪問的所有的變量和函數的有序訪問,作用域鏈的前端始終指向當前執行的代碼所在環境的變量對象,如果這個環境是函數,則將其活動對象作爲變量對象,函數的活動對象最初只包含arguments對象,此後在該函數內部創建所有變量和函數都會變成該活動對象的屬性和方法,當函數執行結束之後就會銷燬這個活動對象。作用域鏈中的下一個變量對象來自包含此執行環境的外部環境的變量對象,以此類推,全局執行環境的變量對象始終是作用域鏈中的最後一個對象。

標識符解析是由作用域鏈前端一級一級地往後搜索標識符的過程,此過程是一個單向的過程,並且只要找到了標識符就會停止搜索,如果回溯到全局執行環境的變量對象還是無法找到此標識符,那麼就會報錯。現在再看看上面的那個例子,此例子有兩個執行環境,第一個執行環境的變量對象是全局對象window,而sayName函數內部的對象變量我們設爲obj;那麼sayName函數內部的作用域鏈就是由obj和window組成的,由obj指向window,在sayName函數內部訪問name變量時,瀏覽器會先去搜索obj對象,但是在obj對象沒有找到name屬性,接着回溯到window對象,成功找到了name屬性,停止搜索,取得name變量的值,而由於在作用域鏈搜索標識符的過程是單向的,所以在全局環境無法訪問到age變量。我們再看看下面這個例子:

var name = 'paper_crane';
function sayName() {
  var name = 'crane';
  alert(name);
}
sayName();           // crane

在上面的例子中,sayName函數內部的變量對象和全局變量對象都聲明瞭一個name變量,但是在內部訪問變量name的時候,就會訪問當前變量對象的name變量,接着就是停止向後回溯了。這個不難理解,但是有一種特殊的情況需要注意一下的:

var name = 'paper_crane';
function sayName() {
  alert(name);            // undefined
  var name = 'crane';
  alert(name);            // crane
}
sayName();

在上面的例子中,第一個alert需要訪問name變量,但是在sayName內部環境裏面還沒有聲明(起碼在我們看來還沒有),所以應該是彈出“paper_crane”纔對,但是實際上卻彈出了“undefined”,爲何?其實JavaScript在聲明變量的時候,會把聲明直接提前到代碼執行前面,所以當第一個alert函數訪問name變量時,會搜索sayName的內部變量對象,而內部變量對象已經聲明瞭name變量,而初始化會則會在代碼設定的位置,這個跟函數聲明提升有點相似,所以第一個alert彈出的是“undefined”。所以在聲明變量的時候,最好在進入此執行環境就把所有的變量都聲明好,不要在邏輯代碼中間隨意的聲明一個變量。

接着對於作用域還需要補充的一點是,JavaScript沒有塊級作用域,也就是說,JavaScript不像c或者java語言一樣,在代碼塊(以{}分離)裏面聲明的變量能夠在當前的執行環境中訪問得到,如下:

function sayAge() {
  if (true) {
    var age = 22;
    alert(age);   // 22
  }
  alert(age);     // 22
}
sayAge();

由於沒有塊級作用域,大量聲明會非常容易造成作用域污染,此時只要使用一個立即執行函數就可以模擬塊級作用域了:

var name = 'paper_crane';
function sayAge() {
  (function() {
    if (true) {
      var age = 22;
      alert(age);   // 22
    }
  })();
  alert(age);       // ReferenceError
}
sayAge();


詞法作用域


JS採用的是詞法作用域。詞法作用域可以這樣理解(純屬個人理解):函數裏面調用的變量在函數聲明的時候就已經確定了,確定規則:當在本作用域找不到該變量的時候,就會向上層的作用域尋找,直到找到或者最終找不到,但是如果本作用域存在這個變量,則絕對不會向上尋找。如下面的例子:

var name = 'paper_crane';
function sayName() {
  alert(name);            // undefined
  var name = 'crane';
  alert(name);            // crane
}
sayName();

這個例子上面舉過,不過上面主要是說明變量聲明會提升。但實際上詞法作用域的本質就是通過變量和函數聲明提升來實現的。上面sayName函數在其作用域內聲明瞭變量name,當函數內使用name變量的時候,在本作用域找到了name變量,所以不會向上層尋找這個變量。

this對象


this對象是一個指針,指向的對象是在運行時基於函數的執行環境綁定的,但是this對象不是指向函數運行時所在的執行環境(變量對象),因爲剛纔在說執行環境的時候說過:執行環境只能在執行代碼解釋的後臺使用到,除了全局執行環境,無法使用js代碼訪問到。而如果this對象指向了函數運行時候所在的執行環境,就違反剛纔所說的原則。所以this對象的指向取決於函數運行時的執行環境,但是不指向執行環境。this指向有以下幾種情況:

指向window


一般情況下,函數內部的this對象都會指向window。來看看例子:

var name = 'paper_crane';
function globalFunction() {
  var name = 'crane';
  alert(this.name);          // paper_crane

  (function() {
    alert(this.name);        // paper_crane
    alert(name);             // crane
  })();
  
  function innerFunction() {
    alert(this.name);        // paper_crane
    alert(name);             // crane
  }
  
  innerFunction();
}
globalFunction();

在上面的例子中,聲明瞭一個變量name和聲明瞭一個globalFunction全局函數,在函數內部又聲明瞭一個name變量,輸出this.name,輸出的是全局變量name的值;接着聲明一個匿名函數,輸出this.name的值也是得到全局變量name的值;接着在內部又聲明瞭一個innerFunction函數,在其內部輸出this.name的值時得到的並不是局部變量name的值,而是全局變量name的值。以上的例子說明全局函數、匿名函數和局部函數的this對象指向的都是window,需要進一步驗證的讀者可以直接輸出this的值。

指向函數所有者


看到所有者相信大家和我一樣想到的是對象,一個對象擁有自己的方法,此時方法內部的this指向此對象。

var obj = {
  name : 'paper_crane',
  showName: function() {
    alert(this.name);        // paper_crane
  }
}
obj.showName();

在上面的例子中聲明瞭一個對象obj,包括一個name屬性和showName方法,sayName方法可以正確的訪問到name屬性,所以this指向的是showName函數的所有者obj。除了這種直接聲明一個對象的情況,在構造類構造函數的時候也指向所有者。例如:

function Student() {
  this.name = 'paper_crane';
  this.showName = function() {
    alert(this.name);         // paper_crane
  }
}
var crane = new Student();
crane.showName();

在上面的例子中,實現了一個Student類構造函數(實際上ES規範中並沒有類的概念,這只是實現面向對象編程的方法,至於面向對象編程有時間再詳談),類裏面有一個name屬性,有一個showName方法,然後聲明瞭一個Student的實例crane,此時調用crane的方法showName就會發現輸出的是paper_crane,所以this指向的是其所有者crane。

改變this的指向


通過上面的例子我們知道this對象指向的是一個對象,但是這是根據函數運行時根據執行環境決定的,但是這並不代表我們不能改變其this的指向。函數對象的 call、apply、bind方法(這是函數對象非繼承而來的方法)可以改變this對象的this對象指向。

var name = 'paper_crane';
var thisObj = {
  name : 'crane'
}
function showName() {
  alert(this.name);
}

showName();                        // paper_crane
showName.call(thisObj);            // crane
showName.apply(thisObj);           // crane
var fun = showName.bind(thisObj);
fun();                             // crane

上面的例子演示了使用函數對象的call、apply、bind方法改變函數this對象的指向,至於以上三個方法的區別和使用具體使用方法有時間再詳談。

箭頭函數中的this


看了詞法作用域和this的指向相關內容中之後,我們可以知道,匿名函數及普通函數內this並不遵循詞法作用域的規則。但是箭頭函數裏面的this則會遵循詞法作用的規則,而且,箭頭函數裏面沒有this值,只會向上層尋找this值。因爲箭頭函數裏面的this遵循詞法作用域規則,所以無法給箭頭函數使用call、apply和bind方法來改變其this指向。

var name = 'paper crane';
var obj = {
		name: 'crane',
		normal: function() {
			console.log(this.name);
		},
		arrow: () => {
			console.log(this.name);
		}
	};

obj.normal();       // crane
obj.arrow();        // paper crane

上面的例子中。normal方法是作爲obj的方法被調用,所以輸出crane。而arrow方法是個箭頭函數,其本身是沒有this值的,所以向其外層尋找this,在上面的例子中也就是全局環境,全局環境的this爲window對象,所以arrow的this對象就是window。

以上是個人對作用域、作用域鏈和this對象的一些理解,對於以上的任何內容有任何疑問或者有何錯誤皆可在指出,萬分感謝。
發佈了32 篇原創文章 · 獲贊 11 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章