2014年1月15日 星期三

PHP 與 Javascript 變數的 Scope

PHP 與 Javascript 是我日常使用的程式語言, 但是其變數的 scope (範疇, 作用域, 有效範圍) 有些不同, 偶而恍神的時候會兩者互相混淆, 故在此紀錄一下兩者差別的測試結果.

首先來看 PHP 變數的 scope. PHP 的變數分為函式外宣告的全域變數與函式內宣告的區域變數兩種, 即使同名也不會互相干擾, 全域變數作用域為整個網頁檔案, 即使在不同的 <?php ?>區段中都是指涉同一個變數, 例如下列範例 1 所示 :

測試範例 1 : http://mybidrobot.allalla.com/phptest/scope_1.php [看原始碼]  

$a=1; //全域變數
function func(){
  $a=2; //區域變數不會改變全域變數之值
  echo $a;
  }
func(); //顯示 2 (顯示區域變數之值)
echo $a; //顯示 1 (顯示全域變數之值, 不會被函式修改)

結果顯示 21, 可見函式中的程式碼看不到外面的全域變數, 因此無法存取外面的 $a 之值. 相同的, 函式外面的程式碼也看不到函式內的區域變數. 如果想要在函式內存取全域變數, 必須在函式內用 global 宣告其為全域變數, 如下列範例 2 所示 :

測試範例 2 : http://mybidrobot.allalla.com/phptest/scope_2.php [看原始碼]  

$a=1; //全域變數
function func(){
  global $a; //宣告 $a 為全域變數
  $a=2; //修改全域變數值為 2
  echo $a; 
  }
func(); //顯示 2 (顯示全域變數之值)
echo $a; //顯示 2 (顯示全域變數之值) 

可見在函式內部, 只要先用 global 宣告, 就可以存取外面的全域變數了, 上例中函式內的 $a 已指向外面的同名全域變數, 因此將其值改為 2, 事實上就是改了外面那個全域變數 $a 之值. 注意, global 必須獨立宣告, 不可以宣告同時賦值, 例如下列用法是錯誤的 :

global $a=2;  //錯誤用法

如果我們傳參數進去會怎樣? 當然會因為 global 的關係改到外部全域變數之值啦, 如下列範例 2-1 所示 :

測試範例 2-1http://mybidrobot.allalla.com/phptest/scope_2_1.php [看原始碼] 

$a=1; //全域變數
function func($a){
  global $a; //宣告 $a 為全域變數
  $a=2; //修改全域變數值為 2
  echo $a; //顯示 2
  }
func(3); //顯示 2 (顯示全域變數之值)
echo $a; //顯示 2 (顯示全域變數之值)

結果顯示 22, 雖然傳入 3, 但在函式內又被改為 2, 故輸出 2, 同時因為 global 緣故, 也把外部之同名全域變數改為 2. 

除了 global 之外, 還可以利用超全域變數 $GLOBALS[] 陣列來存取全域變數 (關聯式陣列, 用變數名稱當索引), 如下列範例 3 所示 :

測試範例 3 : http://mybidrobot.allalla.com/phptest/scope_3.php [看原始碼]  

$a=1; //全域變數
function func(){
  $GLOBALS["a"]=2; //修改全域變數值為 2
  echo $GLOBALS["a"]; 
  }
func(); //顯示 2 (顯示全域變數之值)
echo $a; //顯示 2 (顯示全域變數之值) 

可見效果是一樣的. 因為每宣告一個全域變數, PHP 就會在 $GLOBALS 陣列中添加一個元素, 儲存指向該全域變數的參考.

當函式呼叫完畢, 函式內的區域變數就會從記憶體中釋放, 例如下列範例 4 所示 :

測試範例 4 : http://mybidrobot.allalla.com/phptest/scope_4.php [看原始碼]  

$a=1; //全域變數
function increment(){
  echo ++$a; //沒有設值預設為 null, 增量計算時會轉為 0
  }
echo $a; //顯示 1 (顯示全域變數之值)
increment(); //顯示 1 (null 值為 0 增量後為 1)
increment(); //顯示 1 (null 值為 0 增量後為 1)

結果顯示 111, 後面兩個 1 是呼叫 increment() 函式的結果, 因為區域變數 $a 沒有設值 (=null), 增量計算時會被 PHP 認為是整數, 而整數變數預設為 0, 故先轉為 0 再增量, 故輸出 1, 但是因為函式執行完畢就釋放記憶體中的區域變數, 因此沒辦法記憶其值, 不論呼叫幾次都是輸出 1. 若要記憶區域變數之值, 必須宣告為靜態變數, 如下列範例 5 所示 :

