let简介与妙用

由于本篇文章分两次完成,所以前后可能存在重复解释与说明。如果你只是对let的妙用感兴趣并且对let已经有深入理解那么只需要阅读前面两个小章节,如果你对this、let,js执行机制都不是很明白,建议在看完第一小节的代码以后直接从第三小节"深入理解var与let"开始阅读。

let简介

  • let 关键字用来声明变量,使用 let 声明的变量有几个特点:
  • 不允许重复声明
  • 块级作用域(while、for、if else、{}、eval)
  • 不存在变量提升
  • 不影响作用域链

应用场景:以后在块级声明变量使用 let 就对了

let在for循环中的妙用

💡 需求:三个div模块通过点击模块让相应模块变亮

用this关键字:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>点击 DIV 换色</title>
    <link crossorigin="anonymous" href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css"
        rel="stylesheet">
    <style>
        .item {
            width: 100px;
            height: 50px;
            border: solid 1px rgb(42, 156, 156);
            float: left;
            margin-right: 10px;
        }
    </style>
</head>

<body>
    <div class="container">
        <h2 class="page-header">点击切换颜色</h2>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
    </div>
    <script>
        //获取div元素对象
        let items = document.getElementsByClassName('item');

        //遍历并绑定事件
        for(var i = 0;i<items.length;i++){
            items[i].onclick = function(){
                //修改当前元素的背景颜色
                this.style.background = 'pink';
                // items[i].style.background = 'pink';
            }
        }
        
    </script>
</body>

</html>

截图

💡 Tips:通过点击每一个模块,每个模块都会相应变亮,这是this关键字的功劳,因为点击每一个模块,执行this.style.background = 'pink';这行代码的this对象就是对应的元素对象

常规思路

💡 Tips:items[i].style.background = 'pink';我们大多数人想到的是用这行代码去代替上面一行代码

截图

💡 Tips:会报错Uncaught TypeError: Cannot read property 'style' of undefined,说undefined,为什么呢?因为经过for循环以后i变量已经变成了3,无论点击哪个div,执行onclick里面的函数的时候items[i].style.background中的[i]相当于是[3],所以肯定会报错误,但是如果我们用let关键字就不会出现这个问题

巧用let关键字解决作用域问题

💡 Tips:将for循环里面的var变成let就可以利用作用域的限制让i限制在代码块中,当你去控制台直接输出i的时候,会找不到i,因为i被定义在代码块中而不是Windows对象中,var定义的变量就相当于在Windows中。

let items = document.getElementsByClassName('item');

        //遍历并绑定事件
        for(let i = 0;i<items.length;i++){
            items[i].onclick = function(){
                //修改当前元素的背景颜色
                // this.style.background = 'pink';
                items[i].style.background = 'pink';
            }
        }
// 上面的代码就相当于下面的代码
// {
//             let i =0;
//             items[i].onclick = function(){
//             items[i].style.background = 'pink';
//             }
//         }
// {
//             let i =1;
//             items[i].onclick = function(){
//             items[i].style.background = 'pink';
//             }
//         }
// {
//             let i =2;
//             items[i].onclick = function(){
//             items[i].style.background = 'pink';
//             }
// }
        

截图

解释

如果我们点击第一个div相当于触发了items[i].style.background = 'pink';这行代码,但是items[i]中的i不知道是多少,于是js就会去外面的作用域找,发现i = 0;于是就执行了,但是如果我们用的var的关键字,那么在执行items[i].style.background = 'pink,这行代码的时候,i经过for循环下来,其赋值语句就相当于这行代码var i = 3;那肯定会执行失败。

深入理解var与let

可能很多朋友还是一知半解并且有很多疑问,所以今天我对上面的内容做一次补充,并且还会用JavaScript的任务队列来进一步加深各位的理解。

其实上面的事件绑定,无论是用var还是let都执行了三次3-5的行代码,即进行了三次绑定事件,只是还是没执行回调函数(function里面的函数),仅仅进行了绑定(其实就是声明了一个点击才能触发的函数)。用var声明的i,在执行点击操作之前,i值就已经变成3了(不要认为在进行函数绑定的时候,click事件没有触发for循环就不会继续执行,这种理解是错误的,这里仅仅是绑定事件而不是运行click事件),所以在点击模块以后,items[i].onclick中的i因为是3,所有找不到对应的dom就会报错(items[3]不存在)。

//遍历并绑定事件
        for(var i = 0;i<items.length;i++){
            items[i].onclick = function(){
                //修改当前元素的背景颜色
                this.style.background = 'pink';
                // items[i].style.background = 'pink';
            }
        }

为了让代码运行不让其报错,我们把for循环中的条件判断语句改成i<2,只循环两次,这样i值在for执行完以后就是2,并且为了方便我们清楚执行情况,我们加入一行代码console.log(i)打印出i值;来看看前两个模块到底有没有执行绑定事件,注意控制台输出的i值。

//遍历并绑定事件
for(var i = 0;i<2;i++){
            items[i].onclick = function(){
                //修改当前元素的背景颜色
                this.style.background = 'pink';
        				console.log(i);
                //items[i].style.background = 'pink';
            }
}

