如何理解JavaScript定时器的4种写法-附带面试题讲解

在JavaScript里,我们已经会使用一些原生提供的方法来实现需要延时执行的操作代码,比如很多在线时钟的制作,图片轮播的实现,还有一些广告弹窗,但凡可以自动执行的东西,都是可以和定时器有关的。今天就来和大家分享一下,关于我们在JavaScript里经常会使用到的定时器方法

在JavaScript里,我们要学习四个定时器的使用方法,setTiemout、setInterval、setImmediate、requestAnimationFrame,一起来看看吧!

什么是定时器

JavaScript中提供了一些原生的函数方法来实现延时去执行某一段代码,这个就是定时器

下面我们来认识一下这些定时器

setTimeout:

设置一个定时器,在定时器到期后执行一次函数或代码段

 var timeoutId = window.setTimeout(func[, delay,param1,...]);
 var timeoutId = window.setTimeout(code[, delay]);

上面用到的关键词名称的意义:

timeoutId: 定时器ID

func: 延迟后执行的函数

code: 延迟后执行的代码字符串,不推荐使用原理类似eval()

delay: 延迟的时间(单位:毫秒),默认值为0

param1: 向延迟函数传递而外的参数,IE9以上支持

setInterval:

以固定的时间间隔重复调用一个函数或者代码段

 var intervalId = window.setInterval(func, delay[, param1,...]);
 var intervalId = window.setInterval(code, delay);

intervalId: 重复操作的ID

func: 延迟调用的函数

code: 代码段

delay: 延迟时间,没有默认值

setImmediate:

在浏览器完全结束当前运行的操作之后立即执行指定的函数(仅IE10和Node 0.10+中有实现),类似setTimeout(func, 0)

 var immediateId = setImmediate(func[, param1, param2, ...]);
 var immediateId = setImmediate(func);

immediateId: 定时器ID

func: 回调

requestAnimationFrame:

专门为实现高性能的帧动画而设计的API,但是不能指定延迟时间,而是根据浏览器的刷新频率而定(帧)

 var requestId = window.requestAnimationFrame(func);

func: 回调

举几个栗子加深思考

基本用法

 // 下面代码执行之后会输出什么?
 var intervalId, timeoutId;
 timeoutId = setTimeout(function() {
    console.log(1);
 }, 300);
 setTimeout(function() {
 clearTimeout(timeoutId);
     console.log(2);
 }, 100);
 setTimeout('console.log("5")', 400);
 intervalId = setInterval(function() {
     console.log(4);
 clearInterval(intervalId);
 }, 200);
 // 分别输出: 2、4、5

setInterval 和 setTimeout的区别?

// 执行下面的代码块会输出什么?
 setTimeout(function() {
    console.log('timeout');
 }, 1000);
 setInterval(function() {
    console.log('interval')
 }, 1000);
 // 输出一次 timeout,每隔1S输出一次 interval
 /*--------------------------------*/
 // 通过setTimeout模拟setInterval 和 setInterval有啥区别么?
 varcallback = function() {
 if(times++ > max) {
 clearTimeout(timeoutId);
 clearInterval(intervalId);
 }
     console.log('start', Date.now() - start);
 for(var i = 0; i < 990000000; i++) {}
    console.log('end', Date.now() - start);
 },
 delay = 100,
 times = 0,
 max = 5,
 start = Date.now(),
 intervalId, timeoutId;
 functionimitateInterval(fn, delay) {
    timeoutId = setTimeout(function() {
 fn();
 if(times <= max) {
 imitateInterval(fn ,delay);
 }
 }, delay);
 }
 imitateInterval(callback, delay);
 intervalId = setInterval(callback, delay);

如果是setTimeout和setInterval的话,它俩仅仅在执行次数上有区别,setTimeout一次、setIntervaln次。

而通过setTimeout模拟的setInterval与setInterval的区别则在于:setTimeout只有在回调完成之后才会去调用下一次定时器,而setInterval则不管回调函数的执行情况,当到达规定时间就会在事件队列中插入一个执行回调的事件,所以在选择定时器的方式时需要考虑setInterval的这种特性是否会对你的业务代码有什么影响?

setTimeout(func, 0) 和 setImmediate(func)谁更快?

 console.time('immediate');
 console.time('timeout');
 setImmediate(() => {
     console.timeEnd('immediate');
 });
 setTimeout(() => {
     console.timeEnd('timeout');
 }, 0);

在Node.JS v6.7.0中测试发现setTimeout更早执行

几道经典的定时器面试题

