Function Object Scope in JavaScript

摘要:Function Object Scope in JavaScript


其實基本的function scope在另一篇 JavaScript Function 概述 有介紹過,這篇主要是講function object scope,不過直接看這篇也可以。

一般來說,網頁載入時就會建立 DOM,然後連結一堆 js 檔。以前端程式設程師的觀點來看,在 js 程式中能碰觸得到的物件和網頁載入、執行、渲染的程度有關;就像是網頁還沒完整被載入,那 js 也就無法控制到未產生出來的物件。因此當 js 是被用來和使用者互動或網頁渲染時,最好使用事件驅動導向的方式來寫,而不是傳統的 procedural paradigm。但要寫 js 函式庫時,那就要用偏 OB 或 OO 的 paradigm 來寫囉!

直接以一個最簡單的例子來看,我們在純靜態網頁中連結一個 js 檔,該網頁是沒有內容的,body內是空的。那在這 js 中的全域是指什麼呢?那區域又是什麼?

答案是要根據網頁所建立的 DOM。

HTML DOM 的最上層是一個名稱為 window 的物件,它其實也可以被看待成一個全域物件,任何東西都會綁定在它之下。所以我們在 js 中直接宣告一個區域或全域變數時,兩種變數皆會被當成 window 物件下的子物件。

先來看看下面的例子:

var v1 = 1;
var v2 = function() { alert(window.v1); };
v3 = v2;
window.v2();
window.v3();

三個變數 v1, v2 和 v3,我們直接呼叫 window.v2 來測試 v2 和 v1 是否存在於 window 中,結果是成功 alert 1,而呼叫 v3 的結果也是一樣的。我想讀者可以自行測試看看!

了解全域物件 window 後,你就會知道如果你沒有你寫的 js 程式碼是不被包在一個子物件時,那 js 程式碼中的所有東西都會被綁定在 window 中,然後當網頁要連結多個 js 檔時就開始大亂。我想有經驗的前端程式設計師在寫 js 時是不會在 window 亂搞,而是在 window 下綁定子物件,這種方法就很類似 namespace,只不過它是實體物件。

來看個例子。我定義了一個 base 函式,然後呼叫它來建立函式內的東西,接著呼叫 window.v2 和 window.v3 來測試它們存在的空間。

base = function() {
   var v1 = 1;
   var v2 = function() { alert(v1); };
   v3 = function() { alert(v1)};
};
base();
window.v2();
window.v3();

結果是 v2 不存在於 window 下,所以 window.v2 呼叫會發生錯誤,但 window.v3 是正常的被呼叫,因為在 base 中我們定義 v3 是一個全域變數,那它就會被綁定在 window 下。題外話,不知道你有沒有發現?其實 v3 所指涉的函式 alert 了區域變數 v1,這並不是個好的寫法。

在上個例子中,如果我們不想呼叫 base 函式,而是新增一個 base 物件的話,測試結果還是會一樣,因為 base 函式本身就會被當成建構子被執行一次。和直接呼叫函式的差別是有了 base 物件後,我們就可以指涉物件中的其他子物件或函式。所以想要弄成和 namespace 一樣的話,就直接建 prototype,再新增物件囉!

新增的方式如下:

baseObj = new base();

所以在上個例子中,如果沒有要新增物件時,我們也可以直接以匿名函式的方法來呼叫:

(function() {
   var v1 = 1;
   var v2 = function() { alert(v1); };
   v3 = function() { alert(v1)};
})();

當然 window.v3 是可以正常去呼叫的。

到此為止我們先整理一下:
在全域物件中的區域或全域變數皆綁定在 window 下。
在函式空間中的全域變數是綁定在 window 下。
在函式空間中的區域變數只在此函式中可視。

接下來開始介紹物件的空間。
在上述的 window 其實是個物件,base 是函式物件,baseObj 是根據 base 函式物件的 prototype 所建立的物件。函式物件在某些程度上是有點類似物件導向中的 class,函式物件能被用來創造一個物件,創造的方式是根據該函式物件的 prototype;而物件不能被拿來創造另一個物件,因為物件是沒有 prototype。

簡單地說,如果某變數指涉到的是一個函式,那該變數就會是函式物件,也就是說該變數能當成一個函式來使用,也能用來創造一個物件,然後它自己本身也是一個物件。實際的用法就看下面的例子吧!

dog = function() {
  var me = this;
  this.color = 'black';
  this.bark = function() { alert('bow'); };
};

dog.toString = function() {
  return 'dog';
};
dog();
myDog = new dog();

在這個例子中定義了一個 dog 函式物件,它可以直接被當成物件使用,這邊我們直接綁定一個函式 toString 到 dog 物件上。dog 本身也是個函式能被直接呼叫;最後它還能創造物件 myDog,依照 dog 中的 prototype。

