当前位置: 首页 > news >正文

一道面试题引发的“血案”

es6之前,js的作用域只有两种,全局作用域和函数作用域,没有像C和java那样的块级作用域,于是对于学了C或者java这类语言的然后学习js的同学来说,会遇到很多坑。js的这个特性导致了代码的可阅读性、维护性和容错性都不太好。因此es6可以用let来申明变量,这种方式申明的变量是只能在块作用域里访问,不能跨块访问,也不能跨函数访问。那么我们在使用let的时候,真的就完全知道它怎么用了吗?

引子
看到这样的一个面试题

for(let i = (setTimeout(()=>console.log(i), 2333) , 0); i < 2; i++) {
}

大家猜猜2333毫秒后输出的结果是什么?这里就是“血案”现场了
A类同学:2 ×
B类同学: 0 √
我想A类同学占了大多数,包括我在内

clipboard.png

前期知识点

异步

js中的异步包含以下几种:
1、定时器
2、事件处理函数
3、Promise
4、回调函数
js异步的存在是因为,js是单线程的,如果一些任务需要处理时间比较耗时,那么下面的任务就会一直等这个任务执行完成才能继续,比如一些IO任务,这样就会导致执行效率低效,所以js的设计者意识到了这点,设计了异步执行任务,主线程不必等待异步任务完成才执行下去,这样我们就可以把一些耗时的任务设计成异步任务,将其挂起,让主线程处理完一些比较重要的任务(ui渲染等)后回头再来执行挂起的异步任务。

作用域链

js存在两种类型的作用域,全局作用域和函数作用域。js执行的时候,会创建一个执行上下文(context),并将该执行上下文中的所有变量、函数和函数参数放入一个对象中AO/VO(变量对象|活动对象),并且会保存父级的AO/VO到[[scope]]属性当中。然后在查找变量的时候,会从当前的AO/VO中查找变量,如果没有,就往[[scope]]属性父级VO/AO查找变量,一直到全局的VO中,这样就形成了一个scope chain(作用域链)。通俗点来讲,作用域链就是js在执行的时候用于搜索变量所在的一条链子,所有变量的获取变量会顺着这条链子往上查找,在本作用域内找不到变量的申明,就会往上一级的作用域中查找,直到在全局作用域中还找不到,就找不到该变量了。看下面的例子。

var outer = 1;
function func1() {
var inner1 = 2;
function func2() {
    var inner2 = 3
    console.log(inner2, inner1, outer); // 3 2 1
}
func2()
}

func1();

clipboard.png

1、首先获取inner2,在func2的作用域中(活动对象)找到了inner2的申明,找到了,并且是3;
2、接着获取inner1,发现func2的作用域中没有inner1的申明,那么往创建func2的作用域中查找,即func1中查找inner1的申明,并且为2;
3、接着获取outer,在func2中的作用域中找不到,往作用域链的上一级找,func1中也没有outer的申明,那么就继续往上一级找,在全局作用域中找到了outer,所以是1。
接着我们讲下闭包,所谓闭包用一句话来说就是,函数中的函数,并且里面的函数引用了外面的函数的变量。我们了解了作用域链,那么我们就知道,函数内部是可以访问函数外部的变量的,所以,如果我们在函数中的函数中有访问函数外部变量,且该内部函数被返回的时候就形成了闭包。看下面例子:

function func() {
var name = 'liming';
var sayName = function() {
    console.log(name)
}
return sayName;
}

var sayName = func();
sayName(); // 输出liming

如上面,就是闭包的一个例子,总结开来有两个特点:

1、外部函数包含内部函数,且内部函数访问的外部函数的变量
2、返回内部函数给外部调用
闭包有个缺陷就是容易导致内存泄漏,普通函数调用完后,js引擎就会销毁函数里面的变量,但是闭包的话就不会释放了,所以需要注意点。

解析

选答案A的同学

对于A类同学,答案是错的,但是可以看出A类同学对js的异步和闭包比较熟悉。我们知道setTimeout里面的函数是异步执行的,属于js里面的宏任务(js的异步任务分宏任务和微任务),需要等待js的主线程执行完毕且等到设置的时间后才从宏队列里面取出来执行。所以,等到setTimeout的回调执行的时候,回调函数要获取i的值,这个时候回到函数里面没有i的定义,那么js引擎就会往上一级作用域链中找i,这个时候就找到上一级作用域中的i,A类同学觉得这个时候循环已经结束了(因为for是主线程),那么这个时候的i应该是2了,所以输出的应该++了两次的2。这也就是闭包的知识点,js的设计是,内部可以访问外部,而外部不可以访问内部,所以在setTimeout中的回调中,它可以访问得到外部的i,其实如果把let换成var的话,这个答案就是对的。

关于倒计时,这里有个东西多说一句,就是setInterval的倒计时不是在回调执行完毕后才开始的。这就会导致一种情况,就是如果回调函数里面执行的代码时间比倒计时时间长,那么下次插入队列中的回调就会被取消,也就是倒计时到了以后,这次回调不会执行了,所以建议统一使用setTimeout来代替setInterval。

选择B的同学

选择B的同学,要不就是刚学习js的(也可能是蒙对☺),要不就是对let知识点很熟悉的。

let关键字

