Skip to content

变量作用域

变量的作用域是指变量可以被访问的代码区域。PHP 有两种主要作用域:全局作用域函数作用域。理解作用域规则是避免变量访问错误、编写正确程序的关键。

前置知识

  • 已掌握 变量基础 的命名和赋值
  • 了解 PHP 函数的基本概念
  • 熟悉 globalstatic 关键字的基本用途

基础概念

PHP 中的作用域规则相对简单:在函数之外定义的变量属于全局作用域,在函数内部定义的变量属于局部作用域。PHP 没有块级作用域(与 Java、C++ 等语言不同),在 ifforwhile 等块内定义的变量在块外依然可见。

PHP 的作用域类型

作用域类型说明关键字
全局作用域函数外定义的变量-
局部作用域函数内定义的变量-
静态作用域函数内声明为 static 的变量static
超级全局作用域任何位置都可访问的预定义变量$_GET, $_POST

全局作用域与局部作用域

全局变量

在函数之外定义的变量默认仅存在于全局作用域中。函数内部无法直接访问全局变量:

php
<?php
declare(strict_types=1);

$a = 1; // 全局作用域

function test(): void
{
    // 访问全局变量 $a 会触发未定义变量警告
    // PHP 8.0+:E_WARNING;PHP 8.0 前:E_NOTICE
    // echo $a;
    echo "In function: a is not accessible\n";
}

test();
echo $a . "\n"; // 1(全局作用域内正常访问)

局部变量

在函数内部创建的变量仅在该函数执行期间存在,函数执行完毕后变量即被销毁:

php
<?php
declare(strict_types=1);

function createLocalVariable(): void
{
    $localVar = "I am local";
    echo "Inside function: {$localVar}\n";
}

createLocalVariable();
// echo $localVar; // Error: Undefined variable

没有块级作用域

与 C/Java 的区别

PHP 没有块级作用域ifforwhile 等语句块内定义的变量在块外依然可用。这与其他语言(如 Java、C++、JavaScript 的 let)不同。

php
<?php
declare(strict_types=1);

// PHP 没有块级作用域
for ($i = 0; $i < 3; $i++) {
    $loopVar = "iteration {$i}";
}
echo $loopVar . "\n"; // iteration 2(块外仍然可用)

if (true) {
    $blockVar = "from if block";
}
echo $blockVar . "\n"; // from if block(块外仍然可用)

global 关键字

使用 global 声明全局变量

在函数内部使用 global 关键字可以访问全局变量。global 会创建一个指向同名全局变量的引用:

php
<?php
declare(strict_types=1);

$a = 1;
$b = 2;

function sum(): void
{
    global $a, $b;
    $b = $a + $b;
}

sum();
echo $b . "\n"; // 3(全局变量 $b 被函数修改)

global 创建引用

global 关键字的底层实现是创建一个引用。如果在函数内使用 global 导入了变量后又重新赋值引用,需要注意行为差异:

php
<?php
declare(strict_types=1);

// global 的引用行为示例
function testGlobalRef(): void
{
    global $obj;
    $new = new stdClass();
    $obj = &$new; // 注意:这不会修改全局 $obj
}

function testGlobalNoRef(): void
{
    global $obj;
    $new = new stdClass();
    $obj = $new; // 正确赋值
}

$obj = null;
testGlobalRef();
var_dump($obj); // NULL(引用赋值未生效)

testGlobalNoRef();
var_dump($obj); // object(stdClass)

谨慎使用 global

过度使用 global 会降低代码的可维护性和可测试性。推荐通过参数传递和返回值来实现函数间的数据交换,而不是依赖全局变量。

$GLOBALS 超级全局变量

使用 $GLOBALS 替代 global

$GLOBALS 是一个关联数组,键名为变量名(不含 $),值为变量的内容。它是一个超级全局变量,在任何作用域中都可访问:

php
<?php
declare(strict_types=1);

$a = 1;
$b = 2;

function sumUsingGlobals(): void
{
    $GLOBALS["b"] = $GLOBALS["a"] + $GLOBALS["b"];
}

sumUsingGlobals();
echo $b . "\n"; // 3

PHP 8.1+ 的变化

从 PHP 8.1.0 起,$GLOBALS 变为只读。不再支持通过 $GLOBALS 来写入全局变量:

php
<?php
declare(strict_types=1);

// PHP 8.1+ 中以下代码会报错
// $GLOBALS["newVar"] = "value"; // Fatal error

static 静态变量

基本用法

静态变量仅在函数作用域内存在,但当函数执行结束后其值不会丢失。下次调用该函数时,静态变量保留上次的值:

php
<?php
declare(strict_types=1);

// 不使用 static:每次调用都重置为 0
function counterWithoutStatic(): int
{
    $count = 0;
    $count++;
    return $count;
}

echo counterWithoutStatic() . "\n"; // 1
echo counterWithoutStatic() . "\n"; // 1(总是 1)

// 使用 static:保持上次值
function counterWithStatic(): int
{
    static $count = 0;
    $count++;
    return $count;
}

echo counterWithStatic() . "\n"; // 1
echo counterWithStatic() . "\n"; // 2
echo counterWithStatic() . "\n"; // 3

静态变量与递归

静态变量常用于递归函数中控制终止条件:

php
<?php
declare(strict_types=1);

function recursiveCount(int $max, int $current = 1): void
{
    static $depth = 0;
    $depth++;
    echo str_repeat("  ", $depth - 1) . "Level {$depth}\n";

    if ($current < $max) {
        recursiveCount($max, $current + 1);
    }

    $depth--;
}

recursiveCount(4);

静态变量的初始化表达式(PHP 8.3+)

版本说明

PHP 8.3.0 之前,静态变量只能使用常量表达式初始化。从 PHP 8.3.0 起,允许使用动态表达式(如函数调用)初始化静态变量。

php
<?php
declare(strict_types=1);

function getConfigValue(): int
{
    static $timeout = sqrt(121);  // PHP 8.3+ 允许
    static $count = 0;            // 所有版本允许
    static $sum = 1 + 2;         // 所有版本允许(常量表达式)

    $timeout++;
    return $timeout;
}

echo getConfigValue() . "\n"; // 12(sqrt(121) = 11, 然后 +1)

继承方法中的静态变量(PHP 8.1+)

从 PHP 8.1.0 起,继承(而非覆盖)的方法中的静态变量在父类和子类之间共享:

php
<?php
declare(strict_types=1);

class Foo
{
    public static function counter(): int
    {
        static $count = 0;
        $count++;
        return $count;
    }
}

class Bar extends Foo
{
    // 继承了 counter() 方法
}

echo Foo::counter() . "\n"; // 1
echo Foo::counter() . "\n"; // 2
echo Bar::counter() . "\n"; // 3(PHP 8.1+ 共享静态变量)
echo Bar::counter() . "\n"; // 4
// PHP 8.1 之前:Bar::counter() 返回 1 和 2(各自独立)

详细说明

超级全局变量

超级全局变量(superglobals)在代码的任何位置都可访问,无需 global 声明:

变量说明
$GLOBALS所有全局变量的引用
$_SERVER服务器和执行环境信息
$_GETHTTP GET 请求参数
$_POSTHTTP POST 请求参数
$_FILESHTTP 文件上传变量
$_COOKIEHTTP Cookie 数据
$_SESSION会话变量
$_REQUEST$_GET + $_POST + $_COOKIE
$_ENV环境变量
php
<?php
declare(strict_types=1);

// 超级全局变量在任何作用域内都可直接访问
function showServerInfo(): void
{
    echo "Server: " . ($_SERVER["SERVER_NAME"] ?? "CLI") . "\n";
    echo "Request URI: " . ($_SERVER["REQUEST_URI"] ?? "N/A") . "\n";
}

showServerInfo();

include 文件中的作用域

include 一个文件时,被包含文件继承包含语句所在位置的变量作用域:

php
<?php
declare(strict_types=1);

// main.php
$appName = "MyApp";
$appVersion = "2.0";

// $appName 和 $appVersion 会传递到 included.php
include __DIR__ . "/included.php";

如果 include 在函数内部执行,被包含文件只继承函数作用域内的变量:

php
<?php
declare(strict_types=1);

function renderTemplate(string $file, string $title): void
{
    // $file 和 $title 在函数作用域内
    // 被包含文件只能访问 $file 和 $title
    include $file;
}

匿名函数与箭头函数的作用域

普通匿名函数(闭包)需要使用 use 关键字从父作用域继承变量。而箭头函数(PHP 7.4+)自动捕获父作用域变量:

php
<?php
declare(strict_types=1);

$multiplier = 3;

// 匿名函数:需要 use 关键字
$double = function (int $x) use ($multiplier): int {
    return $x * $multiplier;
};

// 箭头函数:自动捕获父作用域变量
$triple = fn(int $x): int => $x * $multiplier;

echo $double(5) . "\n";  // 15
echo $triple(5) . "\n";  // 15

实战示例

使用依赖注入替代 global

php
<?php
declare(strict_types=1);

// 不推荐:使用 global
$config = ["debug" => true];

function getConfigBad(): array
{
    global $config;
    return $config;
}

// 推荐:通过参数传递
function getConfigGood(array $config): array
{
    return $config;
}

// 更推荐:使用类和依赖注入
class AppConfiguration
{
    public function __construct(
        private readonly array $settings = []
    ) {
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->settings[$key] ?? $default;
    }
}

$appConfig = new AppConfiguration(["debug" => true, "cache_ttl" => 3600]);
echo $appConfig->get("debug") ? "true" : "false"; // true

静态变量实现单例计数器

php
<?php
declare(strict_types=1);

class RequestCounter
{
    private static ?int $requestCount = null;

    public static function increment(): int
    {
        if (self::$requestCount === null) {
            self::$requestCount = 0;
        }
        self::$requestCount++;

        return self::$requestCount;
    }

    public static function getCount(): int
    {
        return self::$requestCount ?? 0;
    }

    public static function reset(): void
    {
        self::$requestCount = null;
    }
}

echo RequestCounter::increment() . "\n"; // 1
echo RequestCounter::increment() . "\n"; // 2
echo RequestCounter::increment() . "\n"; // 3
echo RequestCounter::getCount() . "\n";  // 3

闭包捕获变量的注意事项

php
<?php
declare(strict_types=1);

// use 默认按值捕获
$counter = 0;
$increment = function () use ($counter): int {
    return ++$counter;
};
echo $increment() . "\n"; // 1
echo $increment() . "\n"; // 1(counter 是副本)

// use 按引用捕获
$counter = 0;
$incrementRef = function () use (&$counter): int {
    return ++$counter;
};
echo $incrementRef() . "\n"; // 1
echo $incrementRef() . "\n"; // 2(共享同一个 $counter)

注意事项

1. 避免滥用 global

global 关键字会引入隐式依赖,降低代码的可测试性和可维护性。

2. 注意 $GLOBALS 的只读限制

PHP 8.1+ 中 $GLOBALS 不可写,旧代码需要迁移。

3. 静态变量初始化只执行一次

php
<?php
declare(strict_types=1);

function testStaticInit(): void
{
    static $val = rand(1, 100);
    echo $val . "\n";
}

testStaticInit(); // 某个随机数
testStaticInit(); // 同一个随机数(初始化只执行一次)

4. 匿名函数中的静态变量

如果匿名函数每次被重新创建,其内部静态变量也会重新初始化:

php
<?php
declare(strict_types=1);

function createCounter(): callable
{
    return function (): int {
        static $count = 0;
        return ++$count;
    };
}

$counter1 = createCounter();
$counter2 = createCounter();

echo $counter1() . "\n"; // 1
echo $counter1() . "\n"; // 2
echo $counter2() . "\n"; // 1(独立的静态变量)
echo $counter2() . "\n"; // 2

最佳实践

  1. 通过参数传递替代 global:避免使用全局变量,通过参数和返回值传递数据
  2. 使用类属性替代全局状态:将全局状态封装在类的静态属性中
  3. 优先使用箭头函数:简单闭包使用 fn,自动捕获父作用域
  4. 静态变量用于特定场景:如计数器、递归深度跟踪等
  5. 使用类型声明:函数参数和返回值使用严格类型
  6. 初始化所有变量:在使用前显式赋初值
  7. 使用 readonly 属性(PHP 8.1+):防止意外修改类属性

下一节

了解了变量作用域之后,接下来将学习可变变量——动态确定变量名的特殊用法。请阅读 可变变量

参考链接