几道高级前端面试题解析

为何 0.1 + 0.2 != 0.3,请详述理由

由于 JS 采用 IEEE 754 双精度版本(64位),而且只要采用 IEEE 754 的语言都有该问题。node

咱们都知道计算机表示十进制是采用二进制表示的,因此 0.1 在二进制表示为ajax

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
复制代码

那么如何获得这个二进制的呢,咱们能够来演算下promise

小数算二进制和整数不一样。乘法计算时,只计算小数位,整数位用做每一位的二进制,而且获得的第一位为最高位。因此咱们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只须要去掉第一步乘法,因此得出 0.2 = 2^-3 * 1.10011(0011)浏览器

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其他五十二位都为小数位。由于 0.10.2 都是无限循环的二进制了,因此在小数位末尾处须要判断是否进位(就和十进制的四舍五入同样)。缓存

因此 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004bash

下面说一下原生解决办法,以下代码所示多线程

parseFloat((0.1 + 0.2).toFixed(10))
复制代码

10 个 Ajax 同时发起请求,所有返回展现结果,而且至多容许三次失败,说出设计思路

这个问题相信不少人会第一时间想到 Promise.all ,可是这个函数有一个局限在于若是失败一次就返回了,直接这样实现会有点问题,须要变通下。如下是两种实现思路异步

// 如下是不完整代码,着重于思路 非 Promise 写法
let successCount = 0
let errorCount = 0
let datas = []
ajax(url, (res) => {
     if (success) {
         success++
         if (success + errorCount === 10) {
             console.log(datas)
         } else {
             datas.push(res.data)
         }
     } else {
         errorCount++
         if (errorCount > 3) {
            // 失败次数大于3次就应该报错了
             throw Error('失败三次')
         }
     }
})
// Promise 写法
let errorCount = 0
let p = new Promise((resolve, reject) => {
    if (success) {
         resolve(res.data)
     } else {
         errorCount++
         if (errorCount > 3) {
            // 失败次数大于3次就应该报错了
            reject(error)
         } else {
             resolve(error)
         }
     }
})
Promise.all([p]).then(v => {
  console.log(v);
});
复制代码

基于 Localstorage 设计一个 1M 的缓存系统,须要实现缓存淘汰机制

设计思路以下:函数

  • 存储的每一个对象须要添加两个属性:分别是过时时间和存储时间。
  • 利用一个属性保存系统中目前所占空间大小,每次存储都增长该属性。当该属性值大于 1M 时,须要按照时间排序系统中的数据,删除必定量的数据保证可以存储下目前须要存储的数据。
  • 每次取数据时,须要判断该缓存数据是否过时,若是过时就删除。

如下是代码实现,实现了思路,可是可能会存在 Bug,可是这种设计题通常是给出设计思路和部分代码,不会须要写出一个无问题的代码oop

class Store {
  constructor() {
    let store = localStorage.getItem('cache')
    if (!store) {
      store = {
        maxSize: 1024 * 1024,
        size: 0
      }
      this.store = store
    } else {
      this.store = JSON.parse(store)
    }
  }
  set(key, value, expire) {
    this.store[key] = {
      date: Date.now(),
      expire,
      value
    }
    let size = this.sizeOf(JSON.stringify(this.store[key]))
    if (this.store.maxSize < size + this.store.size) {
      console.log('超了-----------');
      var keys = Object.keys(this.store);
      // 时间排序
      keys = keys.sort((a, b) => {
        let item1 = this.store[a], item2 = this.store[b];
        return item2.date - item1.date;
      });
      while (size + this.store.size > this.store.maxSize) {
        let index = keys[keys.length - 1]
        this.store.size -= this.sizeOf(JSON.stringify(this.store[index]))
        delete this.store[index]
      }
    }
    this.store.size += size

    localStorage.setItem('cache', JSON.stringify(this.store))
  }
  get(key) {
    let d = this.store[key]
    if (!d) {
      console.log('找不到该属性');
      return
    }
    if (d.expire > Date.now) {
      console.log('过时删除');
      delete this.store[key]
      localStorage.setItem('cache', JSON.stringify(this.store))
    } else {
      return d.value
    }
  }
  sizeOf(str, charset) {
    var total = 0,
      charCode,
      i,
      len;
    charset = charset ? charset.toLowerCase() : '';
    if (charset === 'utf-16' || charset === 'utf16') {
      for (i = 0, len = str.length; i < len; i++) {
        charCode = str.charCodeAt(i);
        if (charCode <= 0xffff) {
          total += 2;
        } else {
          total += 4;
        }
      }
    } else {
      for (i = 0, len = str.length; i < len; i++) {
        charCode = str.charCodeAt(i);
        if (charCode <= 0x007f) {
          total += 1;
        } else if (charCode <= 0x07ff) {
          total += 2;
        } else if (charCode <= 0xffff) {
          total += 3;
        } else {
          total += 4;
        }
      }
    }
    return total;
  }
}
复制代码

