0

0

PHP匿名函数及闭包

php中文网

php中文网

发布时间:2016-08-08 09:26:10

|

1427人浏览过

|

来源于php中文网

原创

[iefreer] 转载一篇对php闭包语法讲解比较深入到位的文章,后续还会转一篇这些新语法如何巧妙应用的文章。

匿名函数在编程语言中出现的比较早,最早出现在Lisp语言中,随后很多的编程语言都开始有这个功能了,

目前使用比较广泛的Javascript以及C#,PHP直到5.3才开始真正支持匿名函数,C++的新标准C++0x也开始支持了。

匿名函数是一类不需要指定标示符,而又可以被调用的函数或子例程,匿名函数可以方便的作为参数传递给其他函数,最常见应用是作为回调函数。

闭包(Closure)

立即学习PHP免费学习笔记(深入)”;

说到匿名函数,就不得不提到闭包了,闭包是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数,这个被应用的自由变量将和这个函数一同存在,即使离开了创建它的环境也一样,所以闭包也可认为是有函数和与其相关引用组合而成的实体。在一些语言中,在函数内定义另一个函数的时候,如果内部函数引用到外部函数的变量,则可能产生闭包。在运行外部函数时,一个闭包就形成了。

这个词和匿名函数很容易被混用,其实这是两个不同的概念,这可能是因为很多语言实现匿名函数的时候允许形成闭包。

使用create_function()创建"匿名"函数

前面提到PHP5.3中才才开始正式支持匿名函数,说到这里可能会有细心读者有意见了,因为有个函数是可以生成匿名函数的: create_function函数,在手册里可以查到这个函数在PHP4.1和PHP5中就有了,这个函数通常也能作为匿名回调函数使用,例如如下:

[php] view plaincopy

  1.    
  2. $array = array(1, 2, 3, 4);  
  3. array_walk($array, create_function('$value''echo $value'));  

这段代码只是将数组中的值依次输出,当然也能做更多的事情。 那为什么这不算真正的匿名函数呢,我们先看看这个函数的返回值,这个函数返回一个字符串,通常我们可以像下面这样调用一个函数:

[php] view plaincopy

  1.    
  2. function a() {  
  3.     echo 'function a';  
  4. }  
  5.    
  6. $a = 'a';  
  7. $a();  

我们在实现回调函数的时候也可以采用这样的方式,例如:

[php] view plaincopy

  1.    
  2. function do_something($callback) {  
  3.     // doing  
  4.     # ...  
  5.    
  6.     // done  
  7.     $callback();  
  8. }  

这样就能实现在函数do_something()执行完成之后调用$callback指定的函数。回到create_function函数的返回值:函数返回一个唯一的字符串函数名,出现错误的话则返回FALSE。这么说这个函数也只是动态的创建了一个函数,而这个函数是有函数名的,也就是说,其实这并不是匿名的。只是创建了一个全局唯一的函数而已。

