请马上登录,朋友们都在花潮里等着你哦:)
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 马黑黑 于 2024-4-19 23:43 编辑
递归指的是一个函数在执行过程中调用自己、直至执行条件满足时终止函数的自执行。我们接触得最多的应该就是请求关键帧动画,我们把动画的执行机制做成一个函数,在函数内部使用 requestAnimationFrame() 反复调用该函数:
let raf = null; //操作动画的开关
let render = () => {
/* ... 这里是运行动画的代码 */
raf = requestAnimationFrame(render); //通过请求关键帧动画调用函数自身
};
requestAnimationFrame() 方法是JS封装好的以显示刷新率为运行频率的API,它在函数 render() 的内部调用了函数 render() ;自身,使得函数要执行的任务得以不间断地执行。上例没有在函数内部提供任务终结的直接出口,而是通过请求关键帧动画标识 raf 用做开关,以便可以在函数的外部对动画的运行、暂停进行有效控制。不过地道的递归函数则应在函数内部提供出口条件,换言之,函数自身应该具备终止执行任务的条件,为此,我们在设计递归函数时,除了可以像上例那样可以在外部开、关函数的运行外,还应有能力在函数内部设立开、关逻辑清晰的运行机制,让递归函数适时终止自执行。以下例子,函数需要一个整数型参数,函数的任务是1加到该参数,就是等差数列求和,数学家高斯小时候做的1加到100的那个问题:
let add = (num) => {
if(num <= 0) return 0;
console.log(`${num} + add(${num} - 1)`);//本行测试 :打印运算式子
return num + add(num - 1);
};
console.log(add(100));
add(num) 函数首先设立一个条件,即代码第2行,如果参数 num ≤ 0 将返回 0,这个条件一旦满足,函数就会退出运行。第 4 行代码,整体意思是返回 num + add(num -1),这里信息量很庞大:return 是返回结果;而结果,首先是 num 自身,其次是它依次通过修改参数为 num - 1 来反复调用函数自身、直至参数 num ≤ 1 时把第二行的返回值也加上就退出累加工作。代码第 6 行,运行函数 add(num),传参是 100,并打印出最终累加结果。
需要特别注意,递归函数必须设计出口,以逻辑清晰的条件为临界,令任务执行完毕跳出任务的执行。递归递归,递是传递,将传递出去的指令一一落实,归就是回归,指令执行到满足条件了就要回头,不要无休止地干下去。
利用递归函数,我们可以在canvas画布上画一棵树,思路是这样子:
① 设计一个 draw 函数,需要 5 个参数:树干起始点XY坐标 sx 和 sy、树干单节长度 len、树干宽度 width、树干折向角度 deg;
② 接力绘制树干,每一次都将画布坐标系位移到(sx,sy)处、旋转画布坐标系 deg 个弧度。首次在外部调用函数时使用实数参数,内部的递归调用则依据情况按一定系数调整各个参数;
③ 以 len 参数即一节树干(后来变为树枝)的长度为出口参数,内部递归调用乘上一个小于 1 的参数,这样它不断变短——实际上从第二次开始它们变成了分岔的树枝——,当它短到一定的程度,退出递归调用,退出时还有一个重要的任务,绘制果实;
④ 函数内部要两次递归调用函数自身,实现树枝左右折向,同时也令绘制情景更为复杂化;
⑤ 我们需要树枝的折向角度不那么一致,以避免树的成品过于对称。
这样绘制出来的树不带叶子,适合在沙漠中生长,其果实可以加工成黑石浓缩饮料,价值连城。下面是完整代码:
<canvas id="canv" width="800" height="500" style="border: 1px solid gray"></canvas>
<script>
let ctx = canv.getContext('2d');
let ww = canv.width, hh = canv.height;
let rad = () => Math.random() * 10 + 10; //获取10~20的弧度
/* 绘制函数 以画树干为主体绘制任务,通过递归调用函数自身,变树干为树枝
当树干长度低于 15 改画随机颜色的果实并退出绘制任务
*/
let draw = (sx,sy,len,width,deg) => {
ctx.strokeStyle = 'olivedrab';
ctx.lineWidth = width;
ctx.lineCap = 'square';
ctx.lineJoin = 'miter';
ctx.beginPath();
ctx.save();
ctx.translate(sx,sy);
ctx.rotate(deg * Math.PI / 180);
ctx.moveTo(0, 0);
ctx.lineTo(0, -len);
ctx.stroke();
if(len < 15) {
ctx.beginPath();
ctx.arc(0, -len, 6, 0, deg * Math.PI * 2, false);
ctx.fillStyle = `#${Math.random().toString(16).substr(-6)}`;
ctx.fill();
ctx.restore();
return;
}
//两次不同折角的递归调用函数自身
draw(0, -len, len * 0.8, width * 0.8, deg + rad());
draw(0, -len, len * 0.8, width * 0.8, deg - rad());
ctx.restore();
};
//从函数外部调用绘制函数
draw(ww / 2, hh, hh / 4.5, 10, 0);
</script>
可以将上述代码存为本地 .html 文档,或将代码拿到 pencil code,然后运行查看效果。
|