中高级前端高频面试题分享(一)

简介: 中高级前端高频面试题分享

使用setTimeout代替setInterval进行间歇调用


var executeTimes = 0;
var intervalTime = 500;
var intervalId = null;
// 放开下面的注释运行setInterval的Demo
intervalId = setInterval(intervalFun,intervalTime);
// 放开下面的注释运行setTimeout的Demo
// setTimeout(timeOutFun,intervalTime);
function intervalFun(){
    executeTimes++;
    console.log("doIntervalFun——"+executeTimes);
    if(executeTimes==5){
        clearInterval(intervalId);
    }
}
function timeOutFun(){
    executeTimes++;
    console.log("doTimeOutFun——"+executeTimes);
    if(executeTimes<5){
        setTimeout(arguments.callee,intervalTime);
    }
}

代码比较简单,我们只是在setTimeout的方法里面又调用了一次setTimeout,就可以达到间歇调用的目的。

重点来了,为什么作者建议我们使用setTimeout代替setInterval呢?setTimeout式的间歇调用和传统的setInterval间歇调用有什么区别呢?

区别在于,setInterval间歇调用,是在前一个方法执行前,就开始计时,比如间歇时间是500ms,那么不管那时候前一个方法是否已经执行完毕,都会把后一个方法放入执行的序列中。这时候就会发生一个问题,假如前一个方法的执行时间超过500ms,加入是1000ms,那么就意味着,前一个方法执行结束后,后一个方法马上就会执行,因为此时间歇时间已经超过500ms了。

var executeTimes = 0;
var intervalTime = 500;
var intervalId = null;
var oriTime = new Date().getTime();
// 放开下面的注释运行setInterval的Demo
// intervalId = setInterval(intervalFun,intervalTime);
// 放开下面的注释运行setTimeout的Demo
setTimeout(timeOutFun,intervalTime);
function intervalFun(){
    executeTimes++;
    var nowExecuteTimes = executeTimes;
    var timeDiff = new Date().getTime() - oriTime;
    console.log("doIntervalFun——"+nowExecuteTimes+", after " + timeDiff + "ms");
    var delayParam = 0;
    sleep(1000);
    console.log("doIntervalFun——"+nowExecuteTimes+" finish !");
    if(executeTimes==5){
        clearInterval(intervalId);
    }
}
function timeOutFun(){
    executeTimes++;
    var nowExecuteTimes = executeTimes;
    var timeDiff = new Date().getTime() - oriTime;
    console.log("doTimeOutFun——"+nowExecuteTimes+", after " + timeDiff + "ms");
    var delayParam = 0;
    sleep(1000);
    console.log("doTimeOutFun——"+nowExecuteTimes+" finish !");
    if(executeTimes<5){
        setTimeout(arguments.callee,intervalTime);
    }
}
function sleep(sleepTime){
    var start=new Date().getTime();
    while(true){
        if(new Date().getTime()-start>sleepTime){
            break;    
        }
    }
}

(这里使用大牛提供的sleep函数来模拟函数运行的时间) 执行setInterval的Demo方法,看控制台

doIntervalFun——1, after 500ms
VM2854:19 doIntervalFun——1 finish !
VM2854:16 doIntervalFun——2, after 1503ms
VM2854:19 doIntervalFun——2 finish !
VM2854:16 doIntervalFun——3, after 2507ms
VM2854:19 doIntervalFun——3 finish !
VM2854:16 doIntervalFun——4, after 3510ms
VM2854:19 doIntervalFun——4 finish !
VM2854:16 doIntervalFun——5, after 4512ms
VM2854:19 doIntervalFun——5 finish !

可以发现,fun2和fun1开始的间歇接近1000ms,刚好就是fun1的执行时间,也就意味着fun1执行完后fun2马上就执行了,和我们间歇调用的初衷背道而驰。

我们注释掉setInterval的Demo方法,放开setTimeout的Demo方法,运行,查看控制台

doTimeOutFun——1, after 500ms
VM2621:32 doTimeOutFun——1 finish !
VM2621:29 doTimeOutFun——2, after 2001ms
VM2621:32 doTimeOutFun——2 finish !
VM2621:29 doTimeOutFun——3, after 3503ms
VM2621:32 doTimeOutFun——3 finish !
VM2621:29 doTimeOutFun——4, after 5004ms
VM2621:32 doTimeOutFun——4 finish !
VM2621:29 doTimeOutFun——5, after 6505ms
VM2621:32 doTimeOutFun——5 finish !

这下终于正常了,fun1和fun2相差了1500ms = 1000 + 500,fun2在fun1执行完的500ms后执行。


闭包


  1. 实现私有变量 如果我们写一个函数,里面有一个name值,我们可以允许任何人访问这个name属性,但是只有少部分人,可以修改这个name属性,我们就可以使用闭包,可以在setName值中,写哪些人具有修改的权限。
