幾道高階前端面試題解析

yck發表於2018-04-26
幾道高階前端面試題解析

為什麼 0.1 + 0.2 != 0.3,請詳述理由

因為 JS 採用 IEEE 754 雙精度版本(64位),並且只要採用 IEEE 754 的語言都有該問題。

我們都知道計算機表示十進位制是採用二進位制表示的,所以 0.1 在二進位制表示為

// (0011) 表示迴圈
0.1 = 2^-4 * 1.10011(0011)
複製程式碼

那麼如何得到這個二進位制的呢,我們可以來演算下

幾道高階前端面試題解析

小數算二進位制和整數不同。乘法計算時,只計算小數位,整數位用作每一位的二進位制,並且得到的第一位為最高位。所以我們得出 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.30000000000000004

下面說一下原生解決辦法,如下程式碼所示

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
複製程式碼

最後附上我的公眾號

幾道高階前端面試題解析

相關文章