腦子裡陸陸續續過出來的一些面試題,文章會不斷更新
專案相關
- 自我介紹:職業經歷,專案經歷
- 選一個你覺得印象最深的專案講一講,然後會從專案裡面切入到 web 基礎(html/css/js),這一塊大概會聊 20-30 分鐘,所以一定要提前選好一個自己做過的得意的專案,花一點時間捋一捋你覺得專案中出色的點,用到了比較 hack,比較酷炫的方法解決了哪些痛點。
JS 基礎(ES5)
- 原型:這裡可以談很多,只要圍繞 [[ prototype ]] 談,都沒啥問題
- 閉包:牽扯作用域,可以兩者聯絡起來一起談
- 作用域:詞法作用域,動態作用域
- this:不同情況的呼叫,this 指向分別如何。順帶可以提一下 es6 中箭頭函式沒有 this, arguments, super 等,這些只依賴包含箭頭函式最接近的函式
- call,apply,bind 三者用法和區別:引數、繫結規則(顯示繫結和強繫結),執行效率(最終都會轉換成一個一個的引數去執行)、執行情況(call,apply 立即執行,bind 是return 出一個 this “固定”的函式,這也是為什麼 bind 是強繫結的一個原因)。
注:“固定”這個詞的含義,它指的固定是指只要傳進去了 context,則 bind 中 return 出來的函式 this 便一直指向 context,除非 context 是個變數 6. 變數宣告提升:js 程式碼在執行前都會進行 AST 解析,函式申明預設會提到當前作用域最前面,變數申明也會進行提升。但賦值不會得到提升。關於 AST 解析,這裡也可以說是形成詞法作用域的主要原因
這裡如果面試官問到2,3,4,5,6中的一點,你能夠把2,3,4,5,6整理到一起,串聯起來進行統一的回答效果極佳
具體參考 從指向看JavaScript
JS 基礎(ES6)
- let,const:let 產生塊級作用域(通常配合 for 迴圈或者 {} 進行使用產生塊級作用域),const 申明的變數是常量(記憶體地址不變)
- Promise:這裡你談 promise 的時候,除了將他解決的痛點以及常用的 API 之外,最好進行擴充把 eventloop 帶進來好好講一下,microtask、macrotask 的執行順序,如果看過 promise 原始碼,最好可以談一談 原生 Promise 是如何實現的。Promise 的關鍵點在於callback 的兩個引數,一個是 resovle,一個是 reject。還有就是 Promise 的鏈式呼叫(Promise.then(),每一個 then 都是一個責任人)。
詳細參考 my-promise
- Generator:遍歷器物件生成函式,最大的特點是可以交出函式的執行權
function
關鍵字與函式名之間有一個星號;- 函式體內部使用
yield
表示式,定義不同的內部狀態; next
指標移向下一個狀態
這裡你可以說說 Generator
的非同步程式設計,以及它的語法糖 async
和 awiat
,傳統的非同步程式設計。ES6 之前,非同步程式設計大致如下
- 回撥函式
- 事件監聽
- 釋出/訂閱
傳統非同步程式設計方案之一:協程,多個執行緒互相協作,完成非同步任務。
- async、await:Generator 函式的語法糖。有更好的語義、更好的適用性、返回值是 Promise。
- async => *
- await => yield
基本用法
async function timeout (ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncConsole (value, ms) {
await timeout(ms)
console.log(value)
}
asyncConsole('hello async and await', 1000)
複製程式碼
注:最好把2,3,4 連到一起講
- AMD,CMD,CommonJs,ES6 Module:解決原始無模組化的痛點
- AMD:requirejs 在推廣過程中對模組定義的規範化產出,提前執行,推崇依賴前置
- CMD:seajs 在推廣過程中對模組定義的規範化產出,延遲執行,推崇依賴就近
- CommonJs:模組輸出的是一個值的 copy,執行時載入,載入的是一個物件(module.exports 屬性),該物件只有在指令碼執行完才會生成
- ES6 Module:模組輸出的是一個值的引用,編譯時輸出介面,ES6 模組不是物件,它對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。
CSS相關
- 左邊定寬,右邊自適應方案:float + margin,float + calc
/* 方案1 */
.left {
width: 120px;
float: left;
}
.right {
margin-left: 120px;
}
/* 方案2 */
.left {
width: 120px;
float: left;
}
.right {
width: calc(100% - 120px);
float: left;
}
複製程式碼
- 左右兩邊定寬,中間自適應:float,float + calc, 聖盃佈局(設定BFC,margin負值法),flex
.wrap {
width: 100%;
height: 200px;
}
.wrap > div {
height: 100%;
}
/* 方案1 */
.left {
width: 120px;
float: left;
}
.right {
float: right;
width: 120px;
}
.center {
margin: 0 120px;
}
/* 方案2 */
.left {
width: 120px;
float: left;
}
.right {
float: right;
width: 120px;
}
.center {
width: calc(100% - 240px);
margin-left: 120px;
}
/* 方案3 */
.wrap {
display: flex;
}
.left {
width: 120px;
}
.right {
width: 120px;
}
.center {
flex: 1;
}
複製程式碼
- 左右居中
- 行內元素: text-align: center
- 定寬塊狀元素: 左右 margin 值為 auto
- 不定寬塊狀元素: table佈局,position + transform
/* 方案1 */
.wrap {
text-align: center
}
.center {
display: inline;
/* or */
/* display: inline-block; */
}
/* 方案2 */
.center {
width: 100px;
margin: 0 auto;
}
/* 方案2 */
.wrap {
position: relative;
}
.center {
position: absulote;
left: 50%;
transform: translateX(-50%);
}
複製程式碼
- 上下垂直居中:
- 定高:margin,position + margin(負值)
- 不定高:position + transform,flex,IFC + vertical-align:middle
/* 定高方案1 */
.center {
height: 100px;
margin: 50px 0;
}
/* 定高方案2 */
.center {
height: 100px;
position: absolute;
top: 50%;
margin-top: -25px;
}
/* 不定高方案1 */
.center {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
/* 不定高方案2 */
.wrap {
display: flex;
align-items: center;
}
.center {
width: 100%;
}
/* 不定高方案3 */
/* 設定 inline-block 則會在外層產生 IFC,高度設為 100% 撐開 wrap 的高度 */
.wrap::before {
content: '';
height: 100%;
display: inline-block;
vertical-align: middle;
}
.wrap {
text-align: center;
}
.center {
display: inline-block;
vertical-align: middle;
}
複製程式碼
- 盒模型:content(元素內容) + padding(內邊距) + border(邊框) + margin(外邊距)
延伸: box-sizing
- content-box:預設值,總寬度 = margin + border + padding + width
- border-box:盒子寬度包含 padding 和 border,總寬度 = margin + width
- inherit:從父元素繼承 box-sizing 屬性
- BFC、IFC、GFC、FFC:FC(Formatting Contexts),格式化上下文
- BFC:塊級格式化上下文,容器裡面的子元素不會在佈局上影響到外面的元素,反之也是如此(按照這個理念來想,只要脫離文件流,肯定就能產生 BFC)。產生 BFC 方式如下
float 的值不為 none。
overflow 的值不為 visible。
position 的值不為 relative 和 static。
display 的值為 table-cell, table-caption, inline-block中的任何一個。
用處?常見的多欄佈局,結合塊級別元素浮動,裡面的元素則是在一個相對隔離的環境裡執行。
- IFC:內聯格式化上下文,IFC 的 line box(線框)高度由其包含行內元素中最高的實際高度計算而來(不受到豎直方向的 padding/margin 影響)。
IFC中的line box一般左右都貼緊整個 IFC,但是會因為 float 元素而擾亂。float 元素會位於 IFC 與 line box 之間,使得 line box 寬度縮短。 同個 ifc 下的多個 line box 高度會不同。 IFC 中時不可能有塊級元素的,當插入塊級元素時(如 p 中插入 div )會產生兩個匿名塊與 div 分隔開,即產生兩個 IFC ,每個 IFC 對外表現為塊級元素,與 div 垂直排列。
用處?
水平居中:當一個塊要在環境中水平居中時,設定其為 inline-block 則會在外層產生IFC,通過 text-align 則可以使其水平居中。
垂直居中:建立一個 IFC,用其中一個元素撐開父元素的高度,然後設定其 vertical-align: middle,其他行內元素則可以在此父元素下垂直居中
- GFC:網格佈局格式化上下文(display: grid)
- FFC:自適應格式化上下文(display: flex)
框架相關
- 資料雙向繫結原理:常見資料繫結的方案
- Object.defineProperty(vue):劫持資料的 getter 和 setter
- 髒值檢測(angularjs):通過特定事件進行輪循
- 釋出/訂閱模式:通過訊息釋出並將訊息進行訂閱
詳細細節參考 實現一個屬於我們自己的簡易MVVM庫
擴充:如何監聽陣列變化
- VDOM:三個 part,
- 虛擬節點類,將真實 DOM 節點用 js 物件的形式進行展示,並提供 render 方法,將虛擬節點渲染成真實 DOM
- 節點 diff 比較:對虛擬節點進行 js 層面的計算,並將不同的操作都記錄到 patch 物件
- re-render:解析 patch 物件,進行 re-render
詳細請參考 實現Virtual Dom && Diff
補充1:VDOM 的必要性?
-
建立真實DOM的代價高:真實的 DOM 節點 node 實現的屬性很多,而 vnode 僅僅實現一些必要的屬性,相比起來,建立一個 vnode 的成本比較低。
-
觸發多次瀏覽器重繪及迴流:使用 vnode ,相當於加了一個緩衝,讓一次資料變動所帶來的所有 node 變化,先在 vnode 中進行修改,然後 diff 之後對所有產生差異的節點集中一次對 DOM tree 進行修改,以減少瀏覽器的重繪及迴流。
補充2:vue 為什麼採用 vdom?
引入 Virtual DOM 在效能方面的考量僅僅是一方面。
效能受場景的影響是非常大的,不同的場景可能造成不同實現方案之間成倍的效能差距,所以依賴細粒度繫結及 Virtual DOM 哪個的效能更好還真不是一個容易下定論的問題。
Vue 之所以引入了 Virtual DOM,更重要的原因是為了解耦 HTML 依賴,這帶來兩個非常重要的好處是:
- 不再依賴 HTML 解析器進行模版解析,可以進行更多的 AOT 工作提高執行時效率:通過模版 AOT 編譯,Vue 的執行時體積可以進一步壓縮,執行時效率可以進一步提升;
- 可以渲染到 DOM 以外的平臺,實現 SSR、同構渲染這些高階特性,Weex 等框架應用的就是這一特性。
綜上,Virtual DOM 在效能上的收益並不是最主要的,更重要的是它使得 Vue 具備了現代框架應有的高階特性。
- vue 和 react 區別
- 相同點:都支援 ssr,都有 vdom,元件化開發,實現 webComponents 規範,資料驅動等
- 不同點:vue 是雙向資料流(當然為了實現單資料流方便管理元件狀態,vuex 便出現了),react 是單向資料流。vue 的 vdom 是追蹤每個元件的依賴關係,不會渲染整個元件樹,react 每當應該狀態被改變時,全部子元件都會 re-render。
上面提到的每個點,具體細節還得看自己的理解
- 為什麼用 vue :簡潔、輕快、舒服、沒了
網路基礎類
- 跨域:很多種方法,但萬變不離其宗,都是為了搞定同源策略。重用的有 jsonp、iframe、cors、img、HTML5 postMessage等等。其中用到 html 標籤進行跨域的原理就是 html 不受同源策略影響。但只是接受 Get 的請求方式,這個得清楚。
延伸1:img iframe script 來傳送跨域請求有什麼優缺點?
- iframe
優點:跨域完畢之後DOM操作和互相之間的JavaScript呼叫都是沒有問題的
缺點:1.若結果要以URL引數傳遞,這就意味著在結果資料量很大的時候需要分割傳遞,巨煩。2.還有一個是iframe本身帶來的,母頁面和iframe本身的互動本身就有安全性限制。
- script
優點:可以直接返回json格式的資料,方便處理
缺點:只接受GET請求方式
- 圖片ping
優點:可以訪問任何url,一般用來進行點選追蹤,做頁面分析常用的方法
缺點:不能訪問響應文字,只能監聽是否響應
延伸2:配合 webpack 進行反向代理?
webpack 在 devServer 選項裡面提供了一個 proxy 的引數供開發人員進行反向代理
'/api': {
target: 'http://www.example.com', // your target host
changeOrigin: true, // needed for virtual hosted sites
pathRewrite: {
'^/api': '' // rewrite path
}
},
複製程式碼
然後再配合 http-proxy-middleware
外掛對 api 請求地址進行代理
const express = require('express');
const proxy = require('http-proxy-middleware');
// proxy api requests
const exampleProxy = proxy(options); // 這裡的 options 就是 webpack 裡面的 proxy 選項對應的每個選項
// mount `exampleProxy` in web server
const app = express();
app.use('/api', exampleProxy);
app.listen(3000);
複製程式碼
然後再用 nginx
把允許跨域的源地址新增到報頭裡面即可
說到 nginx
,可以再談談 CORS 配置,大致如下
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'DNT, X-Mx-ReqToken, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 200;
}
}
複製程式碼
- http 無狀態無連線
- http 協議對於事務處理沒有記憶能力
- 對同一個url請求沒有上下文關係
- 每次的請求都是獨立的,它的執行情況和結果與前面的請求和之後的請求是無直接關係的,它不會受前面的請求應答情況直接影響,也不會直接影響後面的請求應答情況
- 伺服器中沒有儲存客戶端的狀態,客戶端必須每次帶上自己的狀態去請求伺服器
- 人生若只如初見,請求過的資源下一次會繼續進行請求
http協議無狀態中的 狀態 到底指的是什麼?!
【狀態】的含義就是:客戶端和伺服器在某次會話中產生的資料
那麼對應的【無狀態】就意味著:這些資料不會被保留
but
- 通過增加cookie和session機制,現在的網路請求其實是有狀態的
- 在沒有狀態的http協議下,伺服器也一定會保留你每次網路請求對資料的修改,但這跟保留每次訪問的資料是不一樣的,保留的只是會話產生的結果,而沒有保留會話
- http-cache:就是 http 快取咯
首先得明確 http 快取的好處
- 減少了冗餘的資料傳輸,減少網費
- 減少伺服器端的壓力
- Web 快取能夠減少延遲與網路阻塞,進而減少顯示某個資源所用的時間
- 加快客戶端載入網頁的速度
常見 http 快取的型別
- 私有快取(一般為本地瀏覽器快取)
- 代理快取
然後談談本地快取
本地快取是指瀏覽器請求資源時命中了瀏覽器本地的快取資源,瀏覽器並不會傳送真正的請求給伺服器了。它的執行過程是:
- 第一次瀏覽器傳送請求給伺服器時,此時瀏覽器還沒有本地快取副本,伺服器返回資源給瀏覽器,響應碼是200 OK,瀏覽器收到資源後,把資源和對應的響應頭一起快取下來。
- 第二次瀏覽器準備傳送請求給伺服器時候,瀏覽器會先檢查上一次服務端返回的響應頭資訊中的Cache-Control,它的值是一個相對值,單位為秒,表示資源在客戶端快取的最大有效期,過期時間為第一次請求的時間減去Cache-Control的值,過期時間跟當前的請求時間比較,如果本地快取資源沒過期,那麼命中快取,不再請求伺服器。
- 如果沒有命中,瀏覽器就會把請求傳送給伺服器,進入快取協商階段。
與本地快取相關的頭有:Cache-Control、Expires,Cache-Control有多個可選值代表不同的意義,而Expires就是一個日期格式的絕對值。
Cache-Control
Cache-Control是HTPP快取策略中最重要的頭,它是HTTP/1.1中出現的,它由如下幾個值
- no-cache:不使用本地快取。需要使用快取協商,先與伺服器確認返回的響應是否被更改,如果之前的響應中存在ETag,那麼請求的時候會與服務端驗證,如果資源未被更改,則可以避免重新下載。
- no-store:直接禁止遊覽器快取資料,每次使用者請求該資源,都會向伺服器傳送一個請求,每次都會下載完整的資源。
- public:可以被所有的使用者快取,包括終端使用者和CDN等中間代理伺服器。
- private:只能被終端使用者的瀏覽器快取,不允許CDN等中繼快取伺服器對其快取。
- max-age:從當前請求開始,允許獲取的響應被重用的最長時間(秒)。
例如:
Cache-Control: public, max-age=1000
複製程式碼
表示資源可以被所有使用者以及代理伺服器快取,最長時間為1000秒。
Expires
Expires是HTTP/1.0出現的頭資訊,同樣是用於決定本地快取策略的頭,它是一個絕對時間,時間格式是如Mon, 10 Jun 2015 21:31:12 GMT,只要傳送請求時間是在Expires之前,那麼本地快取始終有效,否則就會去伺服器傳送請求獲取新的資源。如果同時出現Cache-Control:max-age和Expires,那麼max-age優先順序更高。他們可以這樣組合使用
Cache-Control: public
Expires: Wed, Jan 10 2018 00:27:04 GMT
複製程式碼
所謂的快取協商
當第一次請求時伺服器返回的響應頭中存在以下情況時
- 沒有Cache-Control和Expires
- Cache-Control和Expires過期了
- Cache-Control的屬性設定為no-cache時
那麼瀏覽器第二次請求時就會與伺服器進行協商,詢問瀏覽器中的快取資源是不是舊版本,需不需要更新,此時,伺服器就會做出判斷,如果快取和服務端資源的最新版本是一致的,那麼就無需再次下載該資源,服務端直接返回304 Not Modified 狀態碼,如果伺服器發現瀏覽器中的快取已經是舊版本了,那麼伺服器就會把最新資源的完整內容返回給瀏覽器,狀態碼就是200 Ok,那麼服務端是根據什麼來判斷瀏覽器的快取是不是最新的呢?其實是根據HTTP的另外兩組頭資訊,分別是:Last-Modified/If-Modified-Since 與 ETag/If-None-Match。
Last-Modified 與 If-Modified-Since
- 瀏覽器第一次請求資源時,伺服器會把資源的最新修改時間Last-Modified:Thu, 29 Dec 2011 18:23:55 GMT放在響應頭中返回給瀏覽器
- 第二次請求時,瀏覽器就會把上一次伺服器返回的修改時間放在請求頭If-Modified-Since:Thu, 29 Dec 2011 18:23:55傳送給伺服器,伺服器就會拿這個時間跟伺服器上的資源的最新修改時間進行對比
如果兩者相等或者大於伺服器上的最新修改時間,那麼表示瀏覽器的快取是有效的,此時快取會命中,伺服器就不再返回內容給瀏覽器了,同時Last-Modified頭也不會返回,因為資源沒被修改,返回了也沒什麼意義。如果沒命中快取則最新修改的資源連同Last-Modified頭一起返回。
第一次請求返回的響應頭:
Cache-Control:max-age=3600
Expires: Fri, Jan 12 2018 00:27:04 GMT
Last-Modified: Wed, Jan 10 2018 00:27:04 GMT
複製程式碼
第二次請求的請求頭資訊:
If-Modified-Since: Wed, Jan 10 2018 00:27:04 GMT
複製程式碼
這組頭資訊是基於資源的修改時間來判斷資源有沒有更新,另一種方式就是根據資源的內容來判斷,就是接下來要討論的ETag與If-None-Match
ETag與If-None-Match
ETag/If-None-Match與Last-Modified/If-Modified-Since的流程其實是類似的,唯一的區別是它基於資源的內容的摘要資訊(比如MD5 hash)來判斷
瀏覽器傳送第二次請求時,會把第一次的響應頭資訊ETag的值放在If-None-Match的請求頭中傳送到伺服器,與最新的資源的摘要資訊對比,如果相等,取瀏覽器快取,否則內容有更新,最新的資源連同最新的摘要資訊返回。用ETag的好處是如果因為某種原因到時資源的修改時間沒改變,那麼用ETag就能區分資源是不是有被更新。
第一次請求返回的響應頭:
Cache-Control: public, max-age=31536000
ETag: "15f0fff99ed5aae4edffdd6496d7131f"
複製程式碼
第二次請求的請求頭資訊:
If-None-Match: "15f0fff99ed5aae4edffdd6496d7131f"
複製程式碼
- cookie 和 session
- session: 是一個抽象概念,開發者為了實現中斷和繼續等操作,將 user agent 和 server 之間一對一的互動,抽象為“會話”,進而衍生出“會話狀態”,也就是 session 的概念
- cookie:它是一個世紀存在的東西,http 協議中定義在 header 中的欄位,可以認為是 session 的一種後端無狀態實現
現在我們常說的 “session”,是為了繞開 cookie 的各種限制,通常藉助 cookie 本身和後端儲存實現的,一種更高階的會話狀態實現
session 的常見實現要藉助cookie來傳送 sessionID
- 安全問題,如 XSS 和 CSRF
- XSS:跨站指令碼攻擊,是一種網站應用程式的安全漏洞攻擊,是程式碼注入的一種。常見方式是將惡意程式碼注入合法程式碼裡隱藏起來,再誘發惡意程式碼,從而進行各種各樣的非法活動。
防範:記住一點 “所有使用者輸入都是不可信的”,所以得做輸入過濾和轉義
- CSRF:跨站請求偽造,也稱 XSRF,是一種挾制使用者在當前已登入的Web應用程式上執行非本意的操作的攻擊方法。與 XSS 相比,XSS利用的是使用者對指定網站的信任,CSRF利用的是網站對使用者網頁瀏覽器的信任。
防範:使用者操作驗證(驗證碼),額外驗證機制(token使用)等
詳細點選 對於跨站偽造請求(CSRF)的理解和總結