news 2026/8/30 21:21:24

PHP工程师二星级自测:从字符串截取到安全防护的全面避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP工程师二星级自测:从字符串截取到安全防护的全面避坑指南

有次跟同行聊招人,他说了句让我印象很深的话:来面试的都说自己会PHP,但一上手综合题就露馅。不是不会写,是写得太想当然。substr切中文切出乱码、=====分不清、PDO预处理以为加个占位符就万事大吉……这些恰恰是“二星级”该考的东西。

这份《PHP工程师综合练习卷(二星级)》就是这么来的。它的定位很明确:不考冷门函数,不考背概念,专挑日常开发中天天碰、但大多数人没有细想过的知识点。一星级考“知不知道”,二星级考“熟不熟”,三星级才考“设计得好不好”。适合谁做?写过一两年业务代码、感觉自己什么都会、但一被追问就含糊的PHP工程师;也适合准备面试、想系统查漏补缺的人。

我自己的习惯是先别翻文档,拿编辑器把答案敲一遍,错了再反推原因。这套卷子不是让你刷完就扔的,而是每道题背后都藏着一次线上事故或一次代码评审的教训。

1. 先说清楚:二星级到底在考什么

1.1 练习卷的定位:从“会不会”到“熟不熟”

很多刚入行的朋友有个误区,觉得PHP嘛,语法简单,写几个页面就是会了。但真到项目里,麻烦的从来不是语法,而是那些“看起来能用、实际上经不起追问”的地方。

二星级练习卷出的题,全部是这种类型:日常在用,但你可能没搞懂内部原理。比如字符串截取,谁不会用substr?但遇上中文、Emoji、多字节编码,它就会给你颜色看。再比如错误处理,try...catch谁都会写,但E_WARNINGE_ERROR的差异、日志该怎么配、线上为什么不能开display_errors,很多人答不上来。

我判断一个工程师处在什么水平,不看他会多少框架、写过多少接口,就看他对这些基础知识的“边界感”。知道一个函数擅长什么,更知道它在什么情况下会出问题,这才叫熟。

1.2 出题范围与自测方法

整套练习卷覆盖六个方向:基础语法与字符串处理、面向对象设计、数据库访问与封装、错误处理与安全、框架与工程化、部署与调试。每个方向都是从真实项目里抽出来的高频问题,再套上“题目 + 解析 + 避坑经验”的格式。

自测的时候别急着看答案。我建议你准备一个空目录,起一个临时PHP文件,把题目当小需求写一遍。写完再对照解析,错了就反推:是我不知道这个语法,还是我知道但理解错了?这两种错法对应的补课方式完全不同。

这套卷子不是用来背的,是用来暴露盲区的。你错得越多,说明这次自测越有价值。

2. 字符串、数组和运算符:最容易答错的基础题

2.1 substr 的这一排坑,你踩过几个

先看题目:

echo substr('HelloWorld', 0, 5); // 输出? echo substr('HelloWorld', -5); // 输出? echo substr('你好,世界', 0, 3); // 输出?

前两题没什么悬念:HelloWorld。第三题开始分人了——在没开启mbstring的情况下,substr('你好,世界', 0, 3)不是输出前三个汉字,而是输出一个乱码字符加上半个汉字。

原因很简单:substr是按字节切的,一个UTF-8汉字占3个字节,切3个字节刚好把一个汉字劈成两半。这种问题最常见的出现场景是:截取商品标题、生成文章摘要、导出Excel时的单元格内容。线上环境只要标题里混入中文和英文,截取位置就会变得不可控。

凡是涉及多字节字符的截取,一律用mb_substr

echo mb_substr('你好,世界', 0, 3, 'UTF-8'); // 你好,世

第四个参数指定编码,别省略。你没法保证运行环境的默认编码设置符合预期,显式指定才是可靠做法。这套“显式指定”的思路在后边的PDO配置里还会用到。

2.2 == 和 ===、strpos 的经典陷阱

这道题可以说伴随了每个PHP工程师的成长:

$str = 'abcdef'; var_dump(strpos($str, 'a')); // 输出什么? var_dump(strpos($str, 'a') == false); // 输出什么? var_dump(strpos($str, 'a') === false); // 输出什么?

strpos($str, 'a')返回的是子串首次出现的位置,也就是0。于是第一个var_dump输出int(0),第二个var_dump输出true——因为0 == false在PHP里是成立的。这就是经典的“字符串在开头就判定找不到”的bug。

正确姿势是永远用!==

if (strpos($str, 'a') !== false) { // 存在 }

而且要注意,PHP 8对字符串和数字的比较做了修正。PHP 7时代,'abc' == 0会返回true,PHP 8之后只有数字开头的字符串才会做数值比较,'abc' == 0直接变成false。这意味着你从老项目升级到PHP 8,凡是依赖旧比较结果的地方都要重新过一遍。

2.3 数组函数:高频“工具人”盘点

PHP的数组函数有一百多个,二星级不要求全背,但有几个高频的必须熟练。

array_column是从二维数组里取一列的利器。做下拉选项、列表展示、按ID匹配数据时,一行代码就能搞定,不需要自己写循环:

$users = [ ['id' => 1, 'name' => '张三'], ['id' => 2, 'name' => '李四'], ]; $names = array_column($users, 'name', 'id'); // 输出 [1 => '张三', 2 => '李四']

第三个参数把某个字段当键,这在按ID快速查找时特别实用。

array_maparray_filter则让“遍历+处理+过滤”不用再写三遍foreach。比如批量格式化时间、过滤掉空值:

$data = ['2024-01-01', '', '2024-03-15', null]; $filtered = array_filter($data, fn($v) => !empty($v));

至于网上流传的“PHP语言经典程序100题”,我的看法是:不推荐逐题硬刷,但值得把涉及数组和字符串的那部分过一遍。它的价值不在背题,而在于让你看到同一个函数在不同边界条件下的行为差异——这恰恰是二星级考察的核心。

3. 面向对象:从“会写类”到“会设计类”

3.1 自定义方法的第一步:可见性与单一职责

很多新手的类是这样的:一个index()方法里写了两百行,从查数据库到拼HTML全干了。问他为什么不分方法,他说“也能跑啊”。

二星级要求你能答出为什么不行。核心原因有两个:一是一个方法里塞太多职责,任何一个环节出错,你都得通读整个方法才能定位;二是没法复用,比如另一个页面需要同样的查询逻辑,你只能复制粘贴,改一处忘另一处。

正确的做法是把“自定义方法”当公共接口来设计:

class UserService { private $db; public function __construct(PDO $db) { $this->db = $db; } public function findById(int $id): ?array { $stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]); $user = $stmt->fetch(PDO::FETCH_ASSOC); return $user ?: null; } public function create(array $data): int { $stmt = $this->db->prepare('INSERT INTO users (name, email) VALUES (?, ?)'); $stmt->execute([$data['name'], $data['email']]); return (int)$this->db->lastInsertId(); } }

可见性也不是随便写的。private用于内部辅助方法,protected留给子类扩展,public才是对外暴露的接口。如果你把所有方法都设成public,相当于把类的内部结构全亮给别人,后续改内部实现会因为外部依赖太多而寸步难行。

3.2 self、static、parent:晚期静态绑定题

这是面试高频题,也是二星级练习卷里最值得反复做的一道。

class A { public static function test() { return self::getName(); } protected static function getName() { return 'A'; } } class B extends A { protected static function getName() { return 'B'; } } echo A::test(); // 输出 A echo B::test(); // 输出什么?

答案是A。原因在于self在编译时就绑定了定义方法的那个类,也就是A。不管你是通过B::test()调用,还是通过A::test()调用,self::getName()永远指向A::getName()

如果你想要“调用时动态决定”,用static关键字,这叫晚期静态绑定:

class A { public static function test() { return static::getName(); } // ... } echo B::test(); // 输出 B

两种写法没有绝对对错,关键是明确需求:工厂方法、单例模式、ORM这类需要子类扩展的场景,几乎都是static;而普通工具类的内部调用,self更可控。

3.3 接口与对象:ArrayAccess 与数据形态转换

题目:如何让一个对象像数组一样通过$obj['key']访问属性?

答案是实现ArrayAccess接口。这个接口规定了四个方法:offsetExistsoffsetGetoffsetSetoffsetUnset。这种设计把“读取规则”封装在类内部,外部调用方不需要关心数据是存数组还是存数据库。

class User implements ArrayAccess { private $data = []; public function offsetExists($offset): bool { return isset($this->data[$offset]); } public function offsetGet($offset): mixed { return $this->data[$offset] ?? null; } public function offsetSet($offset, $value): void { $this->data[$offset] = $value; } public function offsetUnset($offset): void { unset($this->data[$offset]); } }

接口在PHP里的意义就是“契约”:你定义好规则,所有实现方都必须遵守。比如支付接口,不管对接微信还是支付宝,对外暴露的方法都叫pay()refund(),调用方不用关心底层实现。

数据形态转换也是二星级的常考点。对象转数组用get_object_vars($obj),数组转对象用(object)$arr。但我在实际项目里更推荐:内部模块之间传对象,对外API统一返回数组。原因很现实——数组在PHP里天生就是“可序列化、可JSON化、可数组函数处理”的万能格式,而对象还得考虑序列化时要不要带上私有属性。明确边界,能让代码好维护很多。

4. 数据库访问:PDO封装到你手上的时候

4.1 PDO 和 mysqli 怎么选

老生常谈,但很多人其实不清楚为什么团队规范里写“一律用PDO”。对比一下:

维度PDOmysqli
支持数据库MySQL、PostgreSQL、SQLite、SQL Server等仅MySQL
预处理统一API,写法一致面向过程和面向对象两套写法
异常处理可设置ERRMODE_EXCEPTION需要手动检查
命名参数支持:name只支持?占位符

如果你的项目未来可能换数据库——哪怕只是从MySQL换到MariaDB——PDO都能把迁移成本压到最低。而且PDO的异常模式是真的好用:

$pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]);

第四个参数里ATTR_EMULATE_PREPARES => false是重点,后面细说。

4.2 预处理为什么能防注入

很多人以为PDO预处理就是把SQL写漂亮点,这是误解。

预处理的原理分两步:第一步,把SQL模板发给数据库服务器,服务器完成解析、编译——这时候SQL结构已经定了,不可能再被改变;第二步,把参数值单独发给服务器,让服务器用这些值去填充模板。参数值再“狡猾”,也只是数据,不会成为SQL结构的一部分。

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?'); $stmt->execute([$_POST['email']]);

如果手动拼SQL:

$sql = "SELECT * FROM users WHERE email = '{$_POST['email']}'";

当输入' OR '1'='1时,拼接出来的SQL会变成WHERE email = '' OR '1'='1',整表数据全查出来了。这就是最基础的SQL注入。

但注意,PDO::ATTR_EMULATE_PREPARES默认是true,这时候PDO在本地模拟预处理,实际上是先把SQL语句和参数拼接在一起再发给MySQL,安全性就打了折扣。生产环境强烈建议设为false,让MySQL自己做真正的预处理。

4.3 从图书管理系统到题库保存:真实CRUD中的数据组织

练习卷里有一类经典题目:“用PHP写一个图书管理系统”。很多人一听就头疼,其实它考的就是CRUD的数据组织。

图书管理系统核心表一般就三张:图书表、分类表、借阅记录表。图书表存idtitlecategory_idauthorstock;借阅记录表存idbook_iduser_idborrow_atreturn_at。列表页要显示图书和分类名时,一条LEFT JOIN就能解决。

另一个类似的题目是“保存题库信息”。这里值得讨论的是:一道选择题的选项怎么存?

方案A是把选项存成JSON字段:

$question = [ 'title' => 'PHP中哪个函数用于数组排序?', 'options' => ['A: sort', 'B: explode', 'C: implode', 'D: substr'], 'answer' => 'A', ];

方案B是建关联表:question表和question_option表一对多。