操作:从左到右分别点击三个模块

控制台打印:打印了两次i值,都是2

页面显示:两个模块变色

结果说明

前两个模块成功点亮最后一个模块不亮,说明我们确实绑定了前两个div的点击事件。this这里指的是调用外层模块的对象,及谁调用的我,我就是谁,这里调用我这个回调函数的是div元素对象,所以this就是div这个对象。控制台两次显示i值都为2,说明div模块拿到的是for循环结束以后的值。

//获取div元素对象
        let items = document.getElementsByClassName('item');

        //遍历并绑定事件
        for(var i = 0;i<2;i++){
            items[i].onclick = function(){
                //修改当前元素的背景颜色
                this.style.background = 'pink';
        				console.log(i);
                //items[i].style.background = 'pink';
            }
        }

我们重新运行代码,这次只点击第三个模块看看有没有任何变化。

截图

解释

点击第三个模块发现没有任何变化,这是因为我们没有绑定第三个模块的点击事件,虽然这时候的i是2,但在for循环中我们只绑定了前面两个模块,所以第三个模块当然没有发生什么变化。

我们对代码做一些稍微的改变,把items[i].style.background = 'pink';改成this.style.background = 'pink';并且点击前面两个div模块,观察是否变色。

//获取div元素对象
        let items = document.getElementsByClassName('item');

        //遍历并绑定事件
        for(var i = 0;i<2;i++){
            items[i].onclick = function(){
                //修改当前元素的背景颜色
                //this.style.background = 'pink';       				
                items[i].style.background = 'pink';
                console.log(i);
            }
        }

截图

对this的解释

是不是很奇怪,为啥全是第三个模块在亮,刚才不是说第三个模块没有绑定事件吗?按理说不会触发他的回调函数呀!?其实这是因为我们虽然没有绑定第三个模块的点击事件,但是我们在第一个模块中和第二个模块中触发了点亮第三个模块的代码 即items[i].style.background = 'pink';注意控制台打印的i值,因为for循环以后i值来到了2,在运行回调函数的时候,真实执行的代码是这样的--items[2].style.background = 'pink'。所以无论是点击一个还是第二个模块,都会导致第三个模块变色。而用this调用的时候,解释执行器在看见this这个关键字的时候会根据上下文替换成对应的对象,如果是点击第一个模块,那么对象就是iteam[0],如果是第二个模块则是iteam[1],所以我们在函数内部调用对象的属性和函数的时候,应该要慎重考虑选择this还是直接通过变量名称,不同的选择会导致程序走向不同的结果。

对let的详细解释

我们将代码变回原样,用let声明变量i。运行下面的代码并依次从左到右点击div模块,然后注释第10行代码打开第8行代码再点击一遍,两次运行结果如下:

代码:

//获取div元素对象
        let items = document.getElementsByClassName('item');

        //遍历并绑定事件
        for(let i = 0;i<items.length;i++){
            items[i].onclick = function(){
                //修改当前元素的背景颜色
                //this.style.background = 'pink';
        				console.log(i);
                items[i].style.background = 'pink';
            }
        }

截图:

与var进行比较

发现两次结果都一样,并且都能顺序打印出012,为什么用let声明的i值与用var声明的i值打印的结果不一样呢?这是因为用let声明的i值在每次迭代的时候都会创建新的块级作用域,这就意味着我们其实声明了三个块级作用域,并且他们的值在每次迭代的时候都会被正确的保存,因此会按顺序输出0到2.

真实执行情况如下:

{
            let i =0;
            items[i].onclick = function(){
            console.log(i);
            items[i].style.background = 'pink';
            }
        }
{
            let i =1;
            items[i].onclick = function(){
            console.log(i); 
            items[i].style.background = 'pink';
            }
        }
{
            let i =2;
            items[i].onclick = function(){
            console.log(i);
            items[i].style.background = 'pink';
            }
}

这样每个块级作用域中都保存着i的值,这些i值互相独立,互不影响,即使for循环结束以后,i值来到了2,但是在每个块级作用域中都保存着不同的i值,所以可以打印出不同的i值。但是用var声明的话,是不会存在三个块级作用域(即使存在也会因为var变量特性全局共享,导致获得的都是同一个值),i值是共享的,for循环结束以后i值来到了3,那么在执行click的回调函数的时候,由于items[i].style.background = 'pink'中的i始终为3,而找不到dom对象所以会报错。但是通过this.style.background = 'pink'仍然可以改变对应div的颜色,这是因为我们在for循环中分别为三个div元素对象绑定了三个回调函数,虽然i值来到了3,但是我们点击div模块的时候,执行器看见了this就会把调用这个函数的对象找到,通过对象的地址,从而成功执行改变颜色的代码,如果仅仅是 items[i]来调用,就会以为i值是3找不到或者错找对象,无法改变对应的模块的颜色。

通过事件机制深入理解let与var的区别