下面代码运行后的结果是什么?

 // 题目一
 var t = true;
 
 setTimeout(function(){
    t = false;
 }, 1000);
 
 while(t){}
 
 alert('end');

 // 题目二
 for(var i = 0; i < 5; i++) {
 setTimeout(function() {
        console.log(i);
 }, 0);
 }
 
 // 题目三
 var obj = {
    msg: 'obj',
 shout: function() {
 alert(this.msg);
 },
 waitAndShout: function() {
 setTimeout(function() {
 this.shout();
 }, 0);
 }
 };
 obj.waitAndShout();

在讲解上面面试题的答案之前,我们先要理解一下定时器的工作原理,以方便理解上面的题目

JS定时器的工作原理

这里将用引用How JavaScript Timers Work中的例子来解释定时器的工作原理,该图为一个简单版的原理图。

如何理解JavaScript定时器的4种写法-附带面试题讲解_第1张图片

在这图中,左侧数字代表时间,单位毫秒;

左侧文字代表某一个操作完成后,浏览器去询问当前队列中存在哪些正在等待执行的操作;

蓝色方块表示正在执行的代码块;

右侧文字代表在代码运行过程中,出现哪些异步事件。

大致流程如下:

1.程序开始时,有一个JS代码块开始执行,执行时长约为18ms,在执行过程中有3个异步事件触发,其中包括一个setTimeout、鼠标点击事件、setInterval

2.第一个setTimeout先运行,延迟时间为10ms,稍后鼠标事件出现,浏览器在事件队列中插入点击的回调函数,稍后setInterval运行,10ms到达之后,setTimeout向事件队列中插入setTimeout的回调

当第一个代码块执行完成后,浏览器查看队列中有哪些事件在等待,他取出排在队列最前面的代码来执行

3.在浏览器处理鼠标点击回调时,setInterval再次检查到到达延迟时间,他将再次向事件队列中插入一个interval的回调,以后每隔指定的延迟时间之后都会向队列中插入一个回调

4.后面浏览器将在执行完当前队头的代码之后,将再次取出目前队头的事件来执行

在这也只是对定时器的工作原理做了简单的叙述,其实实际的实现处理过程会更加复杂。

面试题的答案

在我们理解了定时器的运行原理之后,接下来我们就基于运行原理的基础上,来看看上面的经典面试题的答案

第一题

alert永远都不会执行,因为JS是单线程的,且定时器的回调将在等待当前正在执行的任务完成后才执行,而while(t) {}直接就进入了死循环一直占用线程,不给回调函数执行机会

第二题

代码会输出 5 5 5 5 5,理由同上,当i = 0时,生成一个定时器,将回调插入到事件队列中,等待当前队列中无任务执行时立即执行,而此时for循环正在执行,所以回调被搁置。当for循环执行完成后,队列中存在着5个回调函数,他们的都将执行console.log(i)的操作,因为当前JS代码上中并没有使用块级作用域,所以i的值在for循环结束后一直为5,所以代码将输出5个5

第三题

这个问题涉及到this的指向问题,由setTimeout()调用的代码运行在与所在函数完全分离的执行环境上. 这会导致这些代码中包含的this关键字会指向window (或全局)对象,window对象中并不存在shout方法,所以就会报错,修改方案如下:

 var obj = {
     msg: 'obj',
 shout: function() {
 alert(this.msg);
 },
 waitAndShout: function() {
 var self = this; // 这里将this赋给一个变量
 setTimeout(function() {
            self.shout();
 }, 0);
 }
 };
 obj.waitAndShout();

需要注意的点

1.setTimeout有最小时间间隔限制,HTML5标准为4ms,小于4ms按照4ms处理,但是每个浏览器实现的最小间隔都不同

2.因为JS引擎只有一个线程,所以它将会强制异步事件排队执行

3.如果setInterval的回调执行时间长于指定的延迟,setInterval将无间隔的一个接一个执行

4.this的指向问题可以通过bind函数、定义变量、箭头函数的方式来解决


1.希望获取到页面中所有的checkbox怎么做?(不使用第三方框架)

var domList = document.getElementsByTagName(‘input’)
var checkBoxList = [];
var len = domList.length;  //缓存到局部变量
while (len--) {  //使用while的效率会比for循环更高
  if (domList[len].type == ‘checkbox’) {
      checkBoxList.push(domList[len]);
  }
}

2.当一个DOM节点被点击时候,我们希望能够执行一个函数,应该怎么做?

  1. 直接在DOM里绑定事件:
  2. 在JS里通过onclick绑定:xxx.onclick = test
  3. 通过事件添加进行绑定:addEventListener(xxx, ‘click’, test)

