每秒打印一个数字:从简单到晦涩的多种实现

如果你看到标题中的"每秒打印一个数字",可能已经有了想法,最简单的定时器、在Web中使用requestAnimationFrame等。但是本文是不打算讲解在Web的实现的,我们就用一个简单的例子来看看 JavaScript 中有哪些有趣的实现方式。


一、setTimeout 方案

首先,让我们用 setTimeout 实现这个需求:

js 复制代码
function printNumberWithTimeout() {
    for (let i = 1; i <= 10; i++) {
        setTimeout(() => {
            console.log(i);
        }, i * 1000);
    }
}

思考一下:

  • 为什么需要 i * 1000
    每次循环时,我们都设置了一个延时任务。把延时乘以 i,就实现了每个数字间隔 1 秒的效果。
  • 注意什么?
    使用 let 声明 i 能确保每个 setTimeout 回调拿到正确的数字。如果用 var,由于变量提升和函数作用域问题,结果可能就不对了。

这种方法直白简单,就像是在某一刻开了十个定时器,只不过这十个定时器执行的时间都不一样,是i * 1000


二、setInterval 方案

接下来,换个角度,我们用 setInterval 实现:

js 复制代码
function printNumberWithInterval() {
    let i = 1;
    const intervalId = setInterval(() => {
        console.log(i);
        i++;
        if (i > 10) {
            clearInterval(intervalId);
        }
    }, 1000);
}

和大家聊聊:

  • 每秒一响
    setInterval 就像是闹钟,每隔 1 秒敲响一次。这里我们依靠计数器 i 来决定什么时候该停下闹钟。
  • 清理的重要性
    当数字超过 10 后,我们调用 clearInterval。这一步就像在告诉闹钟,"够了,暂停吧"。

这种方法更加直观,就像一个持续响铃的时钟,当然,及时"关闹钟"是防止内存泄露的关键。


三、递归方式

有没有想过用递归来"接力"打印数字呢?试试这个方法:

js 复制代码
function printNumberWithRecursion(i = 1) {
    if (i > 10) return;
    console.log(i);
    setTimeout(() => {
        printNumberWithRecursion(i + 1);
    }, 1000);
}

思路互动:

  • 递归的魅力
    每次打印后,通过 setTimeout 调用自身,达到"接力赛"的效果。每次等待 1 秒后,递归就带着新的数字重新上场。
  • 递归终止条件
    别忘了给递归加上出口(if (i > 10) return;),否则它会永不停歇。

四、Promise 与 async/await

随着异步编程越来越流行,利用 Promise 和 async/await 的方式也显得尤为优雅:

js 复制代码
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function printNumberWithPromise() {
    for (let i = 1; i <= 10; i++) {
        await sleep(1000);
        console.log(i);
    }
}

你可能会问:

  • 为何"等待"能让代码看起来更同步?
    await 会暂停函数的执行直到 Promise 解决(无论是否正确执行),就像让代码排起队来等待上一个任务结束后再继续。

这里是直接通过await直接暂停了for循环的执行。(之前看到过一篇文章,关于如何是的循环暂停的,感兴趣的小伙伴可以去看看: 面试官:你可以终止 forEach 吗?


五、生成器函数的妙用

如果你觉得上面的方式太"直白",那生成器可能会让你眼前一亮:

js 复制代码
function* numberGenerator() {
    for (let i = 1; i <= 10; i++) {
        yield new Promise(resolve => setTimeout(() => {
            console.log(i);
            resolve();
        }, 1000));
    }
}

async function printNumberWithGenerator() {
    const generator = numberGenerator();
    for (const promise of generator) {
        await promise;
    }
}

一起探讨:

  • 生成器和 yield 是如何工作的?
    当你调用生成器函数numberGenerator时,它并不会立即执行,而是返回一个迭代器对象。每次遇到 yield,函数就会暂停,并返回一个 Promise。等你通过 for...of 迭代并 await 它后,生成器再继续执行。

六、Array.reduce 的高级玩法

最后,我们用函数式编程中的 reduce 来串联 Promise,这可能是最"隐晦"的一种写法了:

js 复制代码
function printWithReduce() {
    Array.from({ length: 10 }, (_, i) => i + 1)
        .reduce((promise, num) =>
            promise.then(() =>
                new Promise(resolve =>
                    setTimeout(() => {
                        console.log(num);
                        resolve();
                    }, 1000)
                )
            ),
            Promise.resolve()
        );
}

让我们拆解这个思路:

  • 从数组到 Promise 链
    首先,通过 Array.from 生成数字数组。接着,用 reduce 把这个数组变成一个 Promise 链。你可以把每个 Promise 看作是链条上的一环,前一环完成后才启动下一环。
  • 为什么选择这种方式?
    函数式编程的魅力:将一系列操作通过链式调用串联起来,代码虽然看上去"隐晦",但却精妙地把异步操作变得井然有序。(当然,如果是面试,就当炫技了)

参考文档

相关推荐
qq. 28040339843 小时前
CSS层叠顺序
前端·css
喝拿铁写前端4 小时前
SmartField AI:让每个字段都找到归属!
前端·算法
猫猫不是喵喵.4 小时前
vue 路由
前端·javascript·vue.js
烛阴4 小时前
JavaScript Import/Export:告别混乱,拥抱模块化!
前端·javascript
bin91534 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例12,TableView16_12 拖拽动画示例
前端·javascript·vue.js·ecmascript·deepseek
GISer_Jing5 小时前
[Html]overflow: auto 失效原因,flex 1却未设置min-height &overflow的几个属性以及应用场景
前端·html
牛马baby5 小时前
Java高频面试之并发编程-01
java·开发语言·面试
程序员黄同学5 小时前
解释 Webpack 中的模块打包机制,如何配置 Webpack 进行项目构建?
前端·webpack·node.js
拉不动的猪5 小时前
vue自定义“权限控制”指令
前端·javascript·vue.js
再学一点就睡5 小时前
浏览器页面渲染机制深度解析:从构建 DOM 到 transform 高效渲染的底层逻辑
前端·css