分享一个前端面试遇到的代码题,一步步深入分析

528 阅读1分钟

hello大家好,我是disguiseFish~在这篇文章里,我给大家分享一下自己曾经遇到的面试题,这道面试题会一点点变难,你能答对几道题呢?

咱们先简单来看一下下面的代码

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}
console.log(new Date, i);

他先是给了我这几行简单的代码,然后问我打印出来的是什么 其实这么简单的代码看似简单,但它其实能衍生出很多问题

大多数同学应该都是知道这几行代码是做什么的吧? 先是打印最外层的console然后内部循环一秒后打印定时器内的console

因为setTimeout相当于是一个异步处理,所以会先执行外面的for循环,也就是循环了5次后 i等于5。这里用var声明的变量,所以每次i++都会重新赋值给var的第一个变量。

分析到这一步,需要你对js的同步异步的区别以及变量作用域,闭包等概念有正确的理解

这个时候在过了1000ms后会打印5次5,打印出来的时间都是一样的标准时间,除了第一个打印的是等了1秒外,之后的间隔都是可以忽略的;分析到这一步需要你对定时器的工作机制有一定的了解

这里代码的实际输出是:

Mon Apr 19 2021 16:49:53 GMT+0800 (GMT+08:00) 5
(1秒后)
Mon Apr 19 2021 16:49:53 GMT+0800 (GMT+08:00) 5
Mon Apr 19 2021 16:49:53 GMT+0800 (GMT+08:00) 5
Mon Apr 19 2021 16:49:53 GMT+0800 (GMT+08:00) 5
Mon Apr 19 2021 16:49:53 GMT+0800 (GMT+08:00) 5
Mon Apr 19 2021 16:49:53 GMT+0800 (GMT+08:00) 5
 

1 考点 闭包

这个时候 他继续追问我 希望输出变成 501234 ,让我改造一下代码

因为js的执行是个单线程,是通过任务队列(回调函数)实现异步操作的,我首选的是通过闭包的方式把每次for循环的值通过入参的方式传进去再打印

for (var i = 0; i < 5; i++) {
    (function(j) {  // 这里 j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i);

这里先是for和最外层的console都是同步,setTimeout是异步

执行顺序是这样的:

先执行for循环,这里创建了五个入参不一样的定时器排在任务队列里,接着执行最外层的console,最后再依次执行在排队的五个定时器,最后分别打印出这五个不同入参定时器的入参值~~

经过学习补增:

我们在敲代码的时候 经常会用到settimeout这个方法来做异步的处理,我们通常都知道它的两个参数,其实它还有第三个参数,如果这里知道它的第三个参数的用途的话,这边的代码会简洁很多:

for (var i = 0; i < 5; i++) {
    setTimeout(function(j) {
        console.log(new Date, j);
    }, 1000, i);
}

console.log(new Date, i);


// 输出是:
Tue Apr 20 2021 14:51:15 GMT+0800 (中国标准时间) 5
(1s后)
Tue Apr 20 2021 14:51:16 GMT+0800 (中国标准时间) 0
Tue Apr 20 2021 14:51:16 GMT+0800 (中国标准时间) 1
Tue Apr 20 2021 14:51:16 GMT+0800 (中国标准时间) 2
Tue Apr 20 2021 14:51:16 GMT+0800 (中国标准时间) 3
Tue Apr 20 2021 14:51:16 GMT+0800 (中国标准时间) 4

setTimeout的第三个参数是给setTimeout第一个函数的参数

比如:

function sum(x,y,z){
    console.log(x+y+z);
}
setTimeout(sum,1000,1,2,3);
// 输出是:6

还有更直接的做法,对循环体做一下处理,让每次循环都能把i值拿到;我们可以利用js中几本呢类型的参数传递是 按值传递 的特征改造代码:

    var output = function (i) {
        setTimeout(function() {
            console.log(new Date, i);
        }, 1000);
    };

    for (var i = 0; i < 5; i++) {
        output(i);  // 这里传过去的 i 会传到方法内部
    }

    console.log(new Date, i);
//     这里的输出也是我们要的

执行步骤: 先for循环5次,执行了不同入参的方法五次,生成了5个不同入参的定时器,打印最外层的console,然后再执行定时器内的打印就达到了我们要的效果

2 考点 ES6

继续上面的代码题,现在希望代码立即输出0,之后每隔一秒一次输出1234,循环结束后大概在第5s的时候输出5,这个代码改造要不动两处的console

立即输出0 然后每隔1s输出递增的i 这样不难想到可以开个定时器 1000 *i 这样的逻辑

for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000 * j);  // 这里通过循环的i修改 0~4 的定时器间隔时间
    })(i);
}

setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
    console.log(new Date, i);
}, 1000 * i);
 

或者 用setTimeout的第3个参数

     for (var i = 0; i < 5; i++) {
        setTimeout(function(j) {
          console.log(new Date(), j)
        }, 1000 * i, i) // 这里修改 0~4 的定时器时间
      }

      setTimeout(function() { 
        console.log(new Date(), i)
      }, 1000 * i)   

这两部分代码的输出都符合要求

执行顺序: 先for循环,循环5遍生成了五个不同入参(0 1 2 3 4)的定时器在任务序列中排队,for循环结束后i为5,走最下面的定时器生成i为5 的定时器拍在任务序列后面,这个时候再依次执行任务序列中的回调函数。依次打印出来的值为 0 1 2 3 4 5 ,且输出的时间间隔均为1s~~

但是,其实面试官想得到的答案不是这个~~

他想考你es6,看你是否能熟练的使用这些知识,处理一部的代码随处可见,但熟悉和掌握异步操作的流程控制是成为合格开发者的基本素养。所以最好用es6的代码编写:

const tasks = [];
for (var i = 0; i < 5; i++) {   // 这里 i 的声明不能改成 let,如果要改该怎么做?
    ((j) => {
        tasks.push(new Promise((resolve) => {
            setTimeout(() => {
                console.log(new Date, j);
                resolve();  // 这里一定要 resolve,否则代码不会按预期 work
            }, 1000 * j);   // 定时器的超时时间逐步增加
        }));
    })(i);
}

Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);   // 注意这里只需要把超时设置为 1 秒
});

再进一步简化:

const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});

// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}

// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);
});

使用Promis处理异步比回调机制让代码可读性更强,Promise的问题就是要处理好reject,不然会抛出错误~~

3 考点 ES7

接下来我们可以用es7的async/await来让这段代码更简洁,

 const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 声明即执行的 async 函数表达式
    for (var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000);
        }
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();
 

总结

以上的总结补充出处:juejin.cn/post/684490…