[php] view plaincopy

  1. $func = create_function('''echo "Function created dynamic";');  
  2. echo $func// lambda_1  
  3.    
  4. $func();    // Function created dynamic  
  5.    
  6. $my_func = 'lambda_1';  
  7. $my_func(); // 不存在这个函数  
  8. lambda_1(); // 不存在这个函数  

上面这段代码的前面很好理解,create_function就是这么用的,后面通过函数名来调用却失败了,这就有些不好理解了,php是怎么保证这个函数是全局唯一的? lambda_1看起来也是一个很普通的函数名,如果我们先定义一个叫做lambda_1的函数呢?这里函数的返回字符串会是lambda_2,它在创建函数的时候会检查是否这个函数是否存在知道找到合适的函数名,但如果我们在create_function之后定义一个叫做lambda_1的函数会怎么样呢? 这样就出现函数重复定义的问题了,这样的实现恐怕不是最好的方法,实际上如果你真的定义了名为lambda_1的函数也是不会出现我所说的问题的。这究竟是怎么回事呢?上面代码的倒数2两行也说明了这个问题,实际上并没有定义名为lambda_1的函数。

也就是说我们的lambda_1和create_function返回的lambda_1并不是一样的!? 怎么会这样呢? 那只能说明我们没有看到实质,只看到了表面,表面是我们在echo的时候输出了lambda_1,而我们的lambda_1是我们自己敲入的. 我们还是使用debug_zval_dump函数来看看吧。

[php] view plaincopy

  1. $func = create_function('''echo "Hello";');  
  2.    
  3. $my_func_name = 'lambda_1';  
  4. debug_zval_dump($func);         // string(9) "lambda_1" refcount(2)  
  5. debug_zval_dump($my_func_name); // string(8) "lambda_1" refcount(2)  

看出来了吧,他们的长度居然不一样,长度不一样也即是说不是同一个函数,所以我们调用的函数当然是不存在的,我们还是直接看看create_function函数到底都做了些什么吧。该实现见: $PHP_SRC/Zend/zend_builtin_functions.c

[php] view plaincopy

  1. #define LAMBDA_TEMP_FUNCNAME    "__lambda_func"  
  2.    
  3. ZEND_FUNCTION(create_function)  
  4. {  
  5.     // ... 省去无关代码  
  6.     function_name = (char *) emalloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG);  
  7.     function_name[0] = '\0';  //   
  8.     do {  
  9.         function_name_length = 1 + sprintf(function_name + 1, "lambda_%d", ++EG(lambda_count));  
  10.     } while (zend_hash_add(EG(function_table), function_name, function_name_length+1, &new_function, sizeof(zend_function), NULL)==FAILURE);  
  11.     zend_hash_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME));  
  12.     RETURN_STRINGL(function_name, function_name_length, 0);  
  13. }  

该函数在定义了一个函数之后,给函数起了个名字,它将函数名的第一个字符变为了'\0'也就是空字符,然后在函数表中查找是否已经定义了这个函数,如果已经有了则生成新的函数名, 第一个字符为空字符的定义方式比较特殊, 因为在用户代码中无法定义出这样的函数, 也就不存在命名冲突的问题了,这也算是种取巧(tricky)的做法,在了解到这个特殊的函数之后,我们其实还是可以调用到这个函数的, 只要我们在函数名前加一个空字符就可以了, chr()函数可以帮我们生成这样的字符串, 例如前面创建的函数可以通过如下的方式访问到:

[php] view plaincopy

  1.    
  2. $my_func = chr(0) . "lambda_1";  
  3. $my_func(); // Hello  

这种创建"匿名函数"的方式有一些缺点:

  1. 函数的定义是通过字符串动态eval的, 这就无法进行基本的语法检查;
  2. 这类函数和普通函数没有本质区别, 无法实现闭包的效果.

真正的匿名函数

在PHP5.3引入的众多功能中, 除了匿名函数还有一个特性值得讲讲: 新引入的__invoke 魔幻方法。

__invoke魔幻方法

这个魔幻方法被调用的时机是: 当一个对象当做函数调用的时候, 如果对象定义了__invoke魔幻方法则这个函数会被调用,这和C++中的操作符重载有些类似, 例如可以像下面这样使用:

[php] view plaincopy

  1. class Callme {  
  2.     public function __invoke($phone_num) {  
  3.         echo "Hello: $phone_num";  
  4.     }  
  5. }  
  6.    
  7. $call = new Callme();  
  8. $call(13810688888); // "Hello: 13810688888  

匿名函数的实现

前面介绍了将对象作为函数调用的方法, 聪明的你可能想到在PHP实现匿名函数的方法了,PHP中的匿名函数就的确是通过这种方式实现的。我们先来验证一下:

大气金融投资金色企业网站源码1.0
大气金融投资金色企业网站源码1.0

