PHP 變數作用域 (Variable Scope)

變數作用域決定了變數在哪裡可以被存取。PHP 有三種主要的作用域:區域 (local)、全域 (global) 和靜態 (static)。

區域作用域 (Local Scope)

在函數內部宣告的變數只能在該函數內存取:

<?php
function test() {
    $localVar = "我是區域變數";
    echo $localVar;  // 正常輸出
}

test();
// echo $localVar;  // 錯誤:未定義的變數
?>

每次呼叫函數時,區域變數都會重新建立:

<?php
function counter() {
    $count = 0;
    $count++;
    echo "Count: $count\n";
}

counter();  // Count: 1
counter();  // Count: 1
counter();  // Count: 1
?>

全域作用域 (Global Scope)

在函數外部宣告的變數屬於全域作用域,但預設無法在函數內存取:

<?php
$globalVar = "我是全域變數";

function test() {
    // echo $globalVar;  // 錯誤:未定義的變數
}

test();
echo $globalVar;  // 正常輸出
?>

使用 global 關鍵字

使用 global 關鍵字可以在函數內存取全域變數:

<?php
$counter = 0;

function increment() {
    global $counter;
    $counter++;
}

increment();
increment();
increment();

echo $counter;  // 3
?>

使用 $GLOBALS 超全域陣列

$GLOBALS 是一個包含所有全域變數的陣列:

<?php
$name = "Alice";
$age = 25;

function showInfo() {
    echo "名稱:" . $GLOBALS['name'] . "\n";
    echo "年齡:" . $GLOBALS['age'] . "\n";
    
    // 也可以修改
    $GLOBALS['age'] = 26;
}

showInfo();
echo "新年齡:$age";  // 26
?>

global vs $GLOBALS

<?php
$x = 10;

function useGlobal() {
    global $x;
    $x = 20;
}

function useGlobals() {
    $GLOBALS['x'] = 30;
}

useGlobal();
echo $x;  // 20

useGlobals();
echo $x;  // 30
?>
過度使用全域變數會讓程式碼難以維護和測試。建議使用參數傳遞和回傳值來傳遞資料。

靜態作用域 (Static Scope)

使用 static 關鍵字宣告的變數會在函數呼叫之間保持其值:

<?php
function counter() {
    static $count = 0;  // 只會初始化一次
    $count++;
    echo "Count: $count\n";
}

counter();  // Count: 1
counter();  // Count: 2
counter();  // Count: 3
?>

static 的初始化

static 變數只會在第一次呼叫時初始化:

<?php
function test() {
    static $initialized = false;
    
    if (!$initialized) {
        echo "第一次呼叫,進行初始化\n";
        $initialized = true;
    } else {
        echo "後續呼叫\n";
    }
}

test();  // 第一次呼叫,進行初始化
test();  // 後續呼叫
test();  // 後續呼叫
?>

static 變數只能用常數初始化

<?php
function test() {
    static $a = 0;              // 正確
    static $b = 1 + 2;          // 正確(常數表達式)
    static $c = [1, 2, 3];      // 正確
    // static $d = rand();      // 錯誤:不能用函數結果初始化
    // static $e = new stdClass(); // PHP 8.1+ 才支援
}
?>

參數作用域

函數參數是區域變數:

<?php
$name = "Alice";

function greet($name) {
    $name = "Bob";  // 只修改區域變數
    echo "Hello, $name!\n";
}

greet($name);     // Hello, Bob!
echo $name;       // Alice(全域變數不受影響)
?>

傳址參數

使用 & 傳址可以修改原變數:

<?php
$name = "Alice";

function changeName(&$name) {
    $name = "Bob";
}

changeName($name);
echo $name;  // Bob(原變數被修改)
?>

閉包中的作用域

閉包(匿名函數)預設無法存取外部變數:

<?php
$message = "Hello";

$greet = function($name) {
    // echo $message;  // 錯誤:未定義的變數
    echo "Hi, $name!";
};

$greet("Alice");
?>

使用 use 關鍵字

使用 use 可以將外部變數傳入閉包:

<?php
$message = "Hello";

$greet = function($name) use ($message) {
    echo "$message, $name!";
};

$greet("Alice");  // Hello, Alice!
?>

use 傳值 vs 傳址