我的判断标准很简单:如果选项内容固定、不会拿选项去做SQL筛选和统计,JSON字段最省事;如果后续要做“哪些题选A的多”“按选项标签筛选题目”这类分析,就必须拆关联表。二星级要求的是能说清楚“为什么这么选”,而不是照搬某个方案。

分页查询也是CRUD里的硬核技能。标准写法是先把查询条件组装成数组,再拼SQL:

$conditions = []; $params = []; if (!empty($_GET['keyword'])) { $conditions[] = 'title LIKE ?'; $params[] = '%' . $_GET['keyword'] . '%'; } $where = $conditions ? 'WHERE ' . implode(' AND ', $conditions) : ''; $countSql = "SELECT COUNT(*) FROM books $where"; // 然后执行 count 和 limit 查询

注意:条件数组和参数数组必须同步维护,顺序不能乱。我在实际项目里见过不少因为这里顺序错位导致数据对不上的事故。

5. 错误处理与安全:二星级和初级的真正分水岭

5.1 错误处理不是 try catch 一句的事

入门时写PHP,报错了就看页面上的红字。但到了二星级,你必须理解PHP的错误体系:E_ERROR是致命错误,脚本直接终止;E_WARNING是警告,脚本继续跑,但结果可能是错的;E_NOTICE是提示,很多是未定义变量这类问题;E_DEPRECATED是弃用提示,老代码升级时全是这玩意儿。

线上环境的第一条铁律是:关闭display_errors,开启log_errors

ini_set('display_errors', '0'); ini_set('log_errors', '1'); ini_set('error_log', '/var/log/php_errors.log');

把错误直接暴露给用户,等于把服务器路径、数据库结构、框架版本信息全送出去了。正确做法是让错误进日志,用户看到的只是一个友好提示页。

除了系统错误,业务逻辑的异常要用try...catch接住,但别什么都吞掉。我见过有人catch (Exception $e)之后什么都不干,问题出在哪全被藏起来了。至少应该error_log($e->getMessage()),或者接入集中日志系统。

ThinkPHP老项目里经常出现的“页面错误!请稍后再试~”就是典型的框架级错误页。这种提示对用户友好,但对排查问题很不友好——你得去runtime/log目录翻当天的日志,找到具体的异常堆栈,才能定位是SQL写错了还是某个类没加载。

5.2 伪协议与文件包含:LFI漏洞的入口

在CTF题里很常见,但真实项目里同样会踩。文件包含漏洞(LFI)的核心是:用户可以控制include的文件路径,导致服务器去读取或执行预期之外的文件。

PHP伪协议是这类攻击的放大器:

// 读取源码,输出base64编码后的内容 php://filter/read=convert.base64-encode/resource=index.php // 把POST内容当作代码执行 php://input // 直接用data协议构造代码 data://text/plain,<?php phpinfo();?>

举个例子。如果你的代码里写了:

$page = $_GET['page']; include $page . '.php';

攻击者传入php://filter/read=convert.base64-encode/resource=config,就能把数据库配置文件的内容读走;传入php://input,再用POST提交一段PHP代码,就能直接在服务器上执行命令。

防御方式:对include的文件名做白名单校验,绝对不要直接用用户输入拼接路径。这也是为什么框架里会提供路由机制,把“访问哪个控制器方法”限定在可控范围内。

5.3 反序列化漏洞:为什么 __wakeup 会成为突破口

PHP的serializeunserialize把对象变成字符串、再变回对象,本身没毛病。但危险在于unserialize反序列化时,会自动触发类里的魔术方法。

最常被利用的是__wakeup()__destruct()__wakeup在反序列化时自动调用,__destruct在对象销毁时自动调用。如果这些方法里有敏感操作——比如写文件、执行SQL、调用某个命令——攻击者构造出精心设计的序列化字符串,就能在服务器上触发这些操作。

CTF里的“[极客大挑战 2019]php”考的就是这个:通过反序列化触发某个类的__wakeup__destruct,实现文件读取或者命令执行。题目里还会配合过滤flag关键字,要求你绕过字符检测。