那么问题来了,Javascript的事件流模型都有什么?

  1. “事件冒泡”:事件开始由最具体的元素接受,然后逐级向上传播
  2. “事件捕捉”:事件由最不具体的节点先接收,然后逐级向下,一直到最具体的
  3. “DOM事件流”:三个阶段:事件捕捉,目标阶段,事件冒泡

3. var numberArray = [3,6,2,4,1,5];

1) 实现对该数组的倒排,输出[5,1,4,2,6,3]

2) 实现对该数组的降序排列,输出[6,5,4,3,2,1]

var numberArray = [3,6,2,4,1,5];
numberArray.reverse(); // 5,1,4,2,6,3
numberArray.sort(function(a,b){  //6,5,4,3,2,1
   return b-a;
})

4.输出今天的日期,以YYYY-MM-DD的方式,

比如今天是2020年7月1日,则输出2020-07-01

var d = new Date();
// 获取年,getFullYear()返回4位的数字
var year = d.getFullYear();
// 获取月,月份比较特殊,0是1月,11是12月
var month = d.getMonth() + 1;
// 变成两位
month = month < 10 ? '0' + month : month;
// 获取日
var day = d.getDate();
day = day < 10 ? '0' + day : day;
alert(year + '-' + month + '-' + day);

5.用js实现随机选取10--100之间的10个数字,存入一个数组,并排序。

var iArray = [];
funtion getRandom(istart, iend){
        var iChoice = iend - istart +1;
        return Math.floor(Math.random() * iChoice + istart);
}
for(var i=0; i<10; i++){
        iArray.push(getRandom(10,100));
}
iArray.sort();

6. 如何消除一个数组里面重复的元素?

var arr = [1, 2, 3, 3, 4, 4, 5, 5, 6, 1, 9, 3, 25, 4];
 
function deRepeat() {
    var newArr = [];
    var obj = {};
    var index = 0;
    var l = arr.length;
    for (var i = 0; i < l; i++) {
        if (obj[arr[i]] == undefined) {
            obj[arr[i]] = 1;
            newArr[index++] = arr[i];
        } else if (obj[arr[i]] == 1)
            continue;
    }
    return newArr;
 
}
var newArr2 = deRepeat(arr);
alert(newArr2); //输出1,2,3,4,5,6,9,25

7.下面这个ul,如何点击每一列的时候alert其index?

  • 这是第一条
  • 这是第二条
  • 这是第三条
答案
// 方法一:
var lis=document.getElementById('2223').getElementsByTagName('li');
for(var i=0;i<3;i++)
{
    lis[i].index=i;
    lis[i].onclick=function(){
        alert(this.index);
    };
}
 
//方法二:
var lis=document.getElementById('2223').getElementsByTagName('li');
for(var i=0;i<3;i++)
{
    lis[i].index=i;
    lis[i].onclick=(function(a){
        return function() {
            alert(a);
        }
    })(i);
}

8.定义一个log方法,让它可以代理console.log的方法。

可行的方法一:

function log(msg)  {
    console.log(msg);
}
log("hello world!") // hello world!

如果要传入多个参数呢?显然上面的方法不能满足要求,所以更好的方法是:

function log() {
    console.log.apply(console, arguments);
};

9.在Javascript中什么是伪数组?如何将伪数组转化为标准数组?

答案:

伪数组(类数组):无法直接调用数组方法或期望length属性有什么特殊的行为,但仍可以对真正数组遍历方法来遍历它们。典型的是函数的argument参数,还有像调用getElementsByTagName,document.childNodes之类的,它们都返回NodeList对象都属于伪数组。可以使用
Array.prototype.slice.call(fakeArray)将数组转化为真正的Array对象。

10.对作用域上下文和this的理解,看下列代码:

var User = {
    count: 1,
 
    getCount: function() {
        return this.count;
    }
};
console.log(User.getCount()); // what?
var func = User.getCount;
console.log(func()); // what?

问两处 console 输出什么?为什么?

答案是 1 和 undefined。

func 是在 winodw 的上下文中被执行的,所以会访问不到 count 属性。


1. 实现一个call函数

// 思路:将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.mycall = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }
  context = context || window
  context.fn = this
  let arg = [...arguments].slice(1)
  let result = context.fn(...arg)
  delete context.fn
  return result
} 

2. 实现一个apply函数

// 思路:将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myapply = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }
  context = context || window
  context.fn = this
  let result
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

3. 实现一个bind函数

