学习一下闭包函数 - Closures

什么是闭包?

PHP 文档这样介绍:

匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数(callback)参数的值。当然,也有其它应用的情况。

真的很含糊,什么叫还有其它应用的情况。。。

找了 JavaScript 的文档:

Closures (闭包)是使用被作用域封闭的变量,函数,闭包等执行的一个函数的作用域。通常我们用和其相应的函数来指代这些作用域。(可以访问独立数据的函数)。

闭包是一个函数和声明该函数的词法环境的组合。从理论角度来说,所有函数都是闭包。

关键在于“作用域”、“环境”。

栗子:

<?php
$closure = function($name) {
    printf("Hello %s\r\n", $name);
};

$closure('World');
// Hello World

为什么要用闭包?

闭包有一个特点,内部函数可以引用外部函数的参数和变量,参数和变量就不会被收回。

环境被保存下来。

栗子:

<?php
$add = function() {
    $sum = 0;
    return function() use (&$sum): int {
        $sum += 1;
        return $sum;
    };
};

$test = $add();

echo $test(), "\n"; // 1
echo $test(), "\n"; // 2

一般函数局部变量无法长久地保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久地保存变量又不会造成全局污染。

PHP 闭包的实现原理

目前是通过 Closure 类来实现,调用闭包函数的过程与 __invoke 魔术方法无关。

这是文档说的,但是很奇怪,下面的代码两个输出是一样的。

<?php
$closure = function($name) {
    printf("Hello %s\r\n", $name);
};

$closure('World'); // Hello World
$closure->__invoke('World'); // Hello World

原理网上找了下,比较少相关文章。

与 JavaScript 闭包区别

最大的区别在于作用域。

可以看到前面加一的例子,使用了 use 关键字,PHP 里叫做变量“继承”,而且逐层传递(只能传递父级作用域的变量)。

这跟 PHP 语言特性有关, PHP 只有函数作用域、类作用域,而没有块级作用域。

<?php

$i = 1;
while($i--) {
    $j = 0;
}

echo $j, "\n"; // 0

奇葩有木有。。。

函数里访问不了函数外部的变量,需要访问的话可以通过传参和 use 传递。

下面的例子可以更好看出区别。

举栗子

使用闭包打印斐波那契数列。我们知道斐波那契数列有下面的规律:

# f(n) 表示数列中第 n 个数的值
f(n) = 0; (n = 0)
f(n) = 1; (n = 1)
f(n) = f(n-1)+f(n-2); (n >= 2)

使用递归的方法

<?php
function fibonacci(int $n): int {
    if ($n < 2) {
        return $n;
    }
    return fibonacci($n-1) + fibonacci($n-2);
}

echo fibonacci(10), "\n"; // 55

如果打印数列,那每一次都需要重复计算前面已经计算过的数据(只需要前两个就好)。

可以使用闭包保存上一次的运行环境。

PHP 版本

<?php
$fibonacci = function (): callable {
    $x = 0;
    $y = 1;
    return function () use (&$x, &$y): int {
        list($x, $y) = [$y, $x+$y];
        return $x;
    };
};

$f = $fibonacci();

for ($i = 0; $i < 10; $i++) {
    echo $f() , "\n";
}

JavaScript 版本

let fibonacci = _ => {
    let x = 0, y = 1;
    return _ => {
        [x, y] = [y, x+y];
        return x;
    };
};

let f = fibonacci();

for (let i = 0; i <= 10; i++) {
    console.log(f());
}

对,说了那么多我就是想证明 JavaScript ES6 好简洁。

本文章首发在 Laravel China 社区

癞蛤蟆想吃炖大鹅