Skip to content

变量的作用域

作用域(scope)回答一件事:这个名字在哪些地方有效。同名变量在不同作用域里可以各存各的,互不覆盖。保存为 scope.php 后执行 php scope.php

php
<?php

$site = "外面";

function show(): void
{
    $site = "函数里";
    echo $site, "\n";
}

show();
echo $site, "\n";

输出:

函数里
外面

变量定义 里先记过这一条。本篇把它展开:全局、局部、静态、以及 PHP 没有块级作用域这些容易踩的点。

三种你现在就要分清的范围

范围在哪声明哪里能用
全局函数外面函数外;函数内默认不能直接用
局部函数里面(含参数)仅该函数这一次调用
超全局PHP 预置任何地方,包括函数内

另外还有函数里的 static(值会在多次调用间保留),以及 include 会继承「当前所在」的作用域。下面逐个看。

全局变量

写在函数外面的变量属于当前脚本的全局作用域:

php
<?php

$app = "PHP Guide";
$version = 8;

echo $app, "\n"; // 可以

同一个文件里,函数外的代码共享这一层。多个 PHP 文件互相 include 时,文件顶层的变量也会并进同一层全局作用域(见下文 include 与作用域)。

函数默认看不到全局变量。下面这样在 PHP 8 会 Warning(未定义变量),不要依赖:

php
<?php

$app = "PHP Guide";

function title(): void
{
    echo $app, "\n"; // Warning: Undefined variable $app
}

title();

局部变量

函数里赋值的变量、以及参数,都是局部的。调用结束就销毁,下次调用重新来:

php
<?php

function greet(string $name): void
{
    $prefix = "你好";
    echo "{$prefix},{$name}\n";
}

greet("小明");
// echo $prefix; // 函数外没有 $prefix
// echo $name;

函数内外可以同名,那是两个变量:

php
<?php

$count = 10;

function bump(): void
{
    $count = 1;
    $count++;
    echo "函数内 ", $count, "\n"; // 2
}

bump();
echo "函数外 ", $count, "\n"; // 10

没有块级作用域

ifforwhile 的花括号不会开一个新作用域。这和 JavaScript(let)不同,也是 PHP 里常被忽略的一点。

php
<?php

if (true) {
    $found = "yes";
}
echo $found, "\n"; // yes,if 外面也能用

for ($i = 0; $i < 3; $i++) {
    $last = $i;
}
echo $i, "\n";    // 3
echo $last, "\n"; // 2

循环 里提过:for$i 在循环结束后仍然存在。

因此:

  • 不要指望「写在 if 里就不会漏到外面」
  • 循环、分支里用过的名字,后面不要不小心接着用
  • foreach ($arr as $item) 结束后 $item 还在,还指向最后那个元素
php
<?php

$langs = ["PHP", "HTML"];
foreach ($langs as $item) {
    echo $item, "\n";
}
echo $item, "\n"; // HTML

需要隔离,就包进函数,或在用完后 unset($item)

用参数和返回值传递

跨函数传递数据,首选传进去、返回来

php
<?php

function addTax(float $price, float $rate): float
{
    return $price * (1 + $rate);
}

$price = 100;
$paid = addTax($price, 0.1);
echo $paid, "\n"; // 110
echo $price, "\n"; // 100,原值没被改

默认按值传入。函数里要改调用方的变量,才用引用参数(少用):

php
<?php

function appendBang(string &$text): void
{
    $text .= "!";
}

$msg = "Hello";
appendBang($msg);
echo $msg, "\n"; // Hello!

能 return 新值就 return,比 & 更好追踪。数组、对象作为参数时另有一套「对象句柄」规则,进阶讲类时再展开;现在先把标量按值传递记熟。

global$GLOBALS

函数里要用某个全局变量,可以声明 global,或通过 $GLOBALS 数组访问。两者都在 超全局变量 里出现过。

php
<?php

$site = "PHP Guide";

function withGlobal(): void
{
    global $site;
    echo $site, "\n";
    $site = "被改了";
}

function withGlobals(): void
{
    echo $GLOBALS["site"], "\n";
}

withGlobal();
withGlobals();
echo $site, "\n";

global $site; 相当于把局部名字 $site 做成全局那个变量的引用。所以函数里赋值会改到外面。

不推荐把 global 当默认手段:

  • 函数依赖「外面碰巧有个同名变量」,难以测试
  • 同名一多,改 A 函数可能弄坏 B
  • 阅读时看不到数据从哪来

PHP 8.1 起不能再把 $GLOBALS 整表替换$GLOBALS = [] 这类写法会失效)。单个键 $GLOBALS["site"] 仍可用。业务代码里两者都少用。