要搞懂JavaScript的执行机制,我们得先明白三个概念宏任务、微任务、同步任务,由于本文章不是专门讲解JavaScript的执行机制,只是为了方便我们彻底搞懂var与let的区别,所以我只做简略的介绍,详细的介绍可以查看其它博主的文章。

事件循环机制

我们都知道JavaScript是单线程执行的,但是这其中一个很大的弊端就是如果某个函数的执行过程需要很长时间,那么必然会导致后续代码长时间无法执行,很容易在执行一些异步事件的时候让页面卡死。所以JavaScript引入了任务队列,任务队列分为宏任务队列与微任务队列。

宏任务队列

setTimeout、setInterval、setImmediate(Node.js 环境中)、I/O 操作、网络请求等 JavaScript中

微任务队列

Promise.then()、Promise.catch()、Promise.finally() MutationObserver 的回调函数以及大部分的dom事件等。

顾名思义,宏任务就是系统认为这些函数可能会消耗很长的时间,微任务就是较小的需要尽快执行的任务 。

同步任务

我们编写的大部分的代码以及调用大部分js内置对象的函数都是同步任务。例如自定义一个函数、对变量进行逻辑运算,调用Math.max()内置对象的函数等。

事件循环机制

1. 执行同步任务:JavaScript 引擎首先执行主线程上的同步任务,按照代码的顺序依次执行每个任务。
2. 检查微任务队列:在执行完所有同步任务后,JavaScript 引擎会检查微任务队列是否为空。如果微任务队列不为空,则会按照先进先出的原则依次执行微任务。
3. 更新 DOM:如果在执行微任务过程中对 DOM 进行了修改,浏览器会在微任务执行完毕后立即更新 DOM。
4. 执行宏任务:微任务执行完毕后,JavaScript 引擎会从任务队列中取出一个宏任务并执行它。
5. 重复步骤 2-4:执行完一个宏任务后,JavaScript 引擎会再次检查微任务队列,并执行微任务,然后更新 DOM,接着执行下一个宏任务,如此循环往复。

流程图

图片来源:https://www.zhihu.com/question/40250879/answer/2520016548

如果仍然不懂JavaScript的事件执行机制可以点击上面链接,有系统介绍。

好了有了以上理解,我们来看以下代码

for (var i = 0; i < 10; i++) {    
  setTimeout(function(){
    console.log(i);
  })
}

截图

解释

在第一个循环中,使用 var 关键字声明的变量 i 在函数作用域内是共享的。每次迭代时, setTimeout 函数都会被添加到事件队列中,并在稍后执行。但是,当 setTimeout 函数执行时,它们都引用了同一个变量 i ,而此时 i 的值已经被循环更新为 10 。因此,最终会输出十个 10 。 (这段代码一般会产生十个宏任务,也有可能会被浏览器引擎优化合并成一个宏任务,因为不同浏览器有不同的宏任务合并策略,此处不做讨论,也不影响执行结果))

再看下面的代码:

for (let j = 0; j < 10; j++) {
  setTimeout(function(){
    console.log(j);
  })
}

截图

解释:

在第二个循环中,使用 let 关键字声明的变量 j 在每次迭代时都会创建一个新的块级作用域。这意味着每个 setTimeout 函数都有自己独立的变量 j ,并且它们的值在每次迭代时都会被正确地保存。因此,最终会按顺序输出 0 到 9 。

for (var i = 0; i < 10; i++) {    
  setTimeout(function(){
    console.log(i);
  })
}
console.log("我先执行");
for (let j = 0; j < 10; j++) {
  setTimeout(function(){
    console.log(j);
  })
}
console.log("我后执行");

截图

解释

1. 首先,代码会执行第一个  for  循环,其中定义了一个  var  变量  i ,并使用  setTimeout  函数创建了十个宏任务。每个宏任务都会在一定时间后执行回调函数,输出当前的  i  值。
2. 接下来,会立即执行  console.log("我先执行") ,输出  "我先执行" 。
3. 然后,代码会执行第二个  for  循环,其中定义了一个  let  变量  j ,并再次使用  setTimeout  函数创建了十个宏任务。
4. 紧接着,会立即执行  console.log("我后执行") ,输出  "我后执行" 。
5. 最后,同步代码块执行完毕,没有微任务需要处理,页面不需要渲染,所以在遵循事件执行机制的情况下,会进行二十次的事件循环,每一次都会取一个宏任务进行执行,宏任务的执行顺序遵循先进先出(FIFO)的原则,因为这个时候for循环已经结束,所以var声明的变量i已经变成了10,所以会输出10个10,而由let声明的变量j,因为每一次循环都创建了新的块级作用域,并在其中保留了对应的i值,所以依次输出0-9的值。

总结

综上所述, for 循环的每次迭代都会创建一个新的块级作用域,并在该作用域内声明一个新的变量 i 。每个迭代中的 i 都是独立的,互不影响。
这种行为使得 let 在 for 循环中非常有用,因为它可以确保每次迭代都有自己独立的变量,避免了变量污染和意外的结果。
相比之下,使用 var 声明的变量在 for 循环中只有一个全局作用域或函数作用域的变量,每次迭代都会共享同一个变量。

  • 28
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值