物件綁定的方法不只一種,直接使用像 namespace 是其中之一。以 OOP 的觀點來看,toString 這種綁定法就像是 dog 的靜態方法。其他的方式如下。

dog = {
   toString: function() { return 'dog'; }
};

dog['toString'] = function() { return 'dog'; };

上述兩種方式中,第一個是直接使用大括號來定義物件內容,這種方式其實常用於第一次的定義,而不是一種加載綁定;第二個是使用陣列的方式去綁定函式,適合一個物件定義好之後,還可以再加載其他物件上去。當使用的變數名稱是字串時,適合用第二種。

在 dog 函式物件中我們可以發現 this 這個關鍵字,它是用來指涉一個函式被呼叫時的 activation object,這個 activation object 也像是被當成一個 scope,我覺得用 scope object 或許會比較好理解。用另外一種說法就是當呼叫一個函式很多次時,該函式的 scope 是有可能不相同,這是因為函式的 closure 可能不相同。我們可以把 scope 想成是一個物件 (scope object),存在於某個 scope 下的物件就是被視為綁定在該 socpe object 下的子物件。

用全域空間為例,在 js 中的全域變數會被綁定到 window 物件下,window 就是一個 scope object。在全域空間中的 this 就會指涉到 window 物件。當有一個物件是被綁定在 window 下的子物件,則該子物件就會形成它自己的一個空間,且在這空間內的 this 就會指涉到這子物件。如視 dog 為物件時,dog 是 window 下的子物件,它形成了自己的一個空間,上述的 toString 就存在 dog 空間中。那用 dog 來創造物件時,那物件空間又是怎麼一回事呢?這個部份等會就會提到。js 的 this 使用起來好像和其他語言不太一樣,但其實是一樣的,只是要多注意 scope 和 closure 的問題。

this 和 closure 還有一些議題在,這篇我不想對 closure 有太多的解釋,因為一般非資工、沒有對程式語言有研究的人,只想知道怎麼使用程式語言,所以還是直接用上面的例子來繼續講解這篇的內容吧!

當dog 函式物件直接被當成函式呼叫時,dog 函式中的 this 會指涉到什麼呢?如果你想不到,那可以先從另一個方面下手,我們可以先來看 myDog 這個由 dog 函式物件所創造出的物件,以物件導向中的類別和物件關係就可以看出,dog 函式物件中的 this 是指涉到它所創造的物件中,在這邊就是指涉到 myDog,所以 dog 函式中的 this.color 和 this.bark 同等於 myDog.color 和 myDog.bark,當然 dog 物件裡不會存在 color 和 bark。那如果單純以函式執行 dog 函式的話,其中的 this 又是怎麼回事?其實 js 會記錄目前的 scope,也就是 activation object,而 this 就會指涉到目前的這個 scope object。在進入 dog 函式後,activation object 是 window,所以直接執行 dog 函式的話,其中的 this 是指到 window,按照上面的例子,在 dog 函式中我們在 window 下綁定了 color 成員屬性和 bark 物件方法。記得 this 是指涉到 activation object,一開始 js 的 activation object 就是 window,即全域空間物件;而被創造出的物件會形成它自己的區域空間,該物件被視為區域空間物件。

dog = function() {
  this.bark = function() { alert('bow'); };
};

dog.toString = function() { return 'dog'; };

那麼上面這個例子你是否能分辨出有什麼不同了嗎?toString 被綁定在 dog 物件下,而 bark 會被綁定在由 dog 所創造出的物件下,所以呼叫 dog.bark 就會發生錯誤。

另外一種綁定變數到被創造出的物件裡的方法就是使用 prototype。我們對上面的 dog 函式物件的 prototype 定義另一個函式如下:

dog.prototype.run = function() { ... };

dog 會依照 prototype 來創造出的物件,再加上原本 this 自我指涉綁定的函式,由 dog 創造的物件裡就會有兩個函式 bark 和 run。同樣的,dog 物件中只會有 toString,不會有 run 這個函式存在。

要注意的是,一般的物件沒有 prototype,因為一般物件已經是成形的個體,也無法去形成其他的物件,所以直接綁定即可。而函式物件能使用 prototype,因為函式物件是一體兩面的物件,它本身不僅僅是物件,也能去形成另一個物件。

最後的總結:
在全域物件中的區域或全域變數皆綁定在 window 下。
在函式空間中的全域變數是綁定在 window 下。
在函式空間中的區域變數只在此函式中可視。
this 會指涉到 activation object,也就是目前的 scope object。

這篇的內容對寫 js 的人來說是很基礎但又非常重要的內容,不過很多人都不清楚就是了,反正一堆人學程式語言以為語法相似就整個概念複製過去!