详细说明 Event loop

众所周知 JS 是门非阻塞单线程语言,由于在最初 JS 就是为了和浏览器交互而诞生的。若是 JS 是门多线程的语言话,咱们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另外一个线程中删除节点),固然能够引入读写锁解决这个问题。

JS 在执行的过程当中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。若是遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,因此本质上来讲 JS 中的异步仍是同步行为。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');
复制代码

以上代码虽然 setTimeout 延时为 0,其实仍是异步。这是由于 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增长。因此 setTimeout 仍是会在 script end 以后打印。

不一样的任务源会被分配到不一样的 Task 队列中,任务源能够分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
复制代码

以上代码虽然 setTimeout 写在 Promise 以前,可是由于 Promise 属于微任务而 setTimeout 属于宏任务,因此会有以上的打印。

微任务包括 process.nextTickpromiseObject.observeMutationObserver

宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

不少人有个误区,认为微任务快于宏任务,实际上是错误的。由于宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。

因此正确的一次 Event loop 顺序是这样的

  1. 执行同步代码,这属于宏任务
  2. 执行栈为空,查询是否有微任务须要执行
  3. 执行全部微任务
  4. 必要的话渲染 UI
  5. 而后开始下一轮 Event loop,执行宏任务中的异步代码

经过上述的 Event loop 顺序可知,若是宏任务中的异步代码有大量的计算而且须要操做 DOM 的话,为了更快的 界面响应,咱们能够把操做 DOM 放入微任务中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不相同。

Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
复制代码
timer

timers 阶段会执行 setTimeoutsetInterval

一个 timer 指定的时间并非准确时间,而是在达到这个时间后尽快执行回调,可能会由于系统正在执行别的事务而延迟。

下限的时间有一个范围:[1, 2147483647] ,若是设定的时间不在这个范围,将被设置为1。

I/O

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

idle, prepare

idle, prepare 阶段内部实现

poll

poll 阶段很重要,这一阶段中,系统会作两件事情

  1. 执行到点的定时器
  2. 执行 poll 队列中的事件

而且当 poll 中没有定时器的状况下,会发现如下两件事情

  • 若是 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
  • 若是 poll 队列为空,会有两件事发生
    • 若是有 setImmediate 须要执行,poll 阶段会中止而且进入到 check 阶段执行 setImmediate
    • 若是没有 setImmediate 须要执行,会等待回调被加入到队列中并当即执行回调

若是有别的定时器须要被执行,会回到 timer 阶段执行回调。

check

check 阶段执行 setImmediate

close callbacks

close callbacks 阶段执行 close 事件

而且在 Node 中,有些状况下的定时器执行顺序是随机的

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 由于可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 不然会执行 setTimeout
复制代码

固然在这种状况下,执行顺序是相同的

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});
// 由于 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,因此会当即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 因此以上输出必定是 setImmediate,setTimeout
复制代码

上面介绍的都是 macrotask 的执行状况,microtask 会在以上每一个阶段完成后当即执行。

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印状况是不一样的
// 浏览器中打印 timer1, promise1, timer2, promise2
// node 中打印 timer1, timer2, promise1, promise2
复制代码

Node 中的 process.nextTick 会先于其余 microtask 执行。

setTimeout(() => {
  console.log("timer1");

  Promise.resolve().then(function() {
    console.log("promise1");
  });
}, 0);

process.nextTick(() => {
  console.log("nextTick");
});
// nextTick, timer1, promise1
复制代码

最后附上个人公众号

相关文章
相关标签/搜索