var person = function(){   
    //变量作用域为函数内部,外部无法访问,不会与外部变量发生重名冲突   
    var name = "FE";      
    return {
      //管理私有变量   
       getName : function(){   
           return name;   
       },   
       setName : function(newName){   
           name = newName;   
       }   
    }   
};
  1. 数据缓存 假如说我们执行一个计算量很大函数,返回一个值,而这个值在其他函数中还有应用,这种情况下使用闭包,可以将该数据保存在内存中,供其他的函数使用(这是在其他博客中看到的,具体不是很清楚,如果有兴趣,可以自己查阅相关文献)。

缺点: 造成内存消耗过大,如果处理不当,会造成内存泄漏


数组中的forEach和map的区别


大多数情况下,我们都要对数组进行遍历,然后经常用到的两个方法就是forEach和map方法。 先来说说它们的共同点

  • 相同点 都是循环遍历数组中的每一项 forEach和map方法里每次执行匿名函数都支持3个参数,参数分别是item(当前每一项),index(索引值),arr(原数组) 匿名函数中的this都是指向window 只能遍历数组 都不会改变原数组
  • 区别 map方法

1.map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。 2.map方法不会对空数组进行检测,map方法不会改变原始数组。 3.浏览器支持:chrome、Safari1.5+、opera都支持,IE9+,

array.map(function(item,index,arr){},thisValue)
var arr = [0,2,4,6,8];
var str = arr.map(function(item,index,arr){
    console.log(this); //window
    console.log("原数组arr:",arr); //注意这里执行5次
    return item/2;
},this);
console.log(str);//[0,1,2,3,4]

若arr为空数组,则map方法返回的也是一个空数组。

forEach方法

  1. forEach方法用来调用数组的每个元素,将元素传给回调函数 2.forEach对于空数组是不会调用回调函数的。
Array.forEach(function(item,index,arr){},this)
var arr = [0,2,4,6,8];
var sum = 0;
var str = arr.forEach(function(item,index,arr){
    sum += item;
    console.log("sum的值为:",sum); //0 2 6 12 20
    console.log(this); //window
},this)
console.log(sum);//20
console.log(str); //undefined

无论arr是不是空数组,forEach返回的都是undefined。这个方法只是将数组中的每一项作为callback的参数执行一次。


for in和for of的区别


遍历数组通常使用for循环,ES5的话也可以使用forEach,ES5具有遍历数组功能的还有map、filter、some、every、reduce、reduceRight等,只不过他们的返回结果不一样。但是使用foreach遍历数组的话,使用break不能中断循环,使用return也不能返回到外层函数。

