幾道高級前端面試題解析

爲何 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

下面說一下原生解決辦法,以下代碼所示markdown

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,可是這種設計題通常是給出設計思路和部分代碼,不會須要寫出一個無問題的代碼函數

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
複製代碼

最後附上個人公衆號

相關文章
相關標籤/搜索