该软件是以php+MySQL进行开发的金融企业类网站源码,运行网站提示填写mysql数据库信息,网站将自行安装。后台地址:http://您的网址/admin/ 默认用户名和密码admin成都艾威尔网络科技有限公司(IVEARS)成立于2012年,主要致力于网站建设,网页设计,网站制作开发及网络营销领域。 服务项目包含了网页设计、网站程序开发、域名注册、国内外空间申请、CMS系统开发、微网站制

下载

[php] view plaincopy

  1. $func = function() {  
  2.     echo "Hello, anonymous function";  
  3. }  
  4.    
  5. echo gettype($func);    // object  
  6. echo get_class($func);  // Closure  

原来匿名函数也只是一个普通的类而已。熟悉Javascript的同学对匿名函数的使用方法很熟悉了,PHP也使用和Javascript类似的语法来定义, 匿名函数可以赋值给一个变量, 因为匿名函数其实是一个类实例, 所以能复制也是很容易理解的, 在Javascript中可以将一个匿名函数赋值给一个对象的属性, 例如:

[php] view plaincopy

  1. var a = {};  
  2. a.call = function() {alert("called");}  
  3. a.call(); // alert called  

这在Javascript中很常见, 但在PHP中这样并不可以, 给对象的属性复制是不能被调用的, 这样使用将会导致类寻找类中定义的方法,在PHP中属性名和定义的方法名是可以重复的, 这是由PHP的类模型所决定的, 当然PHP在这方面是可以改进的, 后续的版本中可能会允许这样的调用,这样的话就更容易灵活的实现一些功能了。目前想要实现这样的效果也是有方法的: 使用另外一个魔幻方法__call(),至于怎么实现就留给各位读者当做习题吧。

闭包的使用

PHP使用闭包(Closure)来实现匿名函数, 匿名函数最强大的功能也就在匿名函数所提供的一些动态特性以及闭包效果,匿名函数在定义的时候如果需要使用作用域外的变量需要使用如下的语法来实现:

[php] view plaincopy

  1. $name = 'TIPI Team';  
  2. $func = function() use($name) {  
  3.     echo "Hello, $name";  
  4. }  
  5.    
  6. $func(); // Hello TIPI Team  

这个use语句看起来挺别扭的, 尤其是和Javascript比起来, 不过这也应该是PHP-Core综合考虑才使用的语法, 因为和Javascript的作用域不同, PHP在函数内定义的变量默认就是局部变量, 而在Javascript中则相反,除了显式定义的才是局部变量, PHP在变异的时候则无法确定变量是局部变量还是上层作用域内的变量, 当然也可能有办法在编译时确定,不过这样对于语言的效率和复杂性就有很大的影响。

这个语法比较直接,如果需要访问上层作用域内的变量则需要使用use语句来申明, 这样也简单易读,说到这里, 其实可以使用use来实现类似global语句的效果。

匿名函数在每次执行的时候都能访问到上层作用域内的变量, 这些变量在匿名函数被销毁之前始终保存着自己的状态,例如如下的例子:

[php] view plaincopy

  1. function getCounter() {  
  2.     $i = 0;  
  3.     return function() use($i) { // 这里如果使用引用传入变量: use(&$i)  
  4.         echo ++$i;  
  5.     };  
  6. }  
  7.    
  8. $counter = getCounter();  
  9. $counter(); // 1  
  10. $counter(); // 1  

和Javascript中不同,这里两次函数调用并没有使$i变量自增,默认PHP是通过拷贝的方式传入上层变量进入匿名函数,如果需要改变上层变量的值则需要通过引用的方式传递。所以上面得代码没有输出1, 2而是1,1

闭包的实现

前面提到匿名函数是通过闭包来实现的, 现在我们开始看看闭包(类)是怎么实现的。匿名函数和普通函数除了是否有变量名以外并没有区别,闭包的实现代码在$PHP_SRC/Zend/zend_closure.c。匿名函数"对象化"的问题已经通过Closure实现, 而对于匿名是怎么样访问到创建该匿名函数时的变量的呢?