防御思路有三个层次:

  1. 不要反序列化不可信数据。如果只是传数据,用json_encode/json_decode就够了,别用serialize/unserialize
  2. 反序列化时限制允许的类,PHP 8提供了unserialize($data, ['allowed_classes' => false]),可以禁止实例化任何对象。
  3. 对类里的魔术方法保持警惕,不要在里面写危险操作。

顺带提一句“PHP序列化中文”这个问题:serialize会把中文转成\u形式,这属于正常输出,不影响反序列化结果。真正容易出问题的是序列化字符串里的类名在被重命名、移动命名空间后反序列化失败——这属于版本迭代时要考虑的兼容性问题,不是bug。

6. 框架、调试与环境:本地写得好,线上跑不了等于零

6.1 ThinkPHP3.2.3 为什么到现在还有人讨论

先说结论:ThinkPHP 3.2.3是2015年前后的框架版本,新的项目我强烈不建议用它。但现实是,存量项目比新项目多得多,很多公司到现在还在维护着基于TP3.2.3的老系统。

这类老代码的典型问题是:I()函数全局获取输入、SQL语句直接拼接、页面报错直接暴露框架版本和路径。你在工单里看到“页面错误!请稍后再试~”,十有八九就是这个版本。

从那类项目里能学到什么?不是学它的写法,而是学它的教训:框架版本要跟着官方安全公告走,不要因为“能跑”就一直不升级。

新一代框架,比如ThinkPHP 6、Laravel,都已经把路由、中间件、依赖注入做成了标配,代码结构清晰很多。如果你还要用PHP做后台管理系统,搭配Layui这类前端框架,后端用ThinkPHP或Laravel提供API,体验比老一套好太多。

6.2 用 PHPStorm 跑通项目的正确姿势

很多初学者在PHPStorm里打开项目,直接点运行,结果发现页面出不来,就开始怀疑IDE坏了。其实PHPStorm只是一个编辑器,它要“运行”PHP项目,必须知道三件事:PHP解释器在哪、项目根目录是哪、用内置服务器还是外部服务器。

先配置CLI解释器:Settings -> PHP -> CLI Interpreter,选择本机的php.exe/usr/bin/php。如果你的环境是Docker,也可以直接在Docker标签页里选择容器中的PHP,PHPStorm会自动从容器里拉取配置,这一步就解决了“本地PHP版本和线上不一致”的问题。

要快速调试接口,不用非得配置完整的Nginx,用PHPStorm自带的内置服务器就够了:Run -> Run 'index.php',或者直接在命令行:

php -S localhost:8080 -t public

-t指定Web根目录。这种方式适合写小接口、测逻辑。要测Xdebug断点,就需要配置Xdebug扩展和PHPStorm的Server映射,这一步稍微繁琐,但一旦配好,调试时的效率提升是质的飞跃。

顺带提一句NetBeans。现在用的人不多了,但它在很早期就提供了很好的PHP支持,而且是免费开源的。如果你实在不想为IDE花钱,NetBeans也是一个不差的备选。

6.3 nginx + php-fpm 与 Docker 打包:环境问题的根治方案

“在我电脑上是好的”这句话,本质是环境不一致。二星级工程师应该对环境部署有清晰的认知。

nginx本身不认识PHP,它把请求通过fastcgi_pass转发给php-fpm处理。核心配置长这样:

server { listen 80; server_name example.com; root /var/www/html/public; index index.php index.html; location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }

fastcgi_pass可以指向127.0.0.1:9000(TCP方式),也可以指向unix:/run/php/php8.2-fpm.sock(Socket方式)。Socket方式少一层网络开销,同一台机器上性能更好,但权限配置麻烦一些。

最常见的坑是权限问题:php-fpm进程默认以www-data用户运行,如果代码目录是root所有,就会报403或无法写入日志。解决方法是把项目目录所有者改成www-data,或者调整php-fpm的user配置。

与其每次在新机器上折腾一遍环境,不如直接用Docker把环境固化下来。一个最简的PHP项目打包:

FROM php:8.2-fpm RUN docker-php-ext-install pdo_mysql COPY . /var/www/html WORKDIR /var/www/html

然后配合docker-compose.yml把nginx、php、mysql、redis四个容器编排起来:

services: php: build: . volumes: - .:/var/www/html nginx: image: nginx:alpine ports: - "8080:80" volumes: - .:/var/www/html - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro

这样一套配置提交到仓库,任何人拉下来跑一条docker-compose up -d,环境和线上就完全一致了。现在很多公司内部用1Panel这类开源面板来管理PHP、MySQL、Redis环境,本质上是把容器和面板管理结合起来,对中小团队来说是个省心选项。

7. 扩展视野:队列、跨域与支付回调

7.1 队列:从 Redis list 到消息队列的演进

凡是涉及“异步处理”的需求,最终都会走到队列。PHP业务里最常见的场景是:用户下单后要发通知、写日志、更新统计,如果全在请求里同步执行,接口响应会越来越慢。

最简单的队列用Redis就能做:生产者往列表左边塞数据,消费者从右边阻塞取数据。

// 生产者 $redis->lpush('task:email', json_encode(['to' => 'user@example.com', 'subject' => '订单通知'])); // 消费者(命令行跑long-running进程) while (true) { $task = $redis->brpop('task:email', 0); // 处理邮件发送 }

brpop的第二个参数是超时时间,0表示一直阻塞等待。这套方案简单直接,适合任务量不大、不需要消息确认和重试的场景。

但如果你的系统需要“消息被处理失败后重试”“多个消费者分担负载”“延迟执行”,就要上RabbitMQ这类专业的消息队列了。它们的核心优势是完整的消息确认机制:消费者处理完一条消息后必须显式ack,否则消息会被重新投递。这套机制保证了消息不丢。

二星级不需要你精通RabbitMQ的每个细节,但至少要知道:队列解决的是“请求与处理解耦”的问题,选型时先看消息量和可靠性要求,再看运维成本。

7.2 JSONP 与 CORS:跨域问题的两条路线

前端页面在a.com,后端接口在b.com,浏览器默认会拦截跨域请求。二星级必须掌握两条主流解法。

JSONP是历史最悠久的方案,核心原理是<script>标签不受同源策略限制。后端返回的不是JSON,而是一段JavaScript调用:

$callback = $_GET['callback']; $data = json_encode(['status' => 'ok']); echo $callback . '(' . $data . ');';

前端用<script src="http://b.com/api?callback=handleResult">加载,handleResult函数就能拿到数据。隐患是:JSONP只能支持GET请求,而且callback参数如果不做白名单校验,可能被注入恶意代码。

更现代、更推荐的方案是CORS。直接在后端响应头里声明允许跨域:

header('Access-Control-Allow-Origin: https://a.com'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE'); header('Access-Control-Allow-Headers: Content-Type, Authorization');

需要注意:如果接口需要携带Cookie,Access-Control-Allow-Origin不能是*,必须明确指定来源域名,否则浏览器会拒绝带上凭证。这是配置CORS时最容易踩的坑。

7.3 回调接口的幂等性:微信支付通知里的门道

支付回调是每个写PHP接口的人迟早要碰的东西。微信支付成功后会异步通知你的服务器,但通知可能重复发送——可能是网络超时重试,可能是微信服务端策略。所以回调接口的第一原则是幂等:同样的通知发多少次,结果都一致。

一个标准的处理流程是:

  1. 接收通知数据,验签。微信支付会用API密钥对参数做MD5或HMAC-SHA256签名,你要用同样的规则重新计算一遍,比对一致才继续处理。
  2. 根据商户订单号查数据库,判断这个订单是否已经处理过。如果已处理,直接返回成功,不再重复发货。
  3. 更新订单状态时,用带条件的UPDATE语句,比如UPDATE orders SET status = 'paid' WHERE order_no = ? AND status = 'unpaid',影响行数为0说明已经被处理过了。
  4. 按微信要求的格式返回响应:<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>。不管业务处理成不成功,签名验证失败也要返回明确的错误码。

我见过不少新人在这个接口上栽跟头:要么忘了验签,结果任何人都能伪造通知;要么没做幂等,通知重复两次货就发了两遍。这两条是支付回调的底线。

还有一类需求是“接收邮箱里的支付消息”:有些老系统没有接入支付回调,而是靠定时去邮箱或支付平台拉取账单。实现思路是用IMAP协议连接邮箱,PHP的imap_*函数可以读取收件箱、解析邮件内容,再从中提取支付信息。这种方案属于“能用但很脆弱”,能用回调还是优先回调。

最后再分享一个小技巧

每次做完这张卷子,我都会把错题整理成一份自己的“踩坑清单”,过两个月再回头做一遍。你会发现,第二次错的题目往往和第一次不一样——第一次是不知道,第二次可能是知道但没记住。这说明基础不是看一遍就牢固的,是需要反复“锤打”的。

如果想检验自己是不是真搞懂了,有个笨但有效的办法:把每道错题讲给旁边的人听,讲到对方点头为止。讲解的过程会逼你把“好像知道”变成“确切知道”。PHP的上限不低,但下限也真的不高——把基础打瓷实,是性价比最高的事情。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 21:20:23

复盘人人网2015研发笔试卷:算法、操作系统与数据库考点

前几天整理旧资料&#xff0c;翻出一份PDF&#xff0c;文件名写着《人人网2015研发笔试卷E》。说实话看到这个名字的时候愣了几秒——那会儿人人网还是很多应届生眼里的热门去处&#xff0c;从校内网改名之后&#xff0c;社交产品做得风生水起&#xff0c;研发笔试也出了好几套…

作者头像 李华
网站建设 2026/8/30 21:20:14

QGIS+QT+VS二次开发实战:从环境搭建到GIS应用构建

简介&#xff1a;本资源是一份面向GIS开发初学者与C桌面应用开发者的技术实践项目&#xff0c;聚焦于基于QGIS SDK与Qt框架在Visual Studio环境下开展轻量级GIS桌面工具的二次开发&#xff0c;重点解决矢量数据&#xff08;Shapefile/GeoJSON/KML等&#xff09;的加载、解析与基…

作者头像 李华
网站建设 2026/8/30 21:19:36

前端面试核心考点全拆解:原理、项目与手写题实战

1. 先说点实际的&#xff1a;金三银四&#xff0c;前端面试到底在考什么又到了金三银四&#xff0c;身边不少前端朋友都在准备换工作&#xff0c;私信问我最多的就是“现在前端面试到底考什么”“八股文还背不背”“项目怎么讲才加分”。说实话&#xff0c;我每年这时候都会帮身…

作者头像 李华
网站建设 2026/8/30 21:18:21

claude-skills Retrospectives实战:用complete-epic做好回顾

claude-skills Retrospectives实战&#xff1a;用complete-epic做好回顾 【免费下载链接】claude-skills 67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer. 项目地址: https://gitcode.com/GitHub_Trending/claud…

作者头像 李华
网站建设 2026/8/30 21:18:16

前端秋招面试核心考点与实战复盘:从简历到Offer的完整指南

9月底&#xff0c;秋招基本尘埃落定。投了八十多份简历&#xff0c;笔试做了二十来场&#xff0c;面试聊了三十多轮&#xff0c;最后手里攥着几个还算满意的前端offer&#xff0c;才终于有底气坐下来把这段经历完整复盘一遍。这篇文章不打算写成流水账&#xff0c;也不准备堆一…

作者头像 李华
网站建设 2026/8/30 21:18:14

前端秋招面试全攻略:技术盘点、项目深挖与实战复盘

1. 秋招前的自我评估&#xff1a;别急着海投&#xff0c;先把“技术栈”理清楚前端这个方向&#xff0c;看起来入门门槛低&#xff0c;但秋招的竞争烈度一点不比后端低。我见过不少同学 7 月份才开始刷题&#xff0c;简历上堆了一堆“熟悉 Vue/React”“了解 webpack”&#xf…

作者头像 李华