从setTimeout来看javascript的作用域

博客围绕一段含setTimeout的JavaScript代码展开,解释了代码输出3个3的原理,一是循环中的函数封闭在共享全局作用域,只有一个i;二是setTimeout回调在循环结束时执行。还给出使输出为0、1、2的方法,即使用let创建隐形作用域和用function创建独立作用域。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先我们来看这样一段代码:

 for (var i=0; i<3; i++) {
     setTimeout(() => {
         console.log(i)
     }, 0)
 }

很多刷过面试题的jser看到这段代码应该很熟悉了,脱口就能说出正确答案是 3 个 3,但有不少人却不明白其中的原理。别小看这段代码,其中暗含了不少的js的基础知识。
从代码层面来,很多童鞋会想当然的将上述代码理解成以下的内容:

    for (var i=0; i<3; i++) {
        console.log(i)
    }

即,输出为0、1、2。

如果这样想的话,他们至少犯了2个错误:

  1. 写出第一段代码的童鞋,都是试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i 。
  2. setTimeout等延迟函数的回调会在循环结束时才会执行,故而不管循环多少次,延迟多少时间,console.log中的i,必然是共享的全局作用域中变量i,在此例中就是3。

从第一段代码的执行顺序来看,其演示代码应该是:

var i=0;
i++;
i++;
i++;
console.log(i);
console.log(i);
console.log(i);

上述代码就很好地解释了第一段代码3个3的执行结果了。

~~ 那怎样才能使上述代码的输出为0、1、2呢?

提供两种方法:

  1. 使用let
    for (let i=0; i<3; i++) {
        setTimeout(() => {
            console.log(i)
        }, 0)
    }

let 本质上这是将一段代码块转换成一个可以被关闭的作用域。或许这样看会比较直观。

    for (var i=0; i<5; i++) {
        let j = i
        setTimeout(() => {
            console.log(j)
        }, 0)
    }

let的存在,会产生一个隐形的作用域,使得每次循环的 i 值通过复制给隐形作用域内的 j 保存下来。

  1. 使用function创建独立作用域
    for (var i=0; i<3; i++) {
        (function(j) {
            setTimeout(() => {
                console.log(j)
            }, 0)
        })(i)
    }

众所周知,js 中 function 可以提供一段独立的作用域,那我们就可以通过在迭代内使用function会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

### 关于 `setTimeout` 是否为 JavaScript 的全局函数 在 JavaScript 中,`setTimeout` 是一个非常常用的功能,用于延迟执行指定的代码片段。然而,关于其是否属于全局函数这一问题存在一定的争议。 从技术角度来看,`setTimeout` 实际上是 **Window 对象** 或者更广泛地说是 **Global Object** 的方法之一[^1]。这意味着它可以像全局函数一样被调用,但实际上它是 Window 对象的一个属性或方法。因此,在严格的定义下,只有那些完全独立于任何对象存在的函数才能被称为真正的“全局函数”,而 `setTimeout` 并不符合这个标准[^1]。 尽管如此,在实际开发过程中以及许多文档中,诸如 `setTimeout`, `setInterval`, 和其他类似的 API 常常被视为全局函数的一部分[^2]。这是因为开发者可以直接通过名称访问它们,无需显式引用所属的对象(即不需要写成 `window.setTimeout`)。这种行为使得这些方法看起来像是直接隶属于语言本身的全局功能。 具体到 `setTimeout` 函数本身,它的主要用途是在一定时间之后运行给定的任务。以下是该函数的一些基本特性及其常见用法: #### 特性和语法 - 它接受至少两个参数:第一个是要执行的回调函数;第二个是以毫秒计的时间间隔。 - 可选地支持传递额外参数至回调函数。 ```javascript // 示例 1: 使用匿名函数作为回调 setTimeout(function() { console.log('This will run after a second.'); }, 1000); // 示例 2: 将数值型变量传入回调函数 const message = 'Delayed output'; setTimeout((msg) => { console.log(msg); }, 500, message); ``` 以上展示了如何利用 `setTimeout` 来安排异步操作,并且说明了当需要向延时处理程序提供动态数据时可能遇到的作用域挑战[^4]^。 值得注意的是,虽然通常情况下我们可以简单地使用 `setTimeout()` 而不必关心上下文环境,但在某些特殊场景比如模块化编程环境下,则需注意绑定正确的 this 上下文[^3]. 综上所述,即使严格意义上讲 `setTimeout` 不应视为纯粹意义上的全局函数,但由于其实现方式使其表现得如同全局可用一般便捷实用,所以在日常讨论里经常会被提及为其中之一[^2]. ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值