<?php
$count = 0;

// 傳值(預設)- 複製值
$increment = function() use ($count) {
    $count++;  // 只修改閉包內的副本
};

$increment();
echo $count;  // 0

// 傳址 - 參考原變數
$increment2 = function() use (&$count) {
    $count++;  // 修改原變數
};

$increment2();
$increment2();
echo $count;  // 2
?>

箭頭函數自動捕獲 (PHP 7.4+)

箭頭函數會自動按值捕獲外部變數:

<?php
$factor = 2;

$double = fn($n) => $n * $factor;

echo $double(5);  // 10

// 箭頭函數不能傳址捕獲
$factor = 3;
echo $double(5);  // 仍然是 10(捕獲的是建立時的值)
?>

類別中的作用域

屬性和方法

<?php
class Counter {
    private int $count = 0;  // 實例屬性
    private static int $totalCalls = 0;  // 靜態屬性
    
    public function increment(): void {
        $this->count++;  // 存取實例屬性用 $this
        self::$totalCalls++;  // 存取靜態屬性用 self::
    }
    
    public function getCount(): int {
        return $this->count;
    }
    
    public static function getTotalCalls(): int {
        return self::$totalCalls;
    }
}

$counter1 = new Counter();
$counter2 = new Counter();

$counter1->increment();
$counter1->increment();
$counter2->increment();

echo $counter1->getCount();  // 2
echo $counter2->getCount();  // 1
echo Counter::getTotalCalls();  // 3
?>

常見的作用域問題

問題 1:無法存取全域變數

<?php
$config = ['debug' => true];

function isDebug() {
    // 錯誤:無法存取 $config
    // return $config['debug'];
    
    // 解決方案 1:使用 global
    global $config;
    return $config['debug'];
    
    // 解決方案 2:使用 $GLOBALS
    return $GLOBALS['config']['debug'];
}

// 更好的解決方案:使用參數
function isDebug2($config) {
    return $config['debug'];
}
?>

問題 2:迴圈中的閉包

<?php
$callbacks = [];

for ($i = 0; $i < 3; $i++) {
    // 錯誤:所有閉包都會使用最後的 $i 值
    $callbacks[] = function() use ($i) {
        echo $i;
    };
}

// 如果 use 是傳址
for ($i = 0; $i < 3; $i++) {
    $callbacks[] = function() use (&$i) {
        echo $i;  // 都會輸出 3
    };
}
?>

問題 3:在函數內修改全域變數

<?php
$users = [];

// 不好的做法
function addUser($name) {
    global $users;
    $users[] = $name;
}

// 好的做法:使用參數和回傳值
function addUser2(array $users, string $name): array {
    $users[] = $name;
    return $users;
}

$users = addUser2($users, "Alice");
?>

最佳實踐

1. 避免使用全域變數

<?php
// 不好
$db = new Database();

function getUsers() {
    global $db;
    return $db->query("SELECT * FROM users");
}

// 好
function getUsers(Database $db) {
    return $db->query("SELECT * FROM users");
}

// 更好:使用類別封裝
class UserRepository {
    public function __construct(
        private Database $db
    ) {}
    
    public function getAll(): array {
        return $this->db->query("SELECT * FROM users");
    }
}
?>

2. 使用依賴注入

<?php
class Logger {
    public function log(string $message): void {
        echo "[LOG] $message\n";
    }
}

class UserService {
    public function __construct(
        private Logger $logger
    ) {}
    
    public function createUser(string $name): void {
        // 建立使用者...
        $this->logger->log("建立使用者:$name");
    }
}

$logger = new Logger();
$userService = new UserService($logger);
$userService->createUser("Alice");
?>

3. 適當使用 static

<?php
// 適合使用 static:快取計算結果
function fibonacci(int $n): int {
    static $cache = [];
    
    if (isset($cache[$n])) {
        return $cache[$n];
    }
    
    if ($n <= 1) {
        return $n;
    }
    
    $cache[$n] = fibonacci($n - 1) + fibonacci($n - 2);
    return $cache[$n];
}

// 適合使用 static:計數器
function getNextId(): int {
    static $id = 0;
    return ++$id;
}

echo getNextId();  // 1
echo getNextId();  // 2
echo getNextId();  // 3
?>