例如如下这段代码:

[php] view plaincopy

  1. $i=100;  
  2. $counter = function() use($i) {  
  3.     debug_zval_dump($i);  
  4. };    
  5.    
  6. $counter();  

通过VLD来查看这段编码编译什么样的opcode了

[php] view plaincopy

  1. $ php -dvld.active=1 closure.php  
  2.    
  3. vars:  !0 = $i, !1 = $counter  
  4. # *  op                           fetch          ext  return  operands  
  5. ------------------------------------------------------------------------  
  6. 0  >   ASSIGN                                                   !0, 100  
  7. 1      ZEND_DECLARE_LAMBDA_FUNCTION                             '%00%7Bclosure  
  8. 2      ASSIGN                                                   !1, ~1  
  9. 3      INIT_FCALL_BY_NAME                                       !1  
  10. 4      DO_FCALL_BY_NAME                              0            
  11. 5    > RETURN                                                   1  
  12.    
  13. function name:  {closure}  
  14. number of ops:  5  
  15. compiled vars:  !0 = $i  
  16. line     # *  op                           fetch          ext  return  operands  
  17. --------------------------------------------------------------------------------  
  18.   3     0  >   FETCH_R                      static              $0      'i'  
  19.         1      ASSIGN                                                   !0, $0  
  20.   4     2      SEND_VAR                                                 !0  
  21.         3      DO_FCALL                                      1          'debug_zval_dump'  
  22.   5     4    > RETURN                                                   null  

上面根据情况去掉了一些无关的输出, 从上到下, 第1开始将100赋值给!0也就是变量$i, 随后执行ZEND_DECLARE_LAMBDA_FUNCTION,那我们去相关的opcode执行函数中看看这里是怎么执行的, 这个opcode的处理函数位于$PHP_SRC/Zend/zend_vm_execute.h中:

[php] view plaincopy

  1. static int ZEND_FASTCALL  ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)  
  2. {  
  3.     zend_op *opline = EX(opline);  
  4.     zend_function *op_array;  
  5.    
  6.     if (zend_hash_quick_find(EG(function_table), Z_STRVAL(opline->op1.u.constant), Z_STRLEN(opline->op1.u.constant), Z_LVAL(opline->op2.u.constant), (void *) &op_arra  
  7. y) == FAILURE ||  
  8.         op_array->type != ZEND_USER_FUNCTION) {  
  9.         zend_error_noreturn(E_ERROR, "Base lambda function for closure not found");  
  10.     }  
  11.    
  12.     zend_create_closure(&EX_T(opline->result.u.var).tmp_var, op_array TSRMLS_CC);  
  13.    
  14.     ZEND_VM_NEXT_OPCODE();  
  15. }  

该函数调用了zend_create_closure()函数来创建一个闭包对象, 那我们继续看看位于$PHP_SRC/Zend/zend_closures.c的zend_create_closure()函数都做了些什么。

[php] view plaincopy

  1. ZEND_API void zend_create_closure(zval *res, zend_function *func TSRMLS_DC)  
  2. {  
  3.     zend_closure *closure;  
  4.    
  5.     object_init_ex(res, zend_ce_closure);  
  6.    
  7.     closure = (zend_closure *)zend_object_store_get_object(res TSRMLS_CC);  
  8.    
  9.     closure->func = *func;  
  10.    
  11.     if (closure->func.type == ZEND_USER_FUNCTION) { // 如果是用户定义的匿名函数  
  12.         if (closure->func.op_array.static_variables) {  
  13.             HashTable *static_variables = closure->func.op_array.static_variables;  
  14.    
  15.             // 为函数申请存储静态变量的哈希表空间  
  16.             ALLOC_HASHTABLE(closure->func.op_array.static_variables);   
  17.             zend_hash_init(closure->func.op_array.static_variables, zend_hash_num_elements(static_variables), NULL, ZVAL_PTR_DTOR, 0);  
  18.    
  19.             // 循环当前静态变量列表, 使用zval_copy_static_var方法处理  
  20.             zend_hash_apply_with_arguments(static_variables TSRMLS_CC, (apply_func_args_t)zval_copy_static_var, 1, closure->func.op_array.static_variables);  
  21.         }  
  22.         (*closure->func.op_array.refcount)++;  
  23.     }  
  24.    
  25.     closure->func.common.scope = NULL;  
  26. }  