let关键字申明的变量具有块级作用域的作用,具有以下特点:
1、不可重复申明同个变量
2、不存在变量提升,所以必须先申明后使用
3、只有块内可见,不会影响块外的变量
其实let还有一个特点,就是在for循环当中,每轮循环都是一个新的值。看下面的的例子:

for(let i = 0; i < 2; i++) {
setTimeout(() => {
    console.log(i); // 分别输出0和1
}, 0)
}

从这个例子可以看出,let变量在for循环中,都会被重新赋值一个新的值,因此上面代码中,for循环中获取的i值都是一个新的,并且这个新i的值是上一次循环的i的值。类似这样的伪代码:

for(var i = 0; i < 2; i++) {
var new_i = i; // 新的i,且新的i应该是和真正的i关联的,比如是new_i_0、new_i_1之类的,这段是伪代码,用来说明,评论的同
学说,let的i是被挟持了,这个解释很赞,所以for中的i其实都是被js引擎挟持了的i,不是我们看到的i
setTimeout(() => {
    console.log(new_i); // 分别输出0和1
}, 0)
}

个人觉得,这个是let的块级作用域相关,每次循环的时候的i都是块级作用域,只对本次循环可见,下次循环不可见。
所以,我们以后如果需要再for循环中获取循环项的时候,可以不用立即执行函数来实现了,可以改为let了。
回到正题。for循环的第一个语句是初始化,这个时候的i就是原本的i,初始化为0,后面的i都是每次循环新生成的i,与初始化的i无关,所以到2333毫秒以后,i的值任然为0,因此打印出来的i就是0了。

for(let i = (setTimeout(()=>console.log(i), 2333) , 0); i < 2; i++) {

}

总结

本篇文章通过一个特殊的面试题,引出了js的异步、作用域链、闭包和let的知识点。
异步包含:
1、定时器
2、事件处理函数
3、Promise
4、回调函数
异步函数的执行时需要主线程空闲的时候执行的,所以我们会把耗时的任务处理为异步。
作用域链:
每个函数执行的时候都会创建一个作用域链的对象,它包含了函数内的所有变量以及创建该函数的函数的所有变量,一直到全局变量,访问变量的时候就会沿着这条链子找。
闭包:
1、外部函数包含内部函数,且内部函数访问的外部函数的变量
2、返回内部函数给外部调用
let:
1、不可重复申明同个变量
2、不存在变量提升,所以必须先申明后使用
3、只有块内可见,不会影响块外的变量
还有在for循环中,每次循环获取let声明的变量都是一个新的变量,而不是初始化时候的那个变量。

相关文章:

  • 个人电脑重装WINDOWN XP 论坛
  • 14 CSS题目附答案
  • 单因素协方差分析
  • 判断某个字符串中是否存在某些字符
  • 阿里云ChatOps实战
  • 一步一步SharePoint 2007之七:改变导航栏中项目的标题和内容
  • 云端一体化差分+安全升级,AliOS Things物联网升级“利器”
  • Pywinauto自动化操作PC微信提取好友微信号
  • BAT都有哪些AIOps的经典案例?
  • 如何查看当前Open的Cursor
  • 基于OHCI的USB主机 —— USB设备获取描述符通用函数
  • Spring Cloud Feign的两种使用姿势
  • Android应用程序安装过程源代码分析(2)
  • 知行不合一,Elon Musk 最大的敌人居然是自己?
  • 第二章 vSphere可用性之准备软硬件环境
  • [LeetCode] Wiggle Sort
  • [原]深入对比数据科学工具箱:Python和R 非结构化数据的结构化
  • 5、React组件事件详解
  • 5分钟即可掌握的前端高效利器:JavaScript 策略模式
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • Angular Elements 及其运作原理
  • CentOS学习笔记 - 12. Nginx搭建Centos7.5远程repo
  • CoolViewPager:即刻刷新,自定义边缘效果颜色,双向自动循环,内置垂直切换效果,想要的都在这里...
  • CSS实用技巧干货
  • JavaScript 一些 DOM 的知识点
  • JWT究竟是什么呢?
  • Lucene解析 - 基本概念
  • Python3爬取英雄联盟英雄皮肤大图
  • Redis 中的布隆过滤器
  • Vultr 教程目录
  • 大整数乘法-表格法
  • 模型微调
  • 使用parted解决大于2T的磁盘分区
  • 听说你叫Java(二)–Servlet请求
  • 小程序开发中的那些坑
  • 学习笔记:对象,原型和继承(1)
  • ​MPV,汽车产品里一个特殊品类的进化过程
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • #HarmonyOS:基础语法
  • #stm32整理(一)flash读写
  • #我与Java虚拟机的故事#连载06:收获颇多的经典之作
  • $.ajax中的eval及dataType
  • (1)SpringCloud 整合Python
  • (30)数组元素和与数字和的绝对差
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (保姆级教程)Mysql中索引、触发器、存储过程、存储函数的概念、作用,以及如何使用索引、存储过程,代码操作演示
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (七)c52学习之旅-中断
  • (四) 虚拟摄像头vivi体验
  • .Net Core和.Net Standard直观理解
  • .NET Micro Framework 4.2 beta 源码探析
  • .net 按比例显示图片的缩略图
  • .net 逐行读取大文本文件_如何使用 Java 灵活读取 Excel 内容 ?
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • .NET企业级应用架构设计系列之结尾篇