5道JavaScript面试题解析,理解变量提升、闭包、事件循环和微任务

问题1:下面这段代码,浏览器控制台上会打印什么?

var a = 10;
function foo(){
  console.log(a);
  var a = 20;
}
foo();

答案:undefind
解析:
使用var关键字声明的变量在JavaScript中会被提升,并在内存中开辟空间,由于没有赋值,无法定义数值类型,所以分配默认值undefined。var声明的变量,真正的数值初始化,是发生在确定赋值的位置。同时,var声明的变量是函数作用域的,也就是需要区分局部变量和全局变量,而let和const是块作用域的。所以这道题的运行过程是这样的:

var a = 10; // 全局作用域,全局变量。a=10
function foo() {
// var a 
//的声明将被提升到到函数的顶部。
// 比如:var a
console.log(a); // 打印 undefined
// 实际初始化值20只发生在这里
   var a = 20; // local scope
}

问题2:如果我们使用 let 或 const 代替 var,输出是?

var a = 10;
function foo(){
  console.log(a);
  let a = 20;
}
foo();

答案:ReferenceError:a undefined
解析:
let和const声明可以让变量在其作用域上受限于它所在的块、语句或表达式中。和var不同的地方在于,这两个声明的变量,不会被提升。并且会有一个暂时死区(TDZ)。如果访问TDZ中的变量的话,就会报ReferenceError,因为他们的作用域是在他们声明的位置的,不会有提升。所以必须在执行到声明的位置才能访问。

var a = 10; // 全局使用域
function foo() { // TDZ 开始
// 创建了未初始化的'a'
   console.log(a); // ReferenceError
// TDZ结束,'a'仅在此处初始化,值为20
   let a = 20;
}

问题3:"newArray"中有哪些元素?

var array = [];
for (var i = 0; i < 3; i++) {
    array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray);

答案:[3, 3, 3]
解释:
这个问题,是循环结构会给大家带来一种块级作用域的误区,在for的循环的头部使用var声明的变量,就是单个声明的变量绑定(单个存储空间)。在循环过程中,这个var声明的i变量是会随循环变化的。但是在循环中执行的数组push方法,最后实际上是push了i最终循环结束的3这个值。所以最后push进去的全都是3。

// 误解作用域:认为存在块级作用域,实际上和i是同一个作用域
var array = [];
for (var i = 0; i < 3; i++) {
    // 三个箭头函数体中的每个'i'都指向相同的绑定,
    // 这就是为什么它们在循环结束时返回相同的值'3'。
    array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]

扩展:
如何使得输出正常[1,2,3]?
1)使用闭包将i暂留在内存中
2) 使用let修饰i,每个循环一个新的i绑定,并保留当前的值

// 使用ES6块级作用域 let修饰 i
var array = [];
for (let i = 0; i < 3; i++) {
    array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]

// 使用闭包
let array = [];
for (var i = 0; i < 3; i++) {
    array[i] = (function(x) {
     return function() {
           return x;
          };
    })(i);
}
const newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]

问题4:如果我们在浏览器控制台中运行’foo’函数,是否会导致堆栈溢出错误?

function foo(){
	setTimeout(foo,0);
}

答案:不会
解析:
JavaScript的并发模式基于我们常说的”事件循环“。
浏览器是提供运行时环境来给我们执行JS代码的。浏览器的主要组成包括有调用堆栈,事件循环,任务队列和WEB API。像什么常用的定时器setTimeout,setInterval这些全局函数就不是JavaScript的一部分,而是WEB API给我们提供的。

JS调用栈是后进先出(LIFO)的。引擎每次从堆栈中取出一个函数,然后从上到下依次运行代码。每当它遇到一些异步代码,如setTimeout,它就把它交给Web API(箭头1)。因此,每当事件被触发时,callback 都会被发送到任务队列(箭头2)。
事件循环(Event loop)不断地监视任务队列(Task Queue),并按它们排队的顺序一次处理一个回调。每当调用堆栈(call stack)为空时,Event loop获取回调并将其放入堆栈(stack )(箭头3)中进行处理。请记住,如果调用堆栈不是空的,则事件循环不会将任何回调推入堆栈。
如此一来,则此题的执行顺序如下

  1. 调用 foo()会将foo函数放入调用堆栈(call stack)。
  2. 在处理内部代码时,JS引擎遇到setTimeout。
  3. 然后将foo回调函数传递给WebAPIs(箭头1)并从函数返回,调用堆栈再次为空
  4. 计时器被设置为0,因此foo将被发送到任务队列(箭头2)。
  5. 由于调用堆栈是空的,事件循环将选择foo回调并将其推入调用堆栈进行处理。
  6. 进程再次重复,堆栈不会溢出。

问题5: 如果在控制台中运行以下函数,页面(选项卡) 是否会有响应

function foo(){
	return Promise.resolve().then(foo());
}

答案:不会
在很多时候,很多做前端开发的人都是认为循环事件图中就只会有一个任务列表。但事实上不是这样的,是可以有多个任务列表的。由浏览器选择其中一个队列并在该队列进行处理回调。
从底层来看,JavaScript中是可以有宏认为和微任务的,比如说setTimeout回调是宏任务,而Promise回调是微任务。
他们有什么区别呢?
主要的区别在于执行方式。宏任务在单个循环周期中一次一个低堆入堆栈,但是微任务队列总是在执行后返回到事件之前清空。所以,如果以处理条目的速度向这个队列添加条目,那么就永远在处理微任务。只有当微任务队列为空时,事件循环才会重新渲染页面。

这段代码,每次去调用【foo】的时候,都会在微任务队列上加另一个【foo】的回调,因此事件循环没办法继续去处理其他的事件了(比如说滚动,点击事件等等),直到该队列完全清空位置。因此,不会执行渲染,会被阻止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值