// 思路:类似call,但返回的是函数
Function.prototype.mybind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  let _this = this
  let arg = [...arguments].slice(1)
  return function F() {
    // 处理函数使用new的情况
    if (this instanceof F) {
      return new _this(...arg, ...arguments)
    } else {
      return _this.apply(context, arg.concat(...arguments))
    }
  }
}

4. new本质

function myNew (fun) {
  return function () {
    // 创建一个新对象且将其隐式原型指向构造函数原型
    let obj = {
      __proto__ : fun.prototype
    }
    // 执行构造函数
    fun.call(obj, ...arguments)
    // 返回该对象
    return obj
  }
}
function person(name, age) {
  this.name = name
  this.age = age
}
let obj = myNew(person)('chen', 18) 
// {name: "chen", age: 18}

5. Object.create的基本实现原理

// 思路:将传入的对象作为原型
function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

6. instanceof的原理

// 思路:右边变量的原型存在于左边变量的原型链上
function instanceOf(left, right) {
  let leftValue = left.__proto__
  let rightValue = right.prototype
  while (true) {
    if (leftValue === null) {
      return false
    }
    if (leftValue === rightValue) {
      return true
    }
    leftValue = leftValue.__proto__
  }
}

7. 实现一个基本的Promise

// 未添加异步处理等其他边界情况
// ①自动执行函数,②三个状态,③then
class Promise {
  constructor (fn) {
    // 三个状态
    this.state = 'pending'
    this.value = undefined
    this.reason = undefined
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
      }
    }
    let reject = value => {
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = value
      }
    }
    // 自动执行函数
    try {
      fn(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  // then
  then(onFulfilled, onRejected) {
    switch (this.state) {
      case 'fulfilled':
        onFulfilled()
        break
      case 'rejected':
        onRejected()
        break
      default:
    }
  }
}

8. 实现浅拷贝

// 1. ...实现
let copy1 = {...{x:1}}
// 2. Object.assign实现
let copy2 = Object.assign({}, {x:1})

9. 使用setTimeout模拟setInterval

// 可避免setInterval因执行时间导致的间隔执行时间不一致
setTimeout (function () {
  // do something
  setTimeout (arguments.callee, 500)
}, 500)

10. js实现一个继承方法

// 借用构造函数继承实例属性
function Child () {
  Parent.call(this)
}
// 寄生继承原型属性
(function () {
  let Super = function () {}
  Super.prototype = Parent.prototype
  Child.prototype = new Super()
})()

11. 实现一个基本的深拷贝

// 1. JOSN.stringify()/JSON.parse()
let obj = {a: 1, b: {x: 3}}
JSON.parse(JSON.stringify(obj))
// 2. 递归拷贝
function deepClone(obj) {
 let copy = obj instanceof Array ? [] : {}
 for (let i in obj) {
  if (obj.hasOwnProperty(i)) {
  copy[i] = typeof obj[i] === 'object'?deepClone(obj[i]):obj[i]
    }
  }
  return copy
}

12. 实现一个基本的Event Bus

// 组件通信,一个触发与监听的过程
class EventEmitter {
  constructor () {
    // 存储事件
    this.events = this.events || new Map()
  }
  // 监听事件
  addListener (type, fn) {
    if (!this.events.get(type)) {
      this.events.set(type, fn)
    }
  }
  // 触发事件
  emit (type) {
    let handle = this.events.get(type)
    handle.apply(this, [...arguments].slice(1))
  }
}
// 测试
let emitter = new EventEmitter()
// 监听事件
emitter.addListener('ages', age => {
  console.log(age)
})
// 触发事件
emitter.emit('ages', 18)  // 18

13. 实现一个双向数据绑定

let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {
    console.log('获取数据了')
  },
  set(newVal) {
    console.log('数据更新了')
    input.value = newVal
    span.innerHTML = newVal
  }
})
// 输入监听
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
})

14. 实现一个简单路由

// hash路由
class Route{
  constructor(){
    // 路由存储对象
    this.routes = {}
    // 当前hash
    this.currentHash = ''
    // 绑定this,避免监听时this指向改变
    this.freshRoute = this.freshRoute.bind(this)
    // 监听
    window.addEventListener('load', this.freshRoute, false)
    window.addEventListener('hashchange', this.freshRoute, false)
  }
  // 存储
  storeRoute (path, cb) {
    this.routes[path] = cb || function () {}
  }
  // 更新
  freshRoute () {
    this.currentHash = location.hash.slice(1) || '/'
    this.routes[this.currentHash]()
  }   
}

15. 实现一个节流函数

