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
?>