如上段代码注释中所说, 继续看看zval_copy_static_var()函数的实现:

[php] view plaincopy

  1. static int zval_copy_static_var(zval **p TSRMLS_DC, int num_args, va_list args, zend_hash_key *key)  
  2. {  
  3.     HashTable *target = va_arg(args, HashTable*);  
  4.     zend_bool is_ref;  
  5.    
  6.     // 只对通过use语句类型的静态变量进行取值操作, 否则匿名函数体内的静态变量也会影响到作用域之外的变量  
  7.     if (Z_TYPE_PP(p) & (IS_LEXICAL_VAR|IS_LEXICAL_REF)) {  
  8.         is_ref = Z_TYPE_PP(p) & IS_LEXICAL_REF;  
  9.    
  10.         if (!EG(active_symbol_table)) {  
  11.             zend_rebuild_symbol_table(TSRMLS_C);  
  12.         }  
  13.         // 如果当前作用域内没有这个变量  
  14.         if (zend_hash_quick_find(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, (void **) &p) == FAILURE) {  
  15.             if (is_ref) {  
  16.                 zval *tmp;  
  17.    
  18.                 // 如果是引用变量, 则创建一个临时变量一边在匿名函数定义之后对该变量进行操作  
  19.                 ALLOC_INIT_ZVAL(tmp);  
  20.                 Z_SET_ISREF_P(tmp);  
  21.                 zend_hash_quick_add(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, &tmp, sizeof(zval*), (void**)&p);  
  22.             } else {  
  23.                 // 如果不是引用则表示这个变量不存在  
  24.                 p = &EG(uninitialized_zval_ptr);  
  25.                 zend_error(E_NOTICE,"Undefined variable: %s", key->arKey);  
  26.             }  
  27.         } else {  
  28.             // 如果存在这个变量, 则根据是否是引用, 对变量进行引用或者复制  
  29.             if (is_ref) {  
  30.                 SEPARATE_ZVAL_TO_MAKE_IS_REF(p);  
  31.             } else if (Z_ISREF_PP(p)) {  
  32.                 SEPARATE_ZVAL(p);  
  33.             }  
  34.         }  
  35.     }  
  36.     if (zend_hash_quick_add(target, key->arKey, key->nKeyLength, key->h, p, sizeof(zval*), NULL) == SUCCESS) {  
  37.         Z_ADDREF_PP(p);  
  38.     }  
  39.     return ZEND_HASH_APPLY_KEEP;  
  40. }  

这个函数作为一个回调函数传递给zend_hash_apply_with_arguments()函数, 每次读取到hash表中的值之后由这个函数进行处理,而这个函数对所有use语句定义的变量值赋值给这个匿名函数的静态变量, 这样匿名函数就能访问到use的变量了。

原文链接:

http://www.php-internals.com/book/?p=chapt04/04-04-anonymous-function

参考阅读:

http://php.net/manual/zh/functions.anonymous.php

以上就介绍了PHP匿名函数及闭包,包括了方面的内容,希望对PHP教程有兴趣的朋友有所帮助。

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

php

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
batoto漫画官网入口与网页版访问指南
batoto漫画官网入口与网页版访问指南

本专题系统整理batoto漫画官方网站最新可用入口,涵盖最新官网地址、网页版登录页面及防走失访问方式说明,帮助用户快速找到batoto漫画官方平台,稳定在线阅读各类漫画内容。

329

2026.02.25

Steam官网正版入口与注册登录指南_新手快速进入游戏平台方法
Steam官网正版入口与注册登录指南_新手快速进入游戏平台方法

