天天看點

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對象的一些了解,對于以上的任何内容有任何疑問或者有何錯誤皆可在指出,萬分感謝。

繼續閱讀