Array.prototype.method=function(){
  console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组"
for (var index in myArray) {
  console.log(myArray[index]);
}

使用for in 也可以遍历数组,但是会存在以下问题:

  1. index索引为字符串型数字,不能直接进行几何运算
  2. 遍历顺序有可能不是按照实际数组的内部顺序
  3. 使用for in会遍历数组所有的可枚举属性,包括原型。例如上栗的原型方法method和name属性

所以for in更适合遍历对象,不要使用for in遍历数组。

那么除了使用for循环,如何更简单的正确的遍历数组达到我们的期望呢(即不遍历method和name),ES6中的for of更胜一筹.

Array.prototype.method=function(){
  console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组";
for (var value of myArray) {
  console.log(value);
}

记住,for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值。

for of遍历的只是数组内的元素,而不包括数组的原型属性method和索引name

遍历对象 通常用for in来遍历对象的键名

Object.prototype.method=function(){
  console.log(this);
}
var myObject={
  a:1,
  b:2,
  c:3
}
for (var key in myObject) {
  console.log(key);
}

for in 可以遍历到myObject的原型方法method,如果不想遍历原型方法和属性的话,可以在循环内部判断一下, hasOwnPropery方法可以判断某属性是否是该对象的实例属性

for (var key in myObject) {
  if(myObject.hasOwnProperty(key)){
    console.log(key);
  }
}

同样可以通过ES5的 Object.keys(myObject)获取对象的实例属性组成的数组,不包括原型方法和属性。

Object.prototype.method=function(){
  console.log(this);
}
var myObject={
  a:1,
  b:2,
  c:3
}
Object.keys(myObject).forEach(function(key,index){  
    console.log(key,myObject[key])
})

实现EventEmitter方法


EventEmitter 的核心就是事件触发与事件监听器功能的封装

class EventEmitter {
    constructor() {
        this.events = {};
    }
    on(eventName, fn) {
        let fnList = this.events[eventName] || [];
        fnList.push(fn)
        if (eventName) {
            this.events[eventName] = fnList;
        }
    }
    emit(eventName, ...agr) {
        let funcs = this.events[eventName];
        if (funcs && funcs.length) {
            for (let j = 0; j < funcs.length; j++) {
                funcs[j](...agr);
            }
        }
    }
    off(eventName, fn) {
        let funcs = this.events[eventName];
        if (fn) {
            this.events[eventName].splice(fn, 1);
        } else {
            delete this.events[eventName]
        }
    }
}


let、var、const区别


var 第一个就是作用域的问题,var不是针对一个块级作用域,而是针对一个函数作用域。举个例子:

function runTowerExperiment(tower, startTime) {
  var t = startTime;
  tower.on("tick", function () {
    ... code that uses t ...
  });
  ... more code ...
}

这样是没什么问题的,因为回调函数中可以访问到变量t,但是如果我们在回调函数中再次命名了变量t呢?

function runTowerExperiment(tower, startTime) {
  var t = startTime;
  tower.on("tick", function () {
    ... code that uses t ...
    if (bowlingBall.altitude() <= 0) {
      var t = readTachymeter();
      ...
    }
  });
  ... more code ...
}

后者就会将前者覆盖。

第二个就是循环的问题。 看下面例子:

var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"];
for (var i = 0; i < messages.length; i++) {
    setTimeout(function () {
        document.write(messages[i]);
    },i*1500);
}

输出结果是:undefined 因为for循环后,i置为3,所以访问不到其值。

let 为了解决这些问题,ES6提出了let语法。let可以在{},if,for里声明,其用法同var,但是作用域限定在块级。但是javascript中不是没有块级作用域吗?这个我们等会讲。还有一点很重要的就是let定义的变量不存在变量提升。

变量提升 这里简单提一下什么叫做变量提升。

var v='Hello World'; 
(function(){ 
    alert(v); 
    var v='I love you'; 
})()

上面的代码输出结果为:undefined。

为什么会这样呢?这就是因为变量提升,变量提升就是把变量的声明提升到函数顶部,比如:

  1. 实际上就是:
1. (function(){ 
2.     var a='One'; 
3.     var b='Two'; 
4.     var c='Three'; 
5. })()
(function(){ 
    var a,b,c; 
    a='One'; 
    b='Two'; 
    c='Three'; 
})()

所以我们刚才的例子实际上是:

var v='Hello World'; 
(function(){ 
    var v;
    alert(v); 
    v='I love you'; 
})()

所以就会返回undefined啦。

这也是var的一个问题,而我们使用let就不会出现这个问题。因为它会报语法错误:

{     
    console.log( a );   // undefined
    console.log( b );   // ReferenceError!      
    var a;
    let b;     
}

再来看看let的块级作用域。

function getVal(boo) {
    if (boo) {
        var val = 'red'
        // ...
        return val
    } else {
        // 这里可以访问 val
        return null
    }
    // 这里也可以访问 val
}

而使用let后:

function getVal(boo) {
    if (boo) {
        let val = 'red'
        // ...
        return val
    } else {
        // 这里访问不到 val
        return null
    }
    // 这里也访问不到 val
}

同样的在for循环中:

function func(arr) {
    for (var i = 0; i < arr.length; i++) {
        // i ...
    }
    // 这里访问得到i
}

使用let后:

function func(arr) {
    for (let i = 0; i < arr.length; i++) {
        // i ...
    }
    // 这里访问不到i
}

也就是说,let只能在花括号内部起作用。

const 再来说说const,const代表一个值的常量索引。

const aa = 11;
alert(aa) //11
aa = 22;
alert(aa) //11

但是常量的值在垃圾回收前永远不能改变,所以需要谨慎使用。

还有一条需要注意的就是和其他语言一样,常量的声明必须赋予初值。即使我们想要一个undefined的常量,也需要声明:

const a = undefined;

块级作用域 最后提一下刚才说到的块级作用域。

在之前,javascript是没有块级作用域的,我们都是通过()来模拟块级作用域。

(function(){
 //这里是块级作用域 
})();

但是在ES6中,{}就可以直接代码块级作用域。所以{}内的内容是不可以在{}外访问得到的。

我们可以看看如下代码:

if (true) {
    function foo() {
        document.write( "1" );
    }
}
else {
    function foo() {
        document.write( "2" );
    }
}
foo();      // 2

在我们所认识的javascript里,这段代码的输出结果为2。这个叫做函数声明提升,不仅仅提升了函数名,也提升了函数的定义。如果你基础不扎实的话,可以看看这篇文章:深入理解javascript之IIFE

但是在ES6里,这段代码或抛出ReferenceErroe错误。因为{}的块级作用域,导致外面访问不到foo(),也就是说函数声明和let定义变量一样,都被限制在块级作用域中了。


目录
相关文章
|
4月前
|
缓存 前端开发 中间件
[go 面试] 前端请求到后端API的中间件流程解析
[go 面试] 前端请求到后端API的中间件流程解析
|
1月前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
66 1
|
3月前
|
Web App开发 前端开发 Linux
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
|
4月前
|
存储 XML 移动开发
前端大厂面试真题
前端大厂面试真题
|
2月前
|
Web App开发 JavaScript 前端开发
前端Node.js面试题
前端Node.js面试题
|
4月前
|
存储 前端开发 JavaScript
44 个 React 前端面试问题
【8月更文挑战第18天】
58 2
|
4月前
|
存储 JavaScript 前端开发
2022年前端js面试题
2022年前端js面试题
46 0
|
4月前
|
存储 前端开发 JavaScript
44 个 React 前端面试问题
44 个 React 前端面试问题
|
4月前
|
存储 JavaScript 前端开发
|
4月前
|
Web App开发 存储 缓存