主题
变量的作用域
作用域(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没有块级作用域
if、for、while 的花括号不会开一个新作用域。这和 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的$value、for的$i用完如果后面还要写代码,留意它们还在- 配置文件
return数组,不要靠「引入后多出几个全局变量」 - 给局部变量起能看懂的名字;不要内外靠同名「碰巧对着」
练习
- 写一个函数,接收
$a、$b,返回它们的和;在函数外echo返回值,并确认函数外没有$a的拷贝被改掉。 - 在
if (true) { $x = 1; }后面echo $x,解释为什么能输出。再把赋值放进函数里,观察函数外能否访问。 - 用
static写一个nextId(),连续调用三次,输出1、2、3。 - 准备
data.php,里面return ["lang" => "PHP"];。在主脚本里$data = require ...取出并输出。再对比:若改成在data.php里写$lang = "PHP";然后在函数内部include,函数外有没有$lang。