本专题系统整理Steam官网最新可用入口,涵盖网页版登录地址、新用户注册流程、账号登录方法及官方游戏商店访问说明,帮助新手玩家快速进入Steam平台,完成注册登录并管理个人游戏库。

49

2026.02.25

TypeScript全栈项目架构与接口规范设计
TypeScript全栈项目架构与接口规范设计

本专题面向全栈开发者,系统讲解基于 TypeScript 构建前后端统一技术栈的工程化实践。内容涵盖项目分层设计、接口协议规范、类型共享机制、错误码体系设计、接口自动化生成与文档维护方案。通过完整项目示例,帮助开发者构建结构清晰、类型安全、易维护的现代全栈应用架构。

33

2026.02.25

Python数据处理流水线与ETL工程实战
Python数据处理流水线与ETL工程实战

本专题聚焦 Python 在数据工程场景下的实际应用,系统讲解 ETL 流程设计、数据抽取与清洗、批处理与增量处理方案,以及数据质量校验与异常处理机制。通过构建完整的数据处理流水线案例,帮助开发者掌握数据工程中的性能优化思路与工程化规范,为后续数据分析与机器学习提供稳定可靠的数据基础。

13

2026.02.25

Java领域驱动设计(DDD)与复杂业务建模实战
Java领域驱动设计(DDD)与复杂业务建模实战

本专题围绕 Java 在复杂业务系统中的建模与架构设计展开,深入讲解领域驱动设计(DDD)的核心思想与落地实践。内容涵盖领域划分、聚合根设计、限界上下文、领域事件、贫血模型与充血模型对比,并结合实际业务案例,讲解如何在 Spring 体系中实现可演进的领域模型架构,帮助开发者应对复杂业务带来的系统演化挑战。

5

2026.02.25

Golang 生态工具与框架:扩展开发能力
Golang 生态工具与框架:扩展开发能力

《Golang 生态工具与框架》系统梳理 Go 语言在实际工程中的主流工具链与框架选型思路,涵盖 Web 框架、RPC 通信、依赖管理、测试工具、代码生成与项目结构设计等内容。通过真实项目场景解析不同工具的适用边界与组合方式,帮助开发者构建高效、可维护的 Go 工程体系,并提升团队协作与交付效率。

19

2026.02.24

Golang 性能优化专题:提升应用效率
Golang 性能优化专题:提升应用效率

《Golang 性能优化专题》聚焦 Go 应用在高并发与大规模服务中的性能问题,从 profiling、内存分配、Goroutine 调度、GC 机制到 I/O 与锁竞争逐层分析。结合真实案例讲解定位瓶颈的方法与优化策略,帮助开发者建立系统化性能调优思维,在保证代码可维护性的同时显著提升服务吞吐与稳定性。

9

2026.02.24

Golang 面试题精选:高频问题与解答
Golang 面试题精选:高频问题与解答

Golang 面试题精选》系统整理企业常见 Go 技术面试问题,覆盖语言基础、并发模型、内存与调度机制、网络编程、工程实践与性能优化等核心知识点。每道题不仅给出答案,还拆解背后的设计原理与考察思路,帮助读者建立完整知识结构,在面试与实际开发中都能更从容应对复杂问题。

7

2026.02.24

Golang 运行与部署实战:从本地到云端
Golang 运行与部署实战:从本地到云端

《Golang 运行与部署实战》围绕 Go 应用从开发完成到稳定上线的完整流程展开,系统讲解编译构建、环境配置、日志与配置管理、容器化部署以及常见运维问题处理。结合真实项目场景,拆解自动化构建与持续部署思路,帮助开发者建立可靠的发布流程,提升服务稳定性与可维护性。

5

2026.02.24

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
前端系列快速入门课程
前端系列快速入门课程

共4课时 | 0.4万人学习

PHP课程
PHP课程

共137课时 | 12.5万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 11.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号