// 思路:在规定时间内只触发一次
function throttle (fn, delay) {
  // 利用闭包保存时间
  let prev = Date.now()
  return function () {
    let context = this
    let arg = arguments
    let now = Date.now()
    if (now - prev >= delay) {
      fn.apply(context, arg)
      prev = Date.now()
    }
  }
}
function fn () {
  console.log('节流')
}
addEventListener('scroll', throttle(fn, 1000)) 

16. 实现一个防抖函数

// 思路:在规定时间内未触发第二次,则执行
function debounce (fn, delay) {
  // 利用闭包保存定时器
  let timer = null
  return function () {
    let context = this
    let arg = arguments
    // 在规定时间内再次触发会先清除定时器后再重设定时器
    clearTimeout(timer)
    timer = setTimeout(function () {
      fn.apply(context, arg)
    }, delay)
  }
}
function fn () {
  console.log('防抖')
}
addEventListener('scroll', debounce(fn, 1000)) 

 问题1:Scope作用范围

考虑下面的代码:

(function() {   var a = b = 5;})();
console.log(b);

什么会被打印在控制台上?
回答
上面的代码会打印 5。
这个问题的诀窍是,这里有两个变量声明,但 a 使用关键字var声明的。代表它是一个函数的局部变量。与此相反,b 变成了全局变量。
这个问题的另一个诀窍是,它没有使用严格模式 ('use strict';) 。如果启用了严格模式,代码就会引发ReferenceError的错误:B没有定义(b is not defined)。请记住,严格模式,则需要明确指定,才能实现全局变量声明。比如,你应该写:

(function() {  
  'use strict'; 
  var a = window.b = 5;
})();
console.log(b);

问题2:创建“原生”(native)方法

给字符串对象定义一个repeatify功能。当传入一个整数n时,它会返回重复n次字符串的结果。例如:

console.log('hello'.repeatify(3));

应打印 hellohellohello。

回答
一个可能的实现如下所示:

String.prototype.repeatify = String.prototype.repeatify || function(times) {  
  var str = '';  
  for (var i = 0; i < times; i++) {   
    str += this;  
  }  
  return str
  ;};

现在的问题测试开发者有关JavaScript继承和prototype的知识点。这也验证了开发者是否知道该如何扩展内置对象(尽管这不应该做的)。
这里的另一个要点是,你要知道如何不覆盖可能已经定义的功能。通过测试一下该功能定义之前并不存在:

String.prototype.repeatify = String.prototype.repeatify || function(times) {/* code here */};

当你被要求做好JavaScript函数兼容时这种技术特别有用。

问题3:this在JavaScript中如何工作的

下面的代码会输出什么结果?给出你的答案。

var fullname = 'John Doe';var obj = {  
  fullname: 'Colin Ihrig',   prop: {    
    fullname: 'Aurelio De Rosa',    
    getFullname: function() {   
      return this.fullname;    
    }   
  }};
console.log(obj.prop.getFullname());
var test = obj.prop.getFullname; 
console.log(test());

回答
答案是Aurelio De Rosa和John Doe。

原因是,在一个函数中,this的行为,取决于JavaScript函数的调用方式和定义方式,而不仅仅是看它如何被定义的。
在第一个 console.log()调用中,getFullname() 被调用作为obj.prop对象的函数。

所以,上下文指的是后者,函数返回该对象的fullname。

与此相反,当getFullname()被分配到test变量时,上下文指的是全局对象(window)。

这是因为test是被隐式设置为全局对象的属性。

出于这个原因,该函数返回window的fullname,即定义在第一行的那个值。

问题4:声明提升(Hoisting)

执行这段代码,输出什么结果。

function test() {  
  console.log(a);  
  console.log(foo());  
  var a = 1;   
  function foo() {
    return 2;   
  }}
test();

回答
这段代码的结果是 undefined 和 2。
原因是,变量和函数的声明都被提前了(移到了函数的顶部),但变量不分配任何值。因此,在打印变量的时候,它在函数中存在(它被声明了),但它仍然是 undefined 。表示换句话说,上面的代码等同于以下内容:

function test() {
  var a;   
  function foo() { 
    return 2;   
  }    
  console.log(a); 
  console.log(foo());  
  a = 1;
} 
test();

问题5:call() 和 apply()

现在让你解决前一个问题,使最后的console.log() 打印 Aurelio De Rosa。
回答
该问题可以通过强制使用 call() 或者 apply() 改变函数上下文。在下面我将使用call(),但在这种情况下,apply()会输出相同的结果:

console.log(test.call(obj.prop));

你可能感兴趣的:(JavaScript,程序猿面试题,面试题,javascript,定时器)