引言
半月刊第三期到來,這段時間 Daily-Interview-Question 新增了 15 道高頻面試題,今天就把最近半月彙總的面試題和部分答案發給大家,幫助大家查漏補缺。
更多更全的面試題和答案在下面的專案地址中,點選檢視。
第 25 題:說說瀏覽器和 Node 事件迴圈的區別
瀏覽器
關於微任務和巨集任務在瀏覽器的執行順序是這樣的:
- 執行一個task(巨集任務)
- 執行完micro-task佇列 (微任務)
如此迴圈往復下去
瀏覽器的 task(巨集任務)執行順序在 html#event-loops 裡面有講就不翻譯了 常見的 task(巨集任務) 比如:setTimeout、setInterval、script(整體程式碼)、 I/O 操作、UI 渲染等。 常見的 micro-task 比如: new Promise().then(回撥)、MutationObserver(html5新特性) 等。
Node
Node的事件迴圈是libuv實現的,引用一張官網的圖:
大體的task(巨集任務)執行順序是這樣的:
- timers定時器:本階段執行已經安排的 setTimeout() 和 setInterval() 的回撥函式。
- pending callbacks待定回撥:執行延遲到下一個迴圈迭代的 I/O 回撥。
- idle, prepare:僅系統內部使用。
- poll 輪詢:檢索新的 I/O 事件;執行與 I/O 相關的回撥(幾乎所有情況下,除了關閉的回撥函式,它們由計時器和 setImmediate() 排定的之外),其餘情況 node 將在此處阻塞。
- check 檢測:setImmediate() 回撥函式在這裡執行。
- close callbacks 關閉的回撥函式:一些準備關閉的回撥函式,如:socket.on('close', ...)。
微任務和巨集任務在Node的執行順序
Node 10以前:
- 執行完一個階段的所有任務
- 執行完nextTick佇列裡面的內容
- 然後執行完微任務佇列的內容
Node 11以後: 和瀏覽器的行為統一了,都是每執行一個巨集任務就執行完微任務佇列。
未完待續,點選檢視更多細節:第25題:瀏覽器和Node 事件迴圈的區別
第 26 題:介紹模組化發展歷程
可從IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module">
這幾個角度考慮。
解答:
模組化主要是用來抽離公共程式碼,隔離作用域,避免變數衝突等。
IIFE: 使用自執行函式來編寫模組化,特點:在一個單獨的函式作用域中執行程式碼,避免變數衝突。
(function(){
return {
data:[]
}
})()
複製程式碼
AMD: 使用requireJS 來編寫模組化,特點:依賴必須提前宣告好。
define('./index.js',function(code){
// code 就是index.js 返回的內容
})
複製程式碼
CMD: 使用seaJS 來編寫模組化,特點:支援動態引入依賴檔案。
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
複製程式碼
CommonJS: nodejs 中自帶的模組化。
var fs = require('fs');
複製程式碼
UMD:相容AMD,CommonJS 模組化語法。
webpack(require.ensure):webpack 2.x 版本中的程式碼分割。
ES Modules: ES6 引入的模組化,支援import 來引入另一個 js 。
import a from 'a';
複製程式碼
未完待續,點選檢視更多細節:第26 題: 前端中的模組化開發
第 27 題:全域性作用域中,用 const 和 let 宣告的變數不在 window 上,那到底在哪裡?如何去獲取?。
在ES5中,頂層物件的屬性和全域性變數是等價的,var 命令和 function 命令宣告的全域性變數,自然也是頂層物件。
var a = 12;
function f(){};
console.log(window.a); // 12
console.log(window.f); // f(){}
複製程式碼
但ES6規定,var 命令和 function 命令宣告的全域性變數,依舊是頂層物件的屬性,但 let命令、const命令、class命令宣告的全域性變數,不屬於頂層物件的屬性。
let aa = 1;
const bb = 2;
console.log(window.aa); // undefined
console.log(window.bb); // undefined
複製程式碼
在哪裡?怎麼獲取?通過在設定斷點,看看瀏覽器是怎麼處理的:
通過上圖也可以看到,在全域性作用域中,用 let 和 const 宣告的全域性變數並沒有在全域性物件中,只是一個塊級作用域(Script)中
怎麼獲取?在定義變數的塊級作用域中就能獲取啊,既然不屬於頂層物件,那就不加 window(global)唄。
let aa = 1;
const bb = 2;
console.log(aa); // 1
console.log(bb); // 2
複製程式碼
未完待續,點選檢視更多細節:第27題:關於 const 和 let 宣告的變數不在 window 上
第 28 題:cookie 和 token 都存放在 header 中,為什麼不會劫持 token?
1、首先token不是防止XSS的,而是為了防止CSRF的; 2、CSRF攻擊的原因是瀏覽器會自動帶上cookie,而瀏覽器不會自動帶上token
未完待續,點選檢視更多細節:第28題:cookie 和 token 都存放在 header 中,為什麼不會劫持 token?
第 29 題:聊聊 Vue 的雙向資料繫結,Model 如何改變 View,View 又是如何改變 Model 的
VM 主要做了兩件微小的事情:
- 從 M 到 V 的對映(Data Binding),這樣可以大量節省你人肉來 update View 的程式碼
- 從 V 到 M 的事件監聽(DOM Listeners),這樣你的 Model 會隨著 View 觸發事件而改變
1、M 到 V 實現
做到這件事的第一步是形成類似於:
// template
var tpl = '<p>{{ text }}</p>';
// data
var data = {
text: 'This is some text'
};
// magic process
template(tpl, data); // '<p>This is some text</p>'
複製程式碼
中間的 magic process 是模板引擎所做的事情,已經有非常多種模板引擎可供選擇
當然你比較喜歡造輪子的話也可以自己實現一個
無論是 Angular 的 $scope,React 的 state 還是 Vue 的 data 都提供了一個較為核心的 model 物件用來儲存模型的狀態;它們的模板引擎稍有差別,不過大體思路相似;拿到渲染後的 string 接下來做什麼不言而喻了(中間還有很多處理,例如利用 model 的 diff 來最小量更新 view )。
但是僅僅是這樣並不夠,我們需要知道什麼時候來更新 view( 即 render ),一般來說主要的 VM 做了以下幾種選擇:
- VM 例項初始化時
- model 動態修改時
其中初始化拿到 model 物件然後 render 沒什麼好講的;model 被修改的時候如何監聽屬性的改變是一個問題,目前有以下幾種思路:
- 藉助於 Object 的 observe 方法
- 自己在 set,以及陣列的常用操作裡觸發 change 事件
- 手動 setState(),然後在裡面觸發 change 事件
知道了觸發 render 的時機以及如何 render,一個簡單的 M 到 V 對映就實現了。
2、V 到 M 實現
從 V 到 M 主要由兩類( 雖然本質上都是監聽 DOM )構成,一類是使用者自定義的 listener, 一類是 VM 自動處理的含有 value 屬性元素的 listener
第一類類似於你在 Vue 裡用 v-on 時繫結的那樣,VM 在例項化得時候可以將所有使用者自定義的 listener 一次性代理到根元素上,這些 listener 可以訪問到你的 model 物件,這樣你就可以在 listener 中改變 model
第二類類似於對含有 v-model 與 value 元素的自動處理,我們期望的是例如在一個輸入框內
<input type="text" v-model="message" />
複製程式碼
輸入值,那麼我與之對應的 model 屬性 message 也會隨之改變,相當於 VM 做了一個預設的 listener,它會監聽這些元素的改變然後自動改變 model,具體如何實現相信你也明白了
未完待續,點選檢視更多細節:第29題
第 30 題:兩個陣列合併成一個陣列
請把兩個陣列 ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2'] 和 ['A', 'B', 'C', 'D'],合併為 ['A1', 'A2', 'A', 'B1', 'B2', 'B', 'C1', 'C2', 'C', 'D1', 'D2', 'D']。
解法:
let a1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
let a2 = ['A', 'B', 'C', 'D'].map((item) => {
return item + 3
})
let a3 = [...a1, ...a2].sort().map((item) => {
if(item.includes('3')){
return item.split('')[0]
}
return item
})
複製程式碼
未完待續,點選檢視更多細節: 第 30 題
第 31 題:改造下面的程式碼,使之輸出0 - 9,寫出你能想到的所有解法。
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
複製程式碼
解法1:
for (var i = 0; i< 10; i++){
setTimeout((i) => {
console.log(i);
}, 1000,i)
}
複製程式碼
解法2:
for (var i = 0; i< 10; i++){
((i) => {
setTimeout(() => {
console.log(i);
}, 1000)
})(i)
}
複製程式碼
解法3:
for (let i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
複製程式碼
未完待續,點選檢視更多細節:第 31 題
第 32 題:Virtual DOM 真的比操作原生 DOM 快嗎?談談你的想法。
採用尤大大的回答:
1. 原生 DOM 操作 vs. 通過框架封裝操作。
這是一個效能 vs. 可維護性的取捨。框架的意義在於為你掩蓋底層的 DOM 操作,讓你用更宣告式的方式來描述你的目的,從而讓你的程式碼更容易維護。沒有任何框架可以比純手動的優化 DOM 操作更快,因為框架的 DOM 操作層需要應對任何上層 API 可能產生的操作,它的實現必須是普適的。針對任何一個 benchmark,我都可以寫出比任何框架更快的手動優化,但是那有什麼意義呢?在構建一個實際應用的時候,你難道為每一個地方都去做手動優化嗎?出於可維護性的考慮,這顯然不可能。框架給你的保證是,你在不需要手動優化的情況下,我依然可以給你提供過得去的效能。
2. 對 React 的 Virtual DOM 的誤解。
React 從來沒有說過 “React 比原生操作 DOM 快”。React 的基本思維模式是每次有變動就整個重新渲染整個應用。如果沒有 Virtual DOM,簡單來想就是直接重置 innerHTML。很多人都沒有意識到,在一個大型列表所有資料都變了的情況下,重置 innerHTML 其實是一個還算合理的操作... 真正的問題是在 “全部重新渲染” 的思維模式下,即使只有一行資料變了,它也需要重置整個 innerHTML,這時候顯然就有大量的浪費。
我們可以比較一下 innerHTML vs. Virtual DOM 的重繪效能消耗:
- innerHTML: render html string O(template size) + 重新建立所有 DOM 元素 O(DOM size)
- Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
Virtual DOM render + diff 顯然比渲染 html 字串要慢,但是!它依然是純 js 層面的計算,比起後面的 DOM 操作來說,依然便宜了太多。可以看到,innerHTML 的總計算量不管是 js 計算還是 DOM 操作都是和整個介面的大小相關,但 Virtual DOM 的計算量裡面,只有 js 計算和介面大小相關,DOM 操作是和資料的變動量相關的。前面說了,和 DOM 操作比起來,js 計算是極其便宜的。這才是為什麼要有 Virtual DOM:它保證了 1)不管你的資料變化多少,每次重繪的效能都可以接受;2) 你依然可以用類似 innerHTML 的思路去寫你的應用。
3. 效能比較也要看場合
在比較效能的時候,要分清楚初始渲染、小量資料更新、大量資料更新這些不同的場合。Virtual DOM、髒檢查 MVVM、資料收集 MVVM 在不同場合各有不同的表現和不同的優化需求。Virtual DOM 為了提升小量資料更新時的效能,也需要針對性的優化,比如 shouldComponentUpdate 或是 immutable data。
- 初始渲染:Virtual DOM > 髒檢查 >= 依賴收集
- 小量資料更新:依賴收集 >> Virtual DOM + 優化 > 髒檢查(無法優化) > Virtual DOM 無優化
- 大量資料更新:髒檢查 + 優化 >= 依賴收集 + 優化 > Virtual DOM(無法/無需優化)>> MVVM 無優化
不要天真地以為 Virtual DOM 就是快,diff 不是免費的,batching 麼 MVVM 也能做,而且最終 patch 的時候還不是要用原生 API。在我看來 Virtual DOM 真正的價值從來都不是效能,而是它 1) 為函式式的 UI 程式設計方式開啟了大門;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。
未完待續,點選檢視更多細節:第 32 題
第 33 題:下面的程式碼列印什麼內容,為什麼?
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
複製程式碼
解答:
- 函式表示式與函式宣告不同,函式名只在該函式內部有效,並且此繫結是常量繫結。
- 對於一個常量進行賦值,在 strict 模式下會報錯,非 strict 模式下靜默失敗。
- IIFE中的函式是函式表示式,而不是函式宣告。
實際上,有點類似於以下程式碼,但不完全相同,因為使用const不管在什麼模式下,都會TypeError型別的錯誤
const foo = function () {
foo = 10;
console.log(foo)
}
(foo)() // Uncaught TypeError: Assignment to constant variable.
複製程式碼
b 函式是一個相當於用const定義的常量,內部無法進行重新賦值,如果在嚴格模式下,會報錯"Uncaught TypeError: Assignment to constant variable." 例如下面的:
var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."
複製程式碼
未完待續,點選檢視更多細節:第 33 題
第 34 題:簡單改造下面的程式碼,使之分別列印 10 和 20。
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
複製程式碼
解法:
未完待續,點選檢視更多細節:第 34 題
第 35 題:瀏覽器快取讀取規則
可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那請求的時候 from memory cache 和 from disk cache 的依據是什麼,哪些資料什麼時候存放在 Memory Cache 和 Disk Cache中?
解答:
總的來說:
- 如果開啟了Service Worker首先會從Service Worker中拿
- 如果新開一個以前開啟過的頁面快取會從Disk Cache中拿(前提是命中強快取)
- 重新整理當前頁面時瀏覽器會根據當前執行環境記憶體來決定是從 Memory Cache 還是 從Disk Cache中拿(可以看到下圖最後幾個檔案有時候是從 Memory Cache中拿有時候是從Disk Cache中拿)
注意:以上回答全部基於chrome瀏覽器
未完待續,點選檢視更多細節:第 35 題
第 36 題:使用迭代的方式實現 flatten 函式。
迭代的實現:
let arr = [1, 2, [3, 4, 5, [6, 7], 8], 9, 10, [11, [12, 13]]]
const flatten = function (arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr
}
console.log(flatten(arr))
複製程式碼
遞迴的實現(ES6簡寫):
const flatten = array => array.reduce((acc, cur) => (Array.isArray(cur) ? [...acc, ...flatten(cur)] : [...acc, cur]), [])
複製程式碼
未完待續,點選檢視更多細節:第 36 題
第 37 題:為什麼 Vuex 的 mutation 和 Redux 的 reducer 中不能做非同步操作?
歡迎在 Issue 區留下你的答案。
第 38 題:下面程式碼中 a 在什麼情況下會列印 1?
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
複製程式碼
解法1:利用 toString
let a = {
i: 1,
toString () {
return a.i++
}
}
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
複製程式碼
解法2:利用 valueOf
let a = {
i: 1,
valueOf () {
return a.i++
}
}
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
複製程式碼
解法3:陣列這個就有點妖了
var a = [1,2,3];
a.join = a.shift;
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
複製程式碼
解法4:ES6的symbol
let a = {
[Symbol.toPrimitive]: (i => () => ++i) (0)
};
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
複製程式碼
解法5:Object.defineProperty
Object.defineProperty(window, 'a', {
get: function() {
return this.value = this.value ? (this.value += 1) : 1;
}
});
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
複製程式碼
未完待續,點選檢視更多細節:第 38 題
第 39 題:介紹下 BFC 及其應用。
BFC是CSS佈局的一個概念,是一塊獨立的渲染區域,是一個環境,裡面的元素不會影響到外部的元素 。
-
如何生成BFC:(即脫離文件流)
- 1、根元素,即HTML元素(最大的一個BFC)
- 2、float的值不為none
- 3、position的值為absolute或fixed
- 4、overflow的值不為visible(預設值。內容不會被修剪,會呈現在元素框之外)
- 5、display的值為inline-block、table-cell、table-caption
-
BFC佈局規則:
-
1.內部的Box會在垂直方向,一個接一個地放置。
-
2.屬於同一個BFC的兩個相鄰的Box的margin會發生重疊
-
3.BFC就是頁面上的一個隔離的獨立容器,容器裡面的子元素不會影響到外面的元素。反之也如此, 文字環繞效果,設定float
-
4.BFC的區域不會與float box重疊。
-
5.計算BFC的高度,浮動元素也參與計算
-
-
BFC作用:
- 1.自適應兩欄佈局
- 2.可以阻止元素被浮動元素覆蓋
- 3.可以包含浮動元素---清除內部浮動 原理:觸發父div的BFC屬性,使下面的子div都處在父div的同一個BFC區域之內
- 4.分屬於不同的BFC時,可以阻止margin重疊
未完待續,點選檢視更多細節:第 39 題
交流
進階系列文章彙總如下,內有優質前端資料,覺得不錯點個star。
我是木易楊,公眾號「高階前端進階」作者,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!