測試範例 5  : http://mybidrobot.allalla.com/phptest/scope_5.php [看原始碼]  

$a=1; //全域變數
function increment(){
  static $a;
  echo ++$a; //增量
  }
echo $a; //顯示 1 (顯示全域變數之值)
increment(); //顯示 1 (預設 null 值為 0 增量後為 1)
increment(); //顯示 2 

對於 Javascript 來說, 它也是有分宣告於函式外的全域變數與函式內的區域變數, 如下列範例 6 所示 :

測試範例 6  : http://mybidrobot.allalla.com/phptest/scope_6.htm [看原始碼]  

    var a=1;   //全域變數
    function func(){
      var a=2;   //區域變數
      document.write(a);  //顯示 2
      }
    func();  //顯示 2
    document.write(a);  //顯示 1

結果顯示 21, 這結果與 PHP 是相同的, 亦即全域變數與區域變數互不侵犯, 函式內看不到外面的變數; 函式外也看不到函式內的變數. 但是如果我們將函式內的變數宣告關鍵字 var 拿掉, 結果就不同了, 如下列範例 7 所示 :

測試範例 7  : http://mybidrobot.allalla.com/phptest/scope_7.htm [看原始碼]  

    var a=1;  //全域變數
    function func(){
      a=2;  //函式內變數未宣告直接賦值, 解譯器將其視為全域變數
      document.write(a);  //顯示 2
      }
    func();  //顯示 2
    document.write(a);  //顯示 2

結果顯示 22, 很奇怪吧! 函式內的變數有沒有用 var 宣告是有很大差別的, 有用 var 宣告的就是區域變數, 即使與函式外的全域變數同名也沒關係, 井水不犯河水, 但若沒有用 var 宣告就直接使用, 就會被 Javascript 解譯器視同全域變數處理 (跟 PHP 使用 global 存取全域變數效果一樣), 如果與全域變數同名, 就會改變全域變數的值; 反過來說也是一樣, 函式內本該對外隱藏的區域變數就全都露了, 從函式封裝上來看,  Javascript 有不夠嚴謹的弱點, 這是與 PHP 的不同之處. 寫 Javascript 要養成良好習慣, 最好用 var 宣告變數.

Javascript 的變數可以不宣告直接賦值, 其實是解譯器在執行前的預編譯階段自動幫我們加上去了, 但只對函式外的變數自動加 var, 不會對函式內的變數加 var, 所以範例 7 函式裡的 a 變數就被當成全域變數處理了. 但是, 如果有傳入參數, 那結果又不同了, 如下列範例 7-1 所示 :

測試範例 7-1  http://mybidrobot.allalla.com/phptest/scope_7_1.htm [看原始碼] 

    var a=1;  //全域變數
    function func(a){  //傳入參數 a
      a=2;  //此 a 變數被視為參數 (區域變數), 故不會更改外部 a 之值
      document.write(a);  //顯示 2
      }
    func(3); //顯示 2
    document.write(a);  //顯示 1

結果顯示 21, 此例中函式 func() 帶了一個參數 a, 雖然函式內第一行 a=2 沒有用 var 宣告, 但是因為有傳入參數 a=3, 因此變數 a 會被解譯器視為傳入之參數, 而參數就是區域變數, 雖然傳入值為 3, 但隨即被改成 2, 故呼叫 func(3) 卻輸出 2. 當然它也不會改到全域變數 a 之值, 因此最後一行指令仍輸出 1. 這跟上面 PHP 的範例 2-1 結果截然不同.

最後來研究一下靜態變數, 在 Javascript 規格裡是沒有靜態變數的, 但範例 7 函式內未宣告的變數等同於全域變數, 這個特性倒是可以拿來當作靜態變數使用, 如下列範例 8 所示 :

測試範例 8  : http://mybidrobot.allalla.com/phptest/scope_8.htm [看原始碼]  

    var a=1;  //全域變數
    function increment(){
      ++a;  //函式內變數未宣告直接賦值, 解譯器將其視為全域變數
      }
    increment();  //a 增量為 2
    increment();  //a 增量為 3
    document.write(a);  //顯示 3

可見因為函式內同名變數未宣告即賦值, 被視為全域變數, 當函式執行完畢時, a 不會消失, 所以每呼叫一次就增量, 跟上面 PHP 的 static 變數效果一樣.