静态变量 static

局部变量调用结束就丢。加上 static,值会留到下一次调用同一个函数

php
<?php

function tick(): int
{
    static $n = 0;
    $n++;
    return $n;
}

echo tick(), "\n"; // 1
echo tick(), "\n"; // 2
echo tick(), "\n"; // 3

$n = 0 只在第一次进入时执行。适合计数、缓存「这个函数自己用的状态」。它仍然是局部的,别的函数看不到 $n

不要用 static 代替明确的参数,也不要拿它保存请求级别的用户数据(那是 Session 的事)。一个请求里调用多次、只想记住自己的进度时,static 才合适。

超全局始终可见

$_GET$_POST$_SERVER$_SESSION 等在函数里直接用,不必 global

php
<?php

function method(): string
{
    return $_SERVER["REQUEST_METHOD"] ?? "CLI";
}

echo method(), "\n";

可见不等于应该到处摸。入口处取出、校验,再当普通参数往下传,函数会更好测。详见 超全局变量

include 与作用域

include / require 引入的文件,继承当前这段代码所在的作用域

在文件顶层引入,被引入文件里的变量是全局的:

php
<?php
// config.php
$dsn = "mysql:host=127.0.0.1";
php
<?php
// app.php
include __DIR__ . "/config.php";
echo $dsn, "\n"; // 看得到

在函数里引入,被引入文件里的变量变成这个函数的局部变量:

php
<?php

function loadConfig(): void
{
    include __DIR__ . "/config.php";
    echo $dsn, "\n"; // 函数内看得到
}

loadConfig();
// echo $dsn; // 函数外没有

这很容易让人懵:同一个 config.php,引入位置不同,变量落在不同作用域。配置文件请在顶层引入,或让文件 return 数组再接住:

php
<?php
// config.php
return [
    "dsn" => "mysql:host=127.0.0.1",
];
php
<?php

$config = require __DIR__ . "/config.php";
echo $config["dsn"], "\n";

return 一份数据,不污染全局,也不依赖「谁引入我」。

闭包里的 use(了解即可)

匿名函数不会自动捕获外面的变量,要显式 use

php
<?php

$prefix = "你好";

$greet = function (string $name) use ($prefix): void {
    echo "{$prefix},{$name}\n";
};

$greet("小明");

use 默认也是按值拷贝。use (&$prefix) 才是引用。现在能看懂即可,函数和闭包专篇再展开。

常量和作用域

const / define 定义的常量没有 $,在函数内外都能直接用,不存在「局部常量」这回事:

php
<?php

const APP = "PHP Guide";

function show(): void
{
    echo APP, "\n";
}

show();

类里面的常量是另一套(self::NAME),讲类时再覆盖。和变量相比:常量全局可见、不能改,适合不会变的配置名。

对照小结

php
<?php

$global = "全局";

function demo(string $arg): void
{
    $local = "局部";
    static $times = 0;
    $times++;

    // $global;          // 默认不可见
    global $global;       // 能见,但优先别用
    echo $GLOBALS["global"], "\n";
    echo $_SERVER["PHP_SELF"] ?? "CLI", "\n"; // 超全局可以直接用
    echo $arg, " ", $local, " ", $times, "\n";
}

demo("参数");
写法建议
参数 + return默认
超全局只在入口读,校验后下传
static函数私有的计数 / 缓存
global / $GLOBALS能不用就不用
指望 if / for 隔离变量不行,PHP 没有块级作用域

书写建议

  • 函数用到的数据从参数来,结果从返回值走
  • 不要为了省事把一堆状态塞进全局变量
  • foreach$valuefor$i 用完如果后面还要写代码,留意它们还在
  • 配置文件 return 数组,不要靠「引入后多出几个全局变量」
  • 给局部变量起能看懂的名字;不要内外靠同名「碰巧对着」

练习

  1. 写一个函数,接收 $a$b,返回它们的和;在函数外 echo 返回值,并确认函数外没有 $a 的拷贝被改掉。
  2. if (true) { $x = 1; } 后面 echo $x,解释为什么能输出。再把赋值放进函数里,观察函数外能否访问。
  3. static 写一个 nextId(),连续调用三次,输出 123
  4. 准备 data.php,里面 return ["lang" => "PHP"];。在主脚本里 $data = require ... 取出并输出。再对比:若改成在 data.php 里写 $lang = "PHP"; 然后在函数内部 include,函数外有没有 $lang

上一篇:超全局变量
下一篇:内置函数

基于 VitePress 构建的 PHP 基础学习指南