前言
劃重點
這個東東是四處收集來的,然後自己整理整理的,如果侵害的原作者的利益,如果造成了侵權等問題,請趕快聯絡俺!馬上刪除!!祝各位早日求職上岸!!衝鴨!!如果有錯誤,大噶評論區告訴俺一下!俺去改改!!加油!
小菜雞的面試の初體驗 juejin.im/post/5e5a64…
那俺們開始咯!!!衝鴨!!!
因為這些大部分也是從網上收集來的,如果大家發現有什麼錯誤的地方,請在評論區指出來,俺去改正改正!
1. 模擬new的過程
實現步驟
- 建立一個新的物件obj
- 連結到原型(新物件的原型指向要繼承的建構函式的原型),obj可以訪問建構函式原型的屬性
- 繫結this實現繼承,obj可以訪問建構函式的屬性
- 如果建構函式返回的是物件則返回它,如果不是則返回obj
function Animals(name, color){
this.name = name
}
Animals.prototype.action = function () {
console.log(this.name, 'walk')
複製程式碼
首先定義一個建構函式,以及他原型的方法
接著實現一個create
方法來模擬new
function create(constructor, ...args){
const obj = new Object()
obj.__proto__ = constructor.prototype
const res = constructor.apply(obj, args)
return res instanceof Object ? res : obj
}
複製程式碼
具體使用則:
const dog = create(Animals, 'dog', 'red')
// const cat = new Animals('cat', 'yellow')
複製程式碼
通過改變一個物件的 [[Prototype]] 屬性來改變和繼承屬性會對效能造成非常嚴重的影響,並且效能消耗的時間也不是簡單的花費在 obj.proto = ... 語句上, 它還會影響到所有繼承自該 [[Prototype]] 的物件,如果你關心效能,你就不應該修改一個物件的 [[Prototype]]。
所以我們可以通過別的方法來改變obj原型的指向,通過Object.create()
方法來繼承
function create(constructor, ...args) {
const obj = Object.create(constructor.prototype)
const res = constructor.apply(obj, args)
return res instanceof Object ? res : obj
}
複製程式碼
2. 函式防抖和節流
首先模擬一下使用者輸入
<div>
<input id="input"></input>
<div id="text">0</div>
</div>
複製程式碼
然後防抖與節流
// 防抖
const debounce = function (fn, delay = 1000) {
let time = null
return function (...args) {
let that = this
time && clearTimeout(time)
time = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
// 節流
// 時間戳版
function throttle(fn, delay = 500) {
let last = 0
return function (...args) {
let now = new Date().getTime()
if (now - last > delay) {
last = now
fn.apply(this, args)
}
}
}}
// 定時器版,初次呼叫會延遲
const throttle = function (fn, delay = 1000) {
let time = null
return function (...args) {
let that = this
if (!time) {
time = setTimeout(function () {
time = null
fn.apply(that, args)
}, delay)
}
}
}
複製程式碼
接下來呼叫即可
const input = document.getElementById('input')
input.oninput = debounce((e) => {
document.getElementById('text').innerTexte.target.value
}, 1500)
複製程式碼
這樣子就可以實現函式防抖和節流啦
其實這裡還有另外一個簡單一點的方法,就是setTimeout使用箭頭函式,這樣就可以直接使用this
,就不用額外生成一個變數that
啦
const debounce = function (fn, delay = 1000) {
let time = null
return function (...args) {
time && clearTimeout(time)
time = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
複製程式碼
3. 輸入url到展示的過程
- URL解析,如果有非法字元,就轉義
- 判斷請求資源快取中有沒有
- DNS解析
- TCP三次握手
- 傳送請求,分析url,設定請求頭
- 伺服器返回請求的檔案(html)
- 瀏覽器渲染
- 解析html檔案,生成dom樹
- 解析css檔案,生成style樹
- 結合dom樹和style樹,生成渲染樹(render tree)
- layout佈局渲染
- GPU畫素繪製頁面
4. 函式的柯里化
// 實現一個add方法,使計算結果能夠滿足如下預期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
複製程式碼
主要是要收集傳進來的引數
function curry(){
const argsList = [...arguments]
const fn = function(){
argsList.push(...arguments)
return fn
}
fn.toString = function(){
return argsList.reduce((a, b) => a + b)
}
return fn
}
console.log(curry(1, 2)(3)(4, 5, 6)) // 21
複製程式碼
這樣就算完成了這個題了,可以自由傳入引數來求和,接下來就將一個普通函式變成柯里化函式
function sum(a, b, c) {
return a + b + c
}
function curry(fn) {
const argsList = [...arguments].splice(1)
return function () {
const newArgsList = argsList.concat([...arguments])
if (newArgsList.length < fn.length) {
// 如果接收的引數還沒有到達函式引數的個數繼續收集引數
return curry.apply(this, [fn, ...newArgsList])
} else {
return fn.apply(this, newArgsList)
}
}
}
const sumAll = curry(sum)
console.log(sumAll(1)(2)(3)) // 6
console.log(sumAll(1)(2, 3)) // 6
複製程式碼
5. 重繪與迴流
1. 重繪
當元素樣式發生改變,但不影響佈局時,瀏覽器將使用重繪進行元素更新,由於此時只需要UI層面的繪製,因此損耗較小
2. 迴流
當元素尺寸、結構或者觸發某些屬性的時候,瀏覽器會重新渲染頁面,這就叫回流。此時,瀏覽器需要重新計算,重新進行頁面佈局,所以損耗較大
一般有以下幾種操作:
- 頁面初次渲染
- 瀏覽器視窗大小改變
- 元素尺寸、位置、內容改變
- 元素字型大小改變
- 新增或刪除可見的dom元素
- 觸發CSS偽類,如
:hover
- 查詢某些屬性或者呼叫某些方法
- clientWidth, clientHeight, clientTop, clientLeft
- offsetWidth, offsetHeight, offsetTop, offsetLeft
- scrollWidth, scrollHeight, scrollTop, scrollLeft
- getComputedStyle()
- getBoundingClientRect()
- scrollTo()
迴流必定觸發重繪,重繪不一定觸發迴流,重繪代價小,迴流代價大
如何避免重繪和迴流
CSS:
- 避免使用table佈局
- 儘可能再dom樹的末端修改class
- 避免使用多層內聯樣式
- 將動畫效果應用到
position: absolute || fixed
上 - 避免使用css表示式(例如
calc
) - CSS3硬體加速(GPU加速)
JavaScript:
- 避免頻繁操作樣式,最好一次性修改style屬性,或者將樣式列表定義成class,並一次性更改class屬性
- 避免頻繁操作dom,建立一個
documentFragment
,在他上面應用所有的dom操作,最後再把他新增到文件中 - 也可以先為元素設定
display: none
,操作結束後再把它顯示出來,因為再display為none的元素上進行dom操作不會引發重繪和迴流 - 避免頻繁讀取會引發重繪迴流的屬性,如果需要多次使用,就用一個變數快取起來
- 對具有複雜動畫的元素使用絕對定位,使他脫離文件流,否則會引起父元素及後續元素頻繁迴流
- 使用cssText來更改樣式
6. 瀏覽器儲存
cookie
通常用於存使用者資訊,登入狀態等,可自行設定過期時間,體積上限為4KlocalStorage
無限期儲存,體積上限為4~5MsessionStorage
瀏覽器視窗關閉則刪除,體積上線為4~5M
7. 網路請求方式post和get
get
會被瀏覽器快取,請求長度受限,會被歷史儲存記錄,瀏覽器回退時候是無害的,一般不帶請求體,傳送一個TCP資料包post
更安全,更多編碼型別,可以發大資料,瀏覽器回退的時候會再次提交請求,一般帶有請求體,傳送兩個TCP資料包
在網路差的時候,post傳送兩個TCP請求包,驗證完整性更好
8. TCP三次握手
為什麼要TCP握手呢
為了防止已失效的連線請求報文段突然又傳回服務端,從而產生錯誤
- 客戶端傳送syn(同步序列編號)請求,進入syn_send狀態,等待確認
- 服務端接受syn包並確認,傳送syn + ack包,進入syn_recv狀態
- 客戶端接受syn + ack包,傳送ack包,雙方進入established狀態
9. TCP四次揮手
- 客戶端傳送fin給服務端,用於關閉client到server的資料傳輸。客戶端進入fin_wait狀態
- 服務端接受fin後,傳送一個ack包給客戶端。服務端進入close_wait狀態
- 服務端傳送一個fin給客戶端,用於關閉server到client的資料傳輸。服務端進入last_ack狀態
- 客戶端收到fin後,進入time_wait狀態,接著傳送一個ack給服務端,服務端進入closed狀態
為什麼建立是3次握手,而關閉是4次揮手呢?
因為建立連線的時候,客戶端接受的是syn + ack包。而關閉的時候,服務端接受fin後,客戶端僅僅是不再傳送資料,但是還是可以接收資料的。服務端此時可以選擇立刻關閉連線,或者再傳送一些資料之後,再傳送fin包來關閉連線。因此fin與ack包一般都會分開傳送。
10. 記憶體洩漏
- 意外的全域性變數
- 閉包
- 未被清空的定時器
- 未被銷燬的事件監聽
- dom引用
11. 類繼承的特點
class Dog {
constructor(name) {
this.name = name;
}
}
class Labrador extends Dog {
// 1
constructor(name, size) {
this.size = size;
}
// 2
constructor(name, size) {
this.name = name;
this.size = size;
}
// 3
constructor(name, size) {
super(name);
this.size = size;
}
};
複製程式碼
像上面的程式碼,1和2都會報錯ReferenceError
引發一個引用錯誤,因為在子類中,在呼叫super
之前,是無法訪問this
的
12. 常用正規表示式
驗證數字的正規表示式集
驗證數字:^[0-9]*$
驗證n位的數字:^\d{n}$
驗證至少n位數字:^\d{n,}$
驗證m-n位的數字:^\d{m,n}$
驗證零和非零開頭的數字:^(0|[1-9][0-9]*)$
驗證有兩位小數的正實數:^[0-9]+(.[0-9]{2})?$
驗證有1-3位小數的正實數:^[0-9]+(.[0-9]{1,3})?$
驗證非零的正整數:^\+?[1-9][0-9]*$
驗證非零的負整數:^\-[1-9][0-9]*$
驗證非負整數(正整數 + 0) ^\d+$
驗證非正整數(負整數 + 0) ^((-\d+)|(0+))$
驗證長度為3的字元:^.{3}$
驗證由26個英文字母組成的字串:^[A-Za-z]+$
驗證由26個大寫英文字母組成的字串:^[A-Z]+$
驗證由26個小寫英文字母組成的字串:^[a-z]+$
驗證由數字和26個英文字母組成的字串:^[A-Za-z0-9]+$
驗證由數字、26個英文字母或者下劃線組成的字串:^\w+$
驗證使用者密碼:^[a-zA-Z]\w{5,17}$ 正確格式為:以字母開頭,長度在6-18之間,只能包含字元、數字和下劃線。
驗證是否含有 ^%&',;=?$\" 等字元:[^%&',;=?$\x22]+
驗證漢字:^[\u4e00-\u9fa5],{0,}$
驗證Email地址:^\w+[-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
驗證InternetURL:^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$ ;^[a-zA-z]+://(w+(-w+)*)(.(w+(-w+)*))*(?S*)?$
驗證電話號碼:^(\d3,4\d3,4|\d{3,4}-)?\d{7,8}$:--正確格式為:XXXX-XXXXXXX,XXXX-XXXXXXXX,XXX-XXXXXXX,XXX-XXXXXXXX,XXXXXXX,XXXXXXXX。
驗證身份證號(15位或18位數字):^\d{15}|\d{}18$
驗證一年的12個月:^(0?[1-9]|1[0-2])$ 正確格式為:“01”-“09”和“1”“12”
驗證一個月的31天:^((0?[1-9])|((1|2)[0-9])|30|31)$ 正確格式為:01、09和1、31。
整數:^-?\d+$
非負浮點數(正浮點數 + 0):^\d+(\.\d+)?$
正浮點數 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
非正浮點數(負浮點數 + 0) ^((-\d+(\.\d+)?)|(0+(\.0+)?))$
負浮點數 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
浮點數 ^(-?\d+)(\.\d+)?$
複製程式碼
13. 跨域
link,img,script標籤可以跨域
跨域行為
- 同源策略限制、安全性考慮(如cookies)
- 協議、IP地址和埠不同都是跨域行為
常見跨域
- jsonp
- cors
- websocket
- postMessage + iframe
- document.domain + iframe
- window.name + iframe
- nginx代理
- iframe巢狀進行跨域
JSONP
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'xxx.com/login?user=xxx&password=123&callback=onBack'
document.head.appendChild(script)
function onBack(res) {
console.log(res)
}
複製程式碼
CORS跨域
CORS (Cross-Orgin Resources Share)跨域資源共享,允許瀏覽器向伺服器發出XMLHttpRequest請求,從而克服跨域問題,他需要瀏覽器與伺服器同時支援
- 瀏覽器端會自動向請求頭新增
orgin
欄位,表明當前請求來源 - 伺服器需要設定響應頭的
Access-Control-Allow-Methods
,Access-Control-Allow-Headers
,Access-Control-Allow-Origin
等欄位,指定允許的方法、頭部、來源等資訊 - 請求分為簡單請求和非簡單請求,非簡單請求會先進行一次
OPTIONS
請求來判斷當前是否允許跨域
簡單請求
請求方法是以下三種之一:
- HEAD
- POST
- GET
Http的請求頭資訊不超過以下幾種欄位:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type (只限三個值:
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)
後端的響應頭資訊:
Access-Control-Allow-Orgin
:此欄位必須有,他的值要麼是請求時orgin
的值,要麼是*表示接受任意域名的訪問Access-Control-Allow-Credentials
:此欄位可選,是一個布林值,用來表示是否允許傳送cookies
Access-Control-Expose-Headers
:此欄位可選,如果想XMLHttpRequest物件的getResponseHeader()
方法獲取更多請求頭,就要在這個欄位指定
非簡單請求
非簡單請求是指對伺服器有特殊要求的請求,例如:
- 請求方法為
PUT
和DELETE
Content-Type
為application/json
非簡單請求會在正式通訊之前,增加一次HTTP查詢請求,稱為預檢請求,就是options
啦
Access-Control-Request-Methor
:此欄位必須有,用來表示瀏覽器的CORS請求會用到哪些HTTP方法,例如PUT
Access-Control-Request-Headers
:此欄位是一個逗號分隔的字串,指定瀏覽器CORS請求會額外傳送的請求頭資訊
CORS 與 JSONP的對比
- JSONP只可以用於
GET
請求,CORS支援所有型別的HTTP請求 - JSONP的優勢是支援老式瀏覽器,以及可以向不支援CORS的網站傳送請求
14. 快取
其實平時開發使用
webpack
進行構建的時候,字尾有hash值,其實這就是與快取相關的東西
快取可以分為強快取和協商快取。強快取不過伺服器,協商快取需要過伺服器。協商快取返回的狀態碼是304。兩種快取可以同時存在,強快取的優先順序大於協商快取。當執行強快取時,如若快取命中,則直接使用快取資料庫中的資料,不執行協商快取。
強快取
Expires (HTTP 1.0):Expires
的值為服務端返回的資料過期時間。當再次請求時間小於過期時間,則直接使用快取資料。但由於服務端和客戶端的時間可能有誤差,可能會出現快取命中的誤差。另外,大多數使用Cache-Control
代替
Pragma (HTTP 1.0):HTTP 1.0遺留下來的欄位,當值為no-cache
時強制驗證快取,Pragma禁用快取,如果給Expires
定義一個未過期的時間,那麼Pragma的優先順序更高。服務端響應新增Pragma: no-cache
,瀏覽器表現的行為和重新整理(F5)相似
Cache-Control (HTTP 1.1):有以下幾種屬性
- private:客戶端可以快取
- public:客戶端和代理伺服器都可以快取
- max-age=t:快取內容將在t秒後失效
- no-cache:需要使用協商快取來驗證快取資料
- no-store:所有內容都不會快取
no-cache不是代表不快取的意思,no-cache代表的是可以快取,但是每次都要通過伺服器驗證快取是否可用。no-store才是不快取內容。
當響應頭Cache-Control
有指定max-age
時,優先順序會比expires
高。
命中強快取的形式:Firefox瀏覽器:灰色的200狀態碼,Chrome瀏覽器:200(from disk cache)或者200 OK(from memory cache)
協商快取
協商快取需要進行對比判斷是否可以使用快取。瀏覽器第一次請求資料時候,伺服器會將快取標識與資料一起響應給客戶端,客戶端將他們備份至快取中。再次請求時候,客戶端會將快取中的標識傳送給服務端,服務端根據此表示判斷。若未失效,返回304狀態碼,瀏覽器拿到此狀態碼,就可以直接使用快取資料了。
Last-Modified:伺服器再響應請求時,會告訴瀏覽器資源的最後修改時間,因為可能最後一秒多次修改,或者是伺服器與客戶端時間不同,可能導致快取未命中,所以之後推出了etag
if-Modified-Since:瀏覽器再次請求伺服器的時候,請求頭會包含此欄位,後面跟著在快取中獲得的最後修改時間。服務端收到此請求發現有if-Modified-Since
後,則與被請求資源的最後修改時間進行對比,如果一致則返回304響應,瀏覽器只需要從快取中獲取資料即可
- 如果資源被修改了,服務端就傳輸一個整體資料,伺服器返回200 OK
- 如果沒有被修改,伺服器只需要傳輸響應頭資訊,伺服器返回 304 Not Modified
if-Unmodified-Since:從某個時間點開始,檔案是否沒有被修改,使用的是相對時間,不需要關心客戶端與服務端的時間偏差。
- 如果沒有被修改,則開始繼續傳送資料,伺服器返回200 OK
- 如果檔案被修改了,則不傳輸,伺服器返回412 Precondition failed(預處理錯誤)
這兩個的區別是一個修改了才下載,一個是沒修改才下載。
如果在伺服器上,一個資源被修改了,但是他的實際內容根本沒有發生變化,會因為Last-Modified時間匹配不上而返回了整個資料給客戶端(即使客戶端快取裡面有一個一模一樣的資源),為了解決這個問題,我們就需要用到HTTP 1.1的Etag
Etag:伺服器響應請求時,通過此欄位告訴瀏覽器當前資源在伺服器生成的唯一標識(生成規則由伺服器決定)
If-Match:條件請求,攜帶上一次請求中資源的Etag
,伺服器根據這個欄位判斷檔案是否有新的修改
If-None-Match:再次請求伺服器時,瀏覽器的請求報文頭部會包含此欄位,後面的值為在快取中獲取的標識,伺服器接收到報文後發現If-None-Match
則與被請求的資源的唯一表示進行對比
- 不同,說明資源被修改過,則相應整個資源內容,返回狀態碼200
- 相同,說明資源沒有被修改,則響應header,瀏覽器直接從快取中獲取資料資訊,伺服器返回狀態碼304
Etag 的精度比 Last-modified 高,屬於強驗證,要求資源位元組級別的一致,優先順序高。如果伺服器端有提供 ETag 的話,必須先對 ETag 進行 Conditional Request。
但是實際應用中由於
Etag
的計算是由演算法得來的,而演算法會佔用伺服器計算的資源,所有伺服器端的資源都是寶貴的,所以就很少使用Etag
了
- 瀏覽器輸入URL地址,回車之後,瀏覽器發現快取中已經有這個檔案了,就不用繼續請求,直接從快取中獲取資料資訊
- F5重新整理,就是瀏覽器向伺服器傳送一個請求,攜帶
If-Modified-Since
來檢視檔案是否過期 - Ctrl + F5強制重新整理,就是先吧快取中檔案刪除,然後再去伺服器請求一個完整的資源,於是客戶端就完成了強制更新的操作了
快取場景
對於大部分的場景都可以使用強快取配合協商快取來解決,但是在一些特殊情況下,可能需要選擇特殊的快取策略
- 對於某些不需要快取的資源,可以使用
Cache=Control: no-store
來表示該資源不需要快取 - 對於頻繁變動的資源,可以使用
Cache-Control: no-cache
並且配合Etag
使用,表示該資源已被快取,但是每次都會傳送請求詢問資源是否更新 - 對於程式碼檔案來說,通常使用
Cache-Control: max-age=31536000
並且配合策略快取使用,然後對檔案進行指紋處理,一旦檔名變動就會立即下載新的檔案
15. 手寫call、apply、bind
Function.prototype.myCall = function (obj = window) {
obj.fn = this
const args = [...arguments].splice(1)
const result = obj.fn(...args)
delete obj.fn
return result
}
Function.prototype.myApply = function (obj = window) {
obj.fn = this
const args = arguments[1]
let result = args ?
obj.fn(...args) :
obj.fn()
delete obj.fn
return result
}
Function.prototype.myBind = function (obj = window) {
const that = this
const args = [...arguments].splice(1)
return function () {
return that.apply(obj, [...args, ...arguments])
}
}
// 然後使用即可
function log() {
console.log(this)
console.log(this.value)
console.log(arguments)
}
const obj = {
value: 123
}
log.myCall(obj, 1, 2, 3)
log.myApply(obj, [1, 2, 3])
log.myApply(obj)
log.myBind(obj)()
log.myBind(obj)(1, 2, 3)
log.myBind(obj, 4, 5, 6)(1, 2, 3)
複製程式碼
16. 偽類和偽元素的區別
偽類和偽元素是為了修飾不在文件樹中的部分,比如一句話的第一個字母,列表中第一個元素。
偽類
偽類用於當已有元素處於某種狀態時候,為其新增對應的樣式,這個狀態是根據使用者行為變化而變化的。比如說hover。雖然他和普通的css類似,可以為已有的元素新增樣式,但是他只有處於dom樹無法描述的狀態才能為元素新增樣式,所以稱為偽類
偽元素
偽元素用於建立一些原本不在文件樹中的元素,併為其新增樣式,比如說:before
。雖然使用者可以看到這些內容,但是其實他不在文件樹中。
區別
偽類的操作物件是文件樹中已存在的元素,而偽元素是建立一個文件樹外的元素。因此,偽類與偽元素的區別就在於:有沒有建立一個文件樹之外的元素
css規範中使用兩個雙冒號::
來表示偽元素,一個冒號:
來表示偽類。例如:::before
,:hover
17. 絕對定位
- 父元素是塊級元素,則子元素設定為父元素內邊距的邊界
- 父元素是行內元素,則子元素設定為父元素的內容邊界
附上層疊上下文表
18. window.onload,doument.onload的區別
- window.onload:指頁面上所有的dom,樣式表,指令碼,圖片,flash都已經載入完成了的時候觸發
- document.onload:指dom載入完成,不包括別的東西
19. 事件委託
事件委託就是把一個元素響應時間(click,keydown....)的函式委託到另一個元素。一般來說,會把一個或者一組元素的時間委託到父層或者更外層元素上。真正繫結事件的是外層元素,當事件響應到需要繫結的元素時候,會通過事件冒泡機制,從而觸發他的外層元素的繫結事件上,然後再外層函式執行
- 類似keydown,onclick這種滑鼠事件,鍵盤事件,支援冒泡的事件型別才有事件委託
- 類似介面事件:change,submit這種就沒有事件代理
事件冒泡
事件模型分為三個階段
- 捕獲階段:在事件冒泡的模型中,捕獲階段不會響應任何事件
- 目標階段:目標階段就是指事件響應到觸發事件的最底層元素上
- 冒泡階段:冒泡階段就是事件的觸發響應會從最底層目標一層一層向外到最外層元素(根節點),事件代理就是利用事件冒泡的機制,把內層需要響應的事件繫結到外層
事件委託的優點
- 減少記憶體消耗
假設有一個列表,裡面很多li,如果為他們每一個都繫結事件就很浪費記憶體,最好是把這個時間繫結到外層
ul
上統一處理
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
// ...... 代表中間還有未知數個 li
複製程式碼
所以事件委託可以大量減少記憶體的消耗,節約效率
- 動態繫結事件 假如我們需要做到動態增刪列表內的元素,那麼在每一次改變的時候又得重新進行事件繫結。如果用了事件委託就沒有這煩惱了,因為事件繫結在父元素上,跟目標元素增刪沒有關係。
所以事件委託可以動態繫結時間,減少很多重複工作
window.onload = () => {
const list = document.getElementById('list')
list.addEventListener('click', function (event) {
console.log(event.target)
})
}
<ul class="list" id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
複製程式碼
這樣點選li標籤的時間就會冒泡到ul上面,然後開始執行繫結的方法
注意
分情況分析:
- 有拿到節點的,優先捕獲,沒有才往上冒泡尋找
- 若是通過node.addEventListener('event',callback,bubble or capture); 誰先呼叫誰先執行
20. instanceof的的實現原理
while (x.__proto__) {
if (x.__proto__ === y.prototype) {
return true
}
x.__proto__ = x.__proto__.__proto__
}
if(x.__proto__ === null){
return false
}
複製程式碼
判斷x物件是否為y的一個例項,會在原型鏈上一直找,找到則返回true
21. 繼承
- 原型繼承
Student.prototype = new Person('b')
Student.prototype.constructor = Student
複製程式碼
缺點:
- 子型別無法超型別傳遞引數
- 子類的方法必須寫在
new Person
後面,不然會被覆蓋
- 類式繼承
function Child(name, parentName) {
Parent.call(this, parentName);
this.name = name;
}
複製程式碼
缺點:
- 沒有原型,每次建立都會執行Parent.call
- 組合繼承
function Child(name, parentName) {
Parent.call(this, parentName);
this.name = name;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
複製程式碼
缺點:
- 父類建構函式會被呼叫兩次
- 寄生組合
function Person(name) {
this.name = name
}
Person.prototype.a = function () {
console.log('person a')
}
Person.prototype.b = function () {
console.log('person b')
}
function Student(name, superName) {
Person.call(this, superName)
this.name = name
}
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student
Student.prototype.b = function () {
console.log('student b')
}
const person = new Person('p')
const student = new Student('s')
console.log(person)
console.log(student)
person.a()
person.b()
student.a()
student.b()
console.log(student instanceof Person)
複製程式碼
22. for...in 和 for...of
- for...in for...in會遍歷可遍歷物件的所有屬性
const arr = [5, 4, 3, 2, 1]
arr.name = 'name'
for(let i in arr){
console.log(i) // 0 1 2 3 4 name(字串型別的索引)
}
複製程式碼
輸出的是0 1 2 3 4 name
,因為我們給arr加了一個name的屬性,for...in也會把他遍歷出來
所以for...in一般用於遍歷物件,值是他的鍵值
- for...of
const arr = [5, 4, 3, 2, 1]
arr.name = 'name'
for(let i in arr){
console.log(i) // 0 1 2 3 4 name(字串型別的索引)
}
複製程式碼
輸出的是5 4 3 2 1
,輸入陣列裡面每個的值
總結
- for...of使用遍歷陣列/陣列物件/字串/map/set等擁有迭代器物件,但是不能遍歷物件,因為物件沒有迭代器物件。他可以使用break,continue,return
- for...in來遍歷物件,或者使用
Object.keys()
- for...in遍歷的是陣列的索引(鍵名),for...of是陣列的值
23. 陣列扁平化
const test = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
]
// 要求將以上陣列扁平化排序且去重
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
// 遞迴版
function flat(arr) {
let list = []
arr.forEach(item => {
if (Array.isArray(item)) {
list.push(...flat(item))
// list = list.concat(flat(item))
} else {
list.push(item)
}
})
return list
}
const res1 = [...new Set(flat(test))].sort((a, b) => a - b)
// ES6版
const res2 = [...new Set(test.flat(Infinity))].sort((a, b) => a - b)
console.log(res1)
console.log(res2)
複製程式碼
24. 前端效能優化
編寫時候
-
寫程式碼的時候注意記憶體洩露(定時器、事件監聽、閉包、全域性變數、dom引用等)
-
css部分不要用table佈局、css表示式等會引起迴流的樣式、可以開啟3d硬體加速(perspective、backface-visibility修復閃爍)
-
js部分不要頻繁修改樣式、操作dom、可以使用防抖節流等、指令碼放在頁面底部
-
webpack部分可以壓縮js,css、分離公共資源例如ui元件庫等、配置元件按需載入(import()實現)
-
瀏覽器部分可以設定快取,使用cdn
-
圖片可以壓縮圖片,使用webp格式、雪碧圖等,圖片多的時候可以用懶載入等方法
-
React部分可以使用shouldComponentUpdate、React.Memo、pureComponent、列表元素設定key,儘量在constructor裡面繫結函式等
-
nginx可以配置負載均衡、多伺服器可以用redis來保持會話
-
頁面內容
- 減少HTTP請求數
- 減少DNS查詢
- 避免重定向
- 快取Ajax請求
- 延遲載入
- 預先載入
- 減少dom元素數量
- 劃分不同內容到不同域名
- 儘量減少使用iframe
- 避免404錯誤
-
伺服器
- 使用CDN
- 新增Expires或者Cache-Control響應頭
- 啟用Gzip
- 配置Etag
- 儘早輸出緩衝
- Ajax請求使用get方法
- 避免圖片src為空
-
Cookie
- 減少cookie大小
- 靜態資源使用無cookie域名
-
Css
- 把樣式表放在
<head>
中 - 不要使用CSS表示式
- 使用
<link
代替@import
- 不要使用filter
- 把樣式表放在
-
JavaScript
- 把指令碼放在頁面底部
- 使用外部JavaScript和Css
- 壓縮JavaScript和Css
- 避免重複指令碼
- 減少dom操作
- 使用高效的時間處理
-
圖片
- 優化圖片
- 優化CSS Sprite
- 不要再HTML中縮放圖片
- 使用體積小、可快取的favicon.ico
-
移動端
- 保持單個檔案小於25KB
- 打包內容為分段(multipart)文件
25. js實現動畫
將div平滑滑動,從快至慢,不能用css3的屬性
<!DOCTYPE html>
<html>
<head>
<script>
window.onload = () => {
var element = document.getElementById('t');
function step(timestamp) {
console.log(timestamp)
var progress = timestamp;
// element.style.left = (progress / 30) * (10 - progress / 1000) + 'px';
element.style.left = Math.pow(progress / 5, .5) * 40 + 'px';
if (progress < 5000) {
window.requestAnimationFrame(step);
} else {
window.cancelAnimationFrame(step)
}
}
window.requestAnimationFrame(step);
}
</script>
<style>
.container {
position: relative;
width: 1000px;
}
#t {
position: absolute;
width: 50px;
height: 50px;
background: red;
}
</style>
</head>
<body>
<div class="container">
<div id="t"></div>
</div>
</body>
</html>
複製程式碼
26. 實現autocomplete屬性
有一個input輸入框,輸入關鍵詞的時候,模擬autocomplete效果補全
<!DOCTYPE html>
<html>
<head>
<script>
window.onload = () => {
const input = document.getElementById('input')
const words = ['珠海', '廣州', '上海', '杭州', '成都']
input.addEventListener("input", debounce(function (e) {
const value = e.target.value
const index = words.findIndex((item) => value && item.indexOf(value) !== -1)
if (index !== -1) {
e.target.value = words[index]
}
}, 500))
function debounce(fn, wait = 500) {
let timeout = null
return function () {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
fn.apply(this, [...arguments])
}, wait)
}
}
}
</script>
<style>
</style>
</head>
<body>
<input id="input" />
</body>
</html>
複製程式碼
27. 程式碼實現深拷貝
const deepClone = (obj) => {
let result = null
if (typeof obj === 'object') {
result = Array.isArray(obj) ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === 'object') {
result[key] = deepClone(obj[key])
} else {
result[key] = obj[key]
}
}
}
} else {
// 如果不是物件與陣列,則直接賦值
result = obj
}
return result
}
複製程式碼
或者使用自帶方法
JSON.parse(JSON.stringify(obj))
複製程式碼
28. 亂序演算法
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
簡單版
arr.sort(() => Math.random() - 0.5)
複製程式碼
洗牌演算法
const shuffle = (arr) => {
let len = arr.length
while (len > 1) {
let index = Math.floor(Math.random() * len--);
[arr[index], arr[len]] = [arr[len], arr[index]]
}
return arr
}
複製程式碼
29. 陣列去重
const arr = [1, 1, 1, 2, 3, 4, 5, 5, 3, 2, 1, 1]
簡單版
[...new Set(arr)]
複製程式碼
迴圈版
const removeDrop = function () {
const result = []
const map = {}
arr.forEach((item) => {
if (!map[item]) {
map[item] = true
result.push(item)
}
})
return result
}
複製程式碼
30. React中的key作用
概述
key是用來幫助react識別哪些內容被更改、新增或者刪除。key值需要一個穩定值,因為如果key發生了變化,react則會觸發UI的重渲染。
-
唯一性: key值必須唯一,如果出現了相同,會丟擲警告,並且只會渲染第一個重複key值的元素。因為react會認為後續擁有相同key值的都是同一個元件。
-
不可讀性: 雖然在元件定義了key,但是子元件中是無法獲取key值的
改變元件的key值的話,可以銷燬之前的元件,再建立新的元件
注意
如果涉及到陣列的動態變更,例如陣列新增元素,刪除,重新排列,這時候如果index作為key值,會導致顯示錯誤的資料
{this.state.data.map((v,idx)=><Item key={idx} v={v} />)}
// 開始時:['a','b','c']=>
<ul>
<li key="0">a <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">c <input type="text"/></li>
</ul>
// 陣列重排 -> ['c','b','a'] =>
<ul>
<li key="0">c <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">a <input type="text"/></li>
</ul>
複製程式碼
key相同,元件有變化,則只會更新變化的屬性,如果key不同,則會銷燬之前的元件,整個元件重新渲染
使用index作為key的場景
第一頁
<ul>
<li key="0">張三</li>
<li key="1">李四</li>
<li key="2">王五</li>
</ul>
第二頁
<ul>
<li key="0">張三三</li>
<li key="1">李四四</li>
<li key="2">王五五</li>
</ul>
複製程式碼
翻頁後,子元件值發生變化,但key不變,只會發生更新,而不會重新渲染
反模式
現在有一個例子,我們不新增key的情況下,react會自動以索引的形式作為key值
let arr = ['first', 'second'];
// list1 和 list2 是等價的
const list1 = arr.map(item => <p>{item}</p>);
const list2 = arr.map((item, index) => <p key={index}>{item}</p>);
複製程式碼
接著我們向陣列末尾加一個元素,react經過diff演算法之後,key值0和1的元素沒有發生改變,所以UI上的操作只是僅僅在末尾新增多一個元素
但是如果在開頭新增一個元素
<!-- before -->
<p key="0">first</p>
<p key="1">second</p>
<!-- after -->
<p key="0">zero</p>
<p key="1">first</p>
<p key="2">second</p>
複製程式碼
所以元素的key值都發生了改變,這樣每個元素都會重新渲染一次,效能就大大受到影響了
總結
簡而言之,改變 key 值來重渲染元件是一種——相較於複雜componentWillReceiveProps生命週期——十分低成本的方式。
31. React系列
元件的render函式在何時被呼叫
每一次state的更改都會使得render函式被呼叫,但頁面的dom不一定會發生修改
元件的生命週期
- 初始化階段(Mounting)
- 更新階段(Updating)
- 析構階段(Unmounting)
初始化階段:
- constructor(): 用於繫結事件,初始化state
- componentWillMount(): 元件將要掛載,在render之前呼叫,每一個元件render之前就呼叫,一般在這操作state。可以在服務端呼叫
- render(): 用作渲染dom
- componentDidMount(): 在render之後,而且是所有子元件都render之後才呼叫,通常用於非同步請求資料,因為在這裡元件都初始化完成了
更新階段:
- componentWillReceiveProps(nextProps): 在這裡可以拿到即將改變的狀態,可以在這裡通過setState方法設定state
- shouldComponentUpdate(nextProps, nextState): 他的返回值決定了接下來的宣告週期是否會被呼叫,預設返回true
- componentWillUpdate(): 不能在這裡改變state,否則會陷入死迴圈
- componentDidUpdate(): 和componentDidMount()類似,在這裡執行Dom操作以及發起網路請求
析構階段
- componentWillUnmount(): 主要執行清除工作,比如取消網路請求,清除事件監聽
Virtual Dom
難點在於如何判斷舊的物件和新的物件之間的差異
react的辦法是隻對比同層的節點,而不是跨層對比
步驟分為兩步:
- 首先從上至下,從左往右遍歷物件,也就是樹的深度遍歷,這一步會將每一個節點新增索引,便於最後渲染差異
- 一旦節點有子元素,就去判斷子元素是否有不同
Virtual Dom的演算法實現主要是三步
- 通過js來模擬建立dom物件
- 把虛擬dom轉換成真實dom插入頁面中
- 發生變化時候,比較兩顆樹的差異,生成差異物件
- 根據差異物件渲染差異到真實dom
setState同步?非同步?
- setState只在合成事件(JSX中的onClick、onChange等)和鉤子函式中是非同步的,在原生事件和setTimeout中都是同步的
- setState的“非同步”並不是說內部是由非同步程式碼實現的,其實本身執行的過程和程式碼都是同步的,只是合成事件和鉤子函式的呼叫順序在更新之前,導致在合成事件和鉤子函式中沒法立馬拿到更新後的值,當然也可以用setState第二個引數來獲得
- setState的批量更新優化是建立在“非同步”(合成事件,鉤子函式)上的。在原生時間和setTimeout中不會批量更新,在“非同步”中如果對一個值多次進行setState,setState的批量更新策略會對其進行覆蓋,取最後一次的執行,如果是同時setState多個不同的值,在更新時會對其進行合併批量更新
class App extends React.Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val);
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}, 0)
}
render() {
return <div>{this.state.val}</div>
}
}
複製程式碼
因為在鉤子函式裡面setState是非同步的,所以前兩次無法獲得val的值,所以都輸出0,然後到setTimeout的時候,setState已經更新了,而且因為是批量更新所以只執行最後一次,所以到setTimeout的時候,val是1。因為setTimeout中是同步的,所以第一次是2,第二次是3
32. React常見面試題
呼叫setState之後發生了什麼
在程式碼呼叫setState函式之後,React會將傳入的引數物件與元件當前的狀態合併,然後觸發調和過程(Reconciliation),經過調和過程,React會以相對高效的方式根據新的狀態構建React元素樹,並且重新渲染整個UI介面。React得到元素樹之後,會計算出新樹與老樹的節點差異,然後根據差異對介面進行最小化的重渲染。在差異演算法中,React能夠相對精確的知道那些地方發生了改變,這就保證了按需更新,而不是全部重新渲染
React中Element和Component的區別
React Element是描述螢幕上可見內容的資料結構,是對於UI的物件表述。而React Component這是可以接受引數輸入並且返回某個React Element的函式或者類
什麼情況下你會優先選擇使用Class Component而不是Functional Component
當元件需要內部狀態以及生命週期函式的時候就選擇Class Component,否則就是用Functional Component
React中refs的作用是什麼
Refs是React提供的可以安全訪問dom元素或者某個元件例項的控制程式碼,可以為元素新增ref屬性然後再回撥函式中接受該元素在dom樹中的控制程式碼
React中keys的作用
keys是React用於追蹤列表中哪些元素被修改、新增或者刪除的輔助標識,需要保證key的唯一性。React diff演算法中,會藉助key值來判斷元素是新建的還是移動而來的,從而減少不必要的元素重渲染。此外React還需要藉助Key值來判斷元素與狀態的關聯關係
受控元件與非受控元件的區別
- 受控元件指那些將表單資料給React統一管理的元件,需要使用setState來更新表單的值
this.state = {
value: ''
}
...
handleChange = (e) => {
this.setState({
value: e.target.value
})
}
...
<input onChange={() => this.handleChange} />
複製程式碼
- 非受控元件就是由dom來存放表單資料,我們可以使用refs來操控dom元素
handleChange = () => {
console.log(this.input.value)
}
...
<input refs={(input) => this.input = input}
onChange={() => this.handleChange} />
複製程式碼
儘管非受控元件比較容易實現,但是我們有時候會需要對資料進行處理,使用受控元件會更加容易
在宣告週期中哪一步應該發起Ajax請求
應該放在componentDidMount()
- Fiber調和演算法會影響到compnentWillMount的觸發次數,如果放在裡面,可能會發起多次Ajax請求
- 如果放在其他生命週期中,假設我們獲取了Ajax請求的結果,並且新增到元件的狀態中,未掛載的元件就會報錯。而在
componentDidMount
中就不會有這些問題了
shouldComponentUpdate的作用是什麼,為何他如此重要
shouldComponentUpdate允許我們手動的判斷元件是否更新,就可以避免不必要的更新渲染
React中的事件處理邏輯
為了解決跨瀏覽器相容性的問題,React會將瀏覽器原生事件封裝為合成事件傳入設定的事件處理器中。合成事件與原生時間採用相同的介面,不過他們遮蔽了底層瀏覽器的細節差異,保證了行為的一致性。React沒有將事件依附在子元素上,而是將所有事件傳送到頂層 document
進行處理
JSX上面的事件全部繫結在 document
上,不僅減少了記憶體消耗,而且元件銷燬的時候可以統一移除事件
如果我們不想事件冒泡的話,應該是呼叫event.preventDefault
,而不是傳統的event.stopPropagation
createElement和cloneElement的區別是什麼
createElement函式是JSX編譯之後使用的建立Element的函式,而cloneElement這是複製某個元素並傳入新的Props
setState函式的第二個引數的作用是什麼
是一個回撥函式,這個函式會在setState函式呼叫完成並且元件開始重渲染的時候被呼叫,我們可以通過這個函式來判斷更新渲染是否完成
為什麼虛擬dom會提高效能
虛擬dom相當於在js和真實dom中間加了一個快取,利用dom diff演算法避免了沒有必要的dom操作,從而提高效能 具體步驟如下:
- 使用js來模擬建立一個dom
- 將這個虛擬dom構建真實dom,插入到頁面中
- 狀態變更時,比較兩顆物件樹的差異,生成差異物件
- 根據差異物件進行更新
diff演算法
- 把樹形結構按照層次分解,只比較同級元素
- 為列表結構的每一個元素都新增唯一的key屬性,方便比較
- 如果是同一型別的元件,則按照tree diff一樣遍歷
- 如果不是同一類,則該元件判斷為dirty component,從而替換整個元件下的所有子節點,就不用花時間來比較差異了
- 合併操作,呼叫setState的時候會將元件標記為dirty,到一個事件迴圈結束,React檢查所有標記dirty的component重新繪製。這樣dom只會被更新一次,節約效能
- 選擇性子樹渲染,利用shouldComponentUpdate來提高diff效能
diff演算法的三種優化策略
- 樹的優化策略
- 層級控制,diff樹同層只比較同一層次的節點
- 如果某個節點不存在,則這個節點直接刪除
- 跨層級比較會進行相應的建立或者刪除
- 元件的優化策略
- 是否為同一型別,有則使用tree diff
- 不是同一型別,則判斷為dirty component。從而替換掉整個元件下的子節點
- 對於同一型別的元件,可以使用shouldComponentUpdate來手動終止diff演算法,加快渲染速度
- 元素的優化策略
- 插入,全新的節點需要執行插入操作
- 移動,節點本身存在,需要進行移動
- 刪除,結構相同內容不同,無需直接複用,需要刪除重建
- 基於唯一標記key來進行計算,快速定位
react效能優化
- 利用shouldComponentUpdate來避免不必要的dom操作
- 使用key來幫助React識別列表子元件的最小變化
- React.memo
- SSR
- render裡面不要使用箭頭函式或者bind,應該再constructor中bind
react的坑點
- componentWillUnmounted
- this.setState不同步
- pureComponent如果prop是多層物件,則不更新
簡述Flux思想
Flux最大的特點就是,資料單向流動
- 使用者訪問View
- View發出使用者的Action
- Dispatcher收到Action,要求Store進行相應的更新
- Store更新後,發出一個“change”事件
- View收到“change”事件後,更新頁面
- 使用防抖需要保持事件引用
event.persist()
展示元件(Presentational component)和容器元件(Container component)之間有何不同
- 展示元件關心元件看起來是什麼,專門通過props來接收資料和回撥,一般不會有自己的狀態
- 容器元件關心元件如何運作,容器元件會為展示元件或者其他容器元件提供資料和行為,一般擁有自己的狀態,因為他是其他元件的資料來源
元件的狀態(state)和屬性(props)有何區別
- state是一種資料結構,用於元件掛載時所需的資料預設值。一般由使用者事件行為改變state
- props是元件的配置,由父元件傳給子元件,props不可改變。
什麼是高階元件
高階元件是一個以元件為引數並返回一個新組建的函式。HOC執行重用程式碼、邏輯和引導抽象。最常見是Redux的connect函式。HOC最好的方式是分享元件之間的行為。如果發現一個地方大量程式碼用來做一件事情,可以考慮重構為HOC
為什麼建議setState的引數是一個callback而不是物件
因為state和props的更新可能是非同步的,不能依賴他們去計算下一個state
除了在建構函式繫結this,還有其他辦法嗎
在回撥中可以使用箭頭函式,但是問題就是每次渲染都會建立一個新的回撥
建構函式中為什麼要呼叫super
因為super()
被呼叫之前,子類是不可以使用this的
React的三種構建元件的方式
React.createClass、ES6 class、無狀態函式
useEffect和useLayoutEffect的區別
呼叫時間不同
- useEffect是在元件渲染完之後執行
- useLayoutEffect是在元件渲染前就執行
useLayoutEffect比useEffect先執行
實現 useState 和 useEffect
React是使用類似單連結串列的形式來代替陣列,通過next按順序串聯所有hook
useEffect特點:
- 兩個引數:一個回撥函式,一個依賴陣列
- 如果依賴陣列不存在,則callback每次render都觸發
- 如果依賴陣列存在,只有他發生改變的時候才執行callback
let arr = [] // 一個陣列,儲存每一個hook
let index = 0 // 下標,存取每個hook對應的位置
function useState(init){
arr[index] = arr[index] || init // 如果存在則用之前的值
const cur = index // 設定一個變數儲存當前下標
function setState(newState){
arr[cur] = newState
render() // 渲染,頁面觸發更新
}
return [arr[index++], setState]
}
function useEffect(callback, deps){
const hasNoDeps = !deps // 如果沒有依賴,則直接每次更新
const lastDeps = arr[index] // 獲取上一次的依賴陣列
const hasChangeDeps = lastDeps
? !deps.every((e, i) => e === lastDeps[i]) // 如果每一項都相同
: true
if(hasNoDeps || hasChangeDeps){
callback()
arr[index] = deps
}
index++
}
複製程式碼
React新特性
- Lazy 和 Supense
import React, {Component, lazy, Suspense} from 'react'
const About = lazy(() => import(/*webpackChunkName: "about" */'./About.jsx'))
class App extends Component {
render() {
return (
<div>
// 我們需要用suspense來進行過渡,顯示一個loading
<Suspense fallback={<div>Loading...</div>}>
<About></About>
</Suspense>
// 這樣子是不行的,因為還沒有載入完成不知道顯示什麼
<!--<About></About>-->
</div>
);
}
}
export default App;
複製程式碼
- 錯誤邊界(Error boundaries)
class App extends Component {
state = {
hasError: false,
}
static getDerivedStateFromError(e) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>error</div>
}
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<About></About>
</Suspense>
</div>
);
}
}
複製程式碼
- React.memo React.memo適用於函式元件,不適用於class元件
與pureComponent
類似,但是pureComponent
提供的shouldComponentUpdate
只是對比傳入的props本身有沒有變化,如果內部的值變了,他也不會更新。例如
class Foo extends PureComponent {
render () {
console.log('Foo render');
return <div>{this.props.person.age}</div>;
}
}
person: {
count: 0,
age: 1
}
// person.age++, 本來應該觸發更新,但是不會因為props的第一級沒有發生變化
複製程式碼
這時候我們可以用memo來優化一下
const Foo = memo(function Foo (props) {
console.log('Foo render');
return <div>{props.person.age}</div>;
})
複製程式碼
memo的第一個引數是函式元件,第二個是與shouldComponentUpdate類似
是一個函式
(nextProps, nextState)=>{
}
複製程式碼
- hook的優勢
- 函式元件沒有this的問題
- 自定義hook方便複用狀態邏輯
- 副作用關注點分離
- 寫起來更加簡單、易懂
- hook的缺點
- hook的執行次數可能會比你預期的還要多
- 有點難以維護useEffect的依賴項
React 狀態提升
常見於子元件的資料存在父元件中
// 父元件
class AllInput extends React.Component {
constructor(props) {
super(props)
this.state = { content: '' }
this.handleContentChange = this.handleContentChange.bind(this)
}
handleContentChange(newContent) {
this.setState({ content: newContent })
}
render() {
return (
<div>
<Input content={ this.state.content } onContentChange={ this.handleContentChange }/>
<br /><br />
<Input content={ this.state.content } onContentChange={ this.handleContentChange }/>
</div>
)
}
}
// 子元件
class Input extends React.Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
handleChange(e) {
setState(修改資料的方法)
this.props.onContentChange(e.target.value)
}
render() {
return (
<input type='text' value={ this.props.content } onChange={ this.handleChange } />
)
}
}
複製程式碼
高階函式
高階函式就是接受一個或者多個函式然後返回一個函式的東西
主要功能有兩個
屬性代理
屬性代理有三個常用的作用
- 操作props
function HigherOrderComponent(WrappedComponent) {
return class extends React.Component {
render() {
const newProps = {
name: '大板慄',
age: 18,
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}
複製程式碼
這樣子,就可以將自定義的props傳給子元件了
- 抽離state
// 普通元件Login
import React, { Component } from 'react';
import formCreate from './form-create';
@formCreate
export default class Login extends Component {
render() {
return (
<div>
<div>
<label id="username">
賬戶
</label>
<input name="username" {...this.props.getField('username')}/>
</div>
<div>
<label id="password">
密碼
</label>
<input name="password" {...this.props.getField('password')}/>
</div>
<div onClick={this.props.handleSubmit}>提交</div>
<div>other content</div>
</div>
)
}
}
//HOC
import React, { Component } from 'react';
const formCreate = WrappedComponent => class extends Component {
constructor() {
super();
this.state = {
fields: {},
}
}
onChange = key => e => {
const { fields } = this.state;
fields[key] = e.target.value;
this.setState({
fields,
})
}
handleSubmit = () => {
console.log(this.state.fields);
}
getField = fieldName => {
return {
onChange: this.onChange(fieldName),
}
}
render() {
const props = {
...this.props,
handleSubmit: this.handleSubmit,
getField: this.getField,
}
return (<WrappedComponent
{...props}
/>);
}
};
export default formCreate;
複製程式碼
例如這樣,就可以將輸入框的state抽離出來
- 操作refs
import React, { Component } from 'react';
const refHoc = WrappedComponent => class extends Component {
componentDidMount() {
console.log(this.instanceComponent, 'instanceComponent');
}
render() {
return (<WrappedComponent
{...this.props}
ref={instanceComponent => this.instanceComponent = instanceComponent}
/>);
}
};
export default refHoc;
複製程式碼
這樣就可以獲取元件的示例了
不可以再無狀態元件(函式型別元件)上使用refs
,因為無狀態元件沒有例項
反向繼承
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
};
}
複製程式碼
反向繼承就是一個函式接收一個元件作為引數傳入,然後返回一個繼承該元件的類,在該類的render
裡面呼叫super.render()
- 可以操作state,但是會難以維護
- 渲染劫持
渲染劫持我們可以控制元件的render,有條件的展示元件 例如
- 條件渲染
function withLoading(WrappedComponent) {
return class extends WrappedComponent {
render() {
if(this.props.isLoading) {
return <Loading />;
} else {
return super.render();
}
}
};
}
複製程式碼
這樣就可以按條件來控制元件渲染了
- 修改render輸出
//hijack-hoc
import React from 'react';
const hijackRenderHoc = config => WrappedComponent => class extends WrappedComponent {
render() {
const { style = {} } = config;
const elementsTree = super.render();
console.log(elementsTree, 'elementsTree');
if (config.type === 'add-style') {
return <div style={{...style}}>
{elementsTree}
</div>;
}
return elementsTree;
}
};
export default hijackRenderHoc;
//usual
@hijackRenderHoc({type: 'add-style', style: { color: 'red'}})
class Usual extends Component {
...
}
複製程式碼
HOC存在的問題
- 靜態方法丟失
- refs屬性不能透傳
- 反向繼承不能保證子元件完整被解析
HOC約定
- props保持一致
- 不能再函式式(無狀態)元件中使用refs,因為他沒有
- 不要改變子元件
- 可以新增包裝名便於除錯
- render裡不要使用HOC
應用場景
- 許可權控制
- 頁面複用
React元件通訊
- 父元件傳子元件props
- 子元件呼叫父元件的回撥函式
- 使用context
- 使用EventEmitter
為什麼選擇React而不是Vue
- React是函式式思想,狀態邏輯什麼的都是通過引數傳入
- 通過js來操作一切,css什麼的也可以使用js的方式
- 社群十分強大,很多功能由社群一起努力做出
- UI庫很棒
- JSX讓元件變得可讀性更好,元件之間的結構看的比較清楚
- 支援服務端渲染,更好的SEO和渲染效能
React 與 MVVM 的關係
- M(odal): 對應元件中生命週期啊,state這些資料
- v(iew): 對應虛擬dom生成的真是dom,還有樣式等
- V(iew)-M(odal): 對應元件中的JSX會通過diff演算法將虛擬dom轉換為真實dom
React的單向繫結與雙向繫結的區別
一般只有UI表單控制元件需要雙向繫結。
- 單向繫結值UI表單變化的時候需要我們手動更新狀態,也就是設定監聽事件
onChange
等 - 雙向繫結則自動更新
在表單互動很多的時候,單向繫結資料更容易追蹤管理和維護,但是需要寫較多的程式碼
33. CSRF 和 XSS
區別
- CSRF需要登入之後操作,XSS不需要
- CSRF是請求頁面api來實現非法操作,XSS是向當前頁面植入js指令碼來修改頁面內容
CSRF
跨站點偽造請求
使用者在一個網站登入之後,產生一個cookie,此時若開啟了一個新的網址,此網址返回了一些惡意的請求,就屬於CSRF攻擊
- 預防
- 驗證http reference
- 請求地址中新增token驗證
- 請求頭中新增token驗證
XSS
跨站指令碼攻擊,一般是通過web頁面插入惡意js程式碼來攻擊。 主要分為三類:
- 反射性: xss程式碼在請求url中攻擊
- 儲存型: 將攻擊指令碼存入服務端,從而傳播
- dom型: 通過dom修改頁面內容
- 預防
- 輸入過濾: 例如過濾
<script>
等 - 輸出轉義: 例如將
<
,>
,/
等字元利用轉義符號轉換一下 - 使用httponly: 讓js指令碼無法訪問cookie
- 儘量使用post方法,使用get的時候限制一下長度
總結
- 加入token是為了防止CSRF的而不是XSS
- CSRF的攻擊是因為瀏覽器自動帶上cookie,而不會自帶token
34. localStorage 和 sessionStorage 和 cookie
localStorage
生命週期是永久,只能存字串型別
- 跨域 使用iframe與postMessage實現
sessionStorage
生命週期為,當前視窗或標籤頁未被關閉,一旦關閉則儲存的資料全部清空
cookie
cookie生命週期在過期時間之前都有效,同瀏覽器下,所有同源視窗之間共享
cookie的屬性
- name: 必須,定義cookie的名稱
- value: 必須,指定cookie的值
- expires: 指定過期時間,採用UTC或GMT格式,如果不設定cookie只會在當前會話(session)有效,瀏覽器視窗關閉cookie就會刪除
- domain: 指定cookie所在域名,預設為設定cookie時候的域名,所指定的域名必須是當前傳送cookie的一部分,只有訪問的域名匹配domain屬性,cookie才會傳送
- path: 指定路徑,必須是絕對路徑,預設為請求該cookie的網頁路徑,只有path屬性匹配向伺服器傳送的路徑,cookie才會傳送,只要path屬性匹配傳送路徑的一部分就可以了,例如
path=/blog
,傳送的路徑是/blogroll
也可以。path屬性生效的前提是domain屬性匹配 - secure: 指定cookie只能在加密協議https下傳送到伺服器,如果通訊是https協議,自動開啟
- max-age: 用來指定cookie有效期,max-age優先順序高於expires
- httponly: httponly屬性用於設定該cookie不能被JavaScript訪問,主要是為了防止xss工具盜取cookie
總結
- 不同瀏覽器無法共享localStorage和sessionStorage
- 相同瀏覽器不同頁面之間可以共享相同的localStorage(頁面屬於相同域名和埠)
- 相同瀏覽器不同標籤頁中無法共享sessionStorage的資訊
35. BFC 及其作用
BFC就是格式化上下文,相當於一個容器,裡面的元素和外部相互不影響,建立的方式有:
- html根元素
- float浮動
- 絕對定位元素position: fixed || absolute
- overflow不為visiable
- display為table、inline-block、flex
BFC主要的作用是
- 清除浮動
- 防止同一個BFC中相鄰元素間的外邊距重合
- BFC元素和浮動元素不會重疊
- BFC在計算高度時會把浮動元素也計算進去
36. 實現 (5).add(3).minus(2) 功能
Number.prototype.add = function(n) {
return this + n;
};
Number.prototype.minus = function(n) {
return this - n;
};
複製程式碼
37. 連續賦值
例1:
var a = 3;
var b = a = 5;
=
var a = 3
var a = 5
var b = a
複製程式碼
所以最後a和b都是5
例2:
var a = { n: 1 }
var b = a
a.x = a = { n: 2 }
複製程式碼
一開始a和b都引用物件{n:1}
,然後到賦值語句
如果賦值語句中出現.
則他會比=
先一步執行,也就是說,我們先給a.x開闢一個空間
也就是說現在的a和b都是變成了
{
n: 1,
x: {
n: 2
}
}
複製程式碼
接下來到a的賦值了,a現在就變成了
{
n: 2
}
複製程式碼
38. display: none, visibility: hidden 和 opacity: 0
display: none
: 不佔空間,不能點選,一般用於隱藏元素,會引發迴流,效能開銷大,非繼承屬性,子孫節點改變也無法顯示(父元素為none)visibility: hidden
: 佔據空間,不能點選,顯示不會導致頁面結構變化,不會撐開,屬於重繪操作,是繼承屬性,子孫節點可以通過修改顯示出來opacity: 0
: 佔據空間,可以點選,一般與transition一起用
39. for 和 forEach 的效能
for 比 forEact 快 因為
- for沒有額外的函式呼叫棧,和上下文
40. react-router的<Link>
和 <a>
有何區別
是禁用了標籤的預設事件,他只會更新匹配的頁面的內容,而不會重新整理整個頁面。他使用的是`history`來進行跳轉,會改變url,但是不會重新整理頁面
41. 執行上下文
- 全域性執行上下文:預設的上下文,任何不在函式內部的程式碼都在全域性上下文裡面。他會執行兩件事情: 建立一個全域性的window物件,並且設定this為這個全域性物件。一個程式只有一個全域性物件
- 函式執行上下文:每當一個函式被呼叫,就會為該函式建立一個新的上下文,每個函式都有自己的上下文,不過是在函式被呼叫的時候建立的。函式上下文可以有任意多個,每當一個新的執行上下文被建立,他會按照定義的順序執行一系列的步驟
- Eval函式執行上下文:執行在
eval
函式內部的程式碼有他自己的執行上下文
42. 執行棧
執行棧就是一個呼叫棧,是一個後進先出資料結構的棧,用來儲存程式碼執行時建立的執行上下文
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
複製程式碼
43. 建立執行上下文
分為兩個階段
- 建立階段
- 執行階段
建立階段
程式碼執行前會建立執行上下文,會發生三件事
- this繫結
- 建立詞法環境元件
- 建立變數環境元件
this繫結
全域性執行上下文中,this指向全域性物件
函式執行上下文中,this取決於函式是如何被呼叫的。如果他被一個引用物件調動,那麼this會設定成那個物件。否則是全域性物件
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因為 'baz' 被
// 物件 'foo' 呼叫
let bar = foo.baz;
bar(); // 'this' 指向全域性 window 物件,因為
// 沒有指定引用物件
複製程式碼
詞法環境
是一個用程式碼的詞法巢狀結構定義識別符號和具體變數和函式關聯的東西。用來儲存函式宣告和let
, const
宣告的變數繫結
變數環境
同樣是一個詞法環境,用來儲存var
變數繫結
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
複製程式碼
只有在呼叫函式multipy的時候才會建立函式執行上下文,let和const一開始不會關聯任何值,但是var已經會設定為undefined了,這就是常說的變數宣告提升
44. 事件迴圈
任務分為兩類
- 同步任務
- 非同步任務
巨集任務
- 主體程式碼
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
微任務
- process.nextTick
- mutation.observer
- Promise.then cath finally
45. link 與 @import
- link屬於html標籤,@import屬於css提供的
- 頁面載入的時候link就載入,而@import是等頁面載入完成之後再載入css
- link不存在相容性問題
- link權重更大
46. 外邊距重疊
- 兩個相鄰的外邊距都是正數時,摺疊效果是兩者之間較大的
- 兩個相鄰的外邊距都是負數時,摺疊效果是絕對值最大的
- 兩個外邊距一正一負時候,效果是相加起來
47. 漸進增強 和 優雅改進
- 漸進增強:針對低版本瀏覽器進行構建頁面,保證最基本的功能,然後再針對高階瀏覽器進行效果、互動等改進
- 優雅改進:一開始就構建好完整的功能,然後再對低版本瀏覽器進行相容
區別:漸進增強是向上相容,優雅改進是向下相容
48. 立即執行函式
作用
- 不破壞全域性的名稱空間
- 如需使用,要將變數傳入
(function(x){
console.log(x);
}(12345))
複製程式碼
49. HTTP2.0 與 HTTP1.1
HTTP2.0
- HTTP2.0基本單位為二進位制,以往是採用文字形式,健壯性不是很好,現在採用二進位制格式,更方便更健壯
- HTTP2.0的多路複用,把多個請求當做多個流,請求響應資料分成多個幀,不同流中的幀交錯傳送,解決了TCP連線數量多,TCP連線慢,所以對於同一個域名只用建立一個連線就可以了
- HTTP2.0壓縮訊息頭,避免了重複請求頭的傳輸,又減少了傳輸的大小
- HTTP2.0服務端推送,瀏覽器傳送請求後,服務端會主動傳送與這個請求相關的資源,之後瀏覽器就不用再次傳送後續的請求了。
- HTTP2.0可以設定請求優先順序,可以按照優先順序來解決阻塞的問題
HTTP1.1
- 快取處理新增etag、if-none-match之類的快取頭來控制快取
- 長連線,可以在TCP連線上傳送多個請求和響應
50. css動畫與js動畫的區別
- css效能好
- css程式碼邏輯相對簡單
- js動畫控制好
- js動畫相容性好
- js可以實現動畫多
- js可以新增事件
51. offset、client和scroll的屬性
- client:padding + content - 滾動條
- offset:padding + content + boder
- scroll:padding + 實際的content
52. 手撕程式碼題
- 輸入根節點,列印標籤路徑,以及路徑上data-v最大值
const root = document.getElementById("root");
function walk(node, path, max) {
if (!node) {
return;
}
path = [...path, node.tagName];
max = Math.max(max, node.dataset.v);
console.log(path, max);
for (let child of node.children) {
walk(child, path, max);
}
}
walk(root, [], -1)
...
<div id="root" data-v="3">
<p data-v="1">p1</p>
<span data-v="2">
span1
<span data-v="4">span2</span>
<span data-v="5">
<p>1</p>
</span>
</span>
<p data-v="99">p2</p>
</div>
複製程式碼
- 實現compose函式
var arr = [func1, func2, func3];
function func1(ctx, next) {
ctx.index++
next();
}
function func2(ctx, next) {
setTimeout(function () {
ctx.index++
next();
});
}
function func3(ctx, next) {
console.log(ctx.index);
}
compose(arr)({ index: 0 });
function compose(arr) {
return function (ctx) {
if (arr.length !== 0) {
let func = arr.shift()
func(ctx, next)
}
function next() {
if (arr.length !== 0) {
let func = arr.shift()
func(ctx, next)
}
}
}
}
複製程式碼
- 放大鏡
53. 行內元素的margin 和 padding
- 水平方向:水平方向上,都有效
- 垂直方向:垂直方向上,都無效,但是padding-bottom, padding-top會顯示出效果,但是高度不會撐開,不會對周圍元素有影響
54. 行內元素和塊級元素
- 行內、塊級元素可以相互轉換
- 行內元素會在一條水平線上排列,塊級則會在新的一行
- 行內元素不可以設定寬高,但是可以設定行高(line-height)
- 行內元素垂直方向上設定margin和padding無效,padding垂直方向有顯示但是不會撐開,不會影響別的元素
- 塊級可以包含行內元素和塊級元素,行內元素只可以包含行內元素和文字
55. margin、padding和translate百分比是按照什麼計算的
- margin:按照父元素的寬來計算的
- padding:按照父元素的寬來計算的
- translate:是按照本身的寬高計算的
56. display: inline-block元素之間有間隙
原因
inline-block
對外是inline
,對內是block
,會將連續的空白符解析為一個空格
解決辦法
- 刪除空格
- 父元素設定
font-size: 0
- 父元素設定
letter-spacing: -4px
- 子元素設定
vertical-align: bottom
去除垂直間隙
57. HTTPS原理
HTTP是明文傳輸,傳輸的每一個環節都可能會被第三方竊取或者篡改。具體來說就是HTTP資料經過TCP層,然後經過WIFI路由器、運營商和目標伺服器,都可能被中間人拿到資料並進行篡改,這就是常說的中間人攻擊
HTTPS是一個加強版的HTTP。採用對稱加密和非對稱加密結合的方式來保護瀏覽器和服務端之間的通訊安全。 對稱加密演算法加密資料 + 非對稱加密演算法交換金鑰 + 數字證照驗證身份 = 安全
HTTPS由兩部分組成:HTTP + SSL/TLS,也就是在HTTP上又加了一層處理加密資訊的模組。服務端和客戶端的資訊傳輸都會通過TLS進行加密,所以傳輸的資料是加密後的資料
加密過程
首先瀏覽器會給伺服器傳送一個client_random
和一個加密的方法列表
伺服器接受後給瀏覽器返回另一個隨機數server_random
和加密方法
現在兩者擁有三種相同的憑證: client_random
, server_random
和加密方法
接著用這個加密方法將兩個隨機數混合起來生成金鑰,這個金鑰就是瀏覽器和伺服器通訊的暗號
了
對稱加密和非對稱加密
-
對稱加密是最簡單的方式,指的是加密和解密用的是同樣的祕鑰,優點是保證了訊息的保密性,但缺點是金鑰可能會洩露
-
非對稱加密是說,如果有A、B兩把金鑰,用A加密過的資料包只能用B解密,反之用B加密的資料包只能由A解密,客戶端在發訊息的時候,先用公鑰加密,伺服器再用私鑰解密。但是因為公鑰不是保密的,可能會被竊取然後對訊息進行篡改
數字證照和數字簽名
為了解決非對稱加密中公匙來源的不安全性。我們可以使用數字證照和數字簽名來解決。
首先本地生成一對金鑰,通過公鑰去CA申請數字證照
-
數字簽名:CA拿到資訊後會通過單向的hash演算法把資訊加密,然後生成一個摘要,然後CA會用自己的私鑰對摘要進行加密,最後得出的結果就是數字簽名了
-
數字證照:最後CA會將申請的資訊還有數字簽名整合在一起,就生成了數字證照了,其實也就是一個公鑰
-
數字證照如何起作用:
- 客戶端用CA的公鑰解密數字證照,如果數字證照來源合法,解密成功後,客戶端就獲得了資訊摘要了。然後客戶端會按照和CA一樣的hash演算法將申請資訊生成一份摘要。
- 然後和解密出來的那份摘要進行對比,如果內容完整,則說明沒有被篡改。客戶端就可以從證照中拿到伺服器的公鑰和伺服器進行安全的非對稱加密通訊了。伺服器要想獲得客戶端的公鑰也可以通過這個方式
握手過程
- 客戶端申請https通訊傳送一個隨機數,還有加密方法
- 伺服器響應然後向客戶端傳送數字證照(公鑰)還有隨機數給客戶端
- 客戶端TLS驗證證照,拿到公鑰,如果沒有問題就生成一個隨機值,然後用公鑰加密傳給伺服器
- 伺服器收到後用私鑰解密,拿到了對稱金鑰,之後可以通過這個金鑰進行資訊傳輸了,然後建立SSL連線通道
- 共享金鑰交換成功,客戶端和服務端開始加密通訊
- 斷開連線
客戶端: 你好,我要發起一個 HTTPS 請求,請給我公鑰
伺服器: 好的,這是我的證照,裡面有加密後的公鑰
客戶端: 解密成功以後告訴伺服器: 這是我的 (用公鑰加密後的) 對稱祕鑰。
伺服器: 好的,我知道你的祕鑰了,後續就用它傳輸。
複製程式碼
使用
https加解密耗時比較長,也很消耗資源,如果不是安全性要求非常高可以不用
58. setImmediate() 和 process.nextTick()
- setTimeout為在指定時間之後就執行
- setImmediate設計是當前輪詢階段完成後就立即執行 如果放在同一個I/O迴圈內使用,setImmediate總是被優先呼叫,他的優勢是如果在I/O週期內被排程的話,他會在所有定時器之前執行。他執行的方式是當前任務佇列的尾部
- process.nextTick觸發的是在當前執行棧的尾部,下一次事件迴圈之前,總是在非同步之前。
為什麼要使用process.nextTick(),因為可以允許使用者處理錯誤,或者是在事件迴圈繼續之前重試請求
59. 瀏覽器和node的事件迴圈
瀏覽器任務分為巨集任務和微任務
- 巨集任務:script中的程式碼、setTimeout、setInterval、I/O、UI render
- 微任務:promise、Object.observe、MutationObserver
node分為以下幾種:
- microTask:微任務
- nextTick:process.nextTick
- timers: 各類定時器
- I/O callback:是否有已完成的I/O操作的回撥函式,來自上一輪的輪訓的殘留,執行幾乎所有的回撥,但是不包括close,定時器還有setImmediate
- idle,prepare:僅在內部使用
- poll:等待新的I/O事件,會因timers和超時事件等結束時間,一般阻塞在這裡
- check:執行setImmediate的回撥
- close callback:關閉所有的closing handles,一些onclose事件
Node環境下
迴圈之前,會執行以下操作:
- 同步任務
- 發出非同步請求
- 規劃定時器生效的時間
- 執行process.nextTick()
開始迴圈 迴圈中進行的操作:
- 清空timers佇列,清空nextTick佇列,清空microTask佇列
- 清空I/O佇列,清空nextTick佇列,清空microTask佇列
- 清空check佇列,清空nextTick佇列,清空microTask佇列
- 清空close佇列,清空nextTick佇列,清空microTask佇列
區別
- node環境下的setTimeout定時器會依次一起執行,瀏覽器是一個一個分開的
- 瀏覽器環境下微任務的執行是每個巨集任務執行之後,而node中微任務會在各個階段之間執行,一個階段結束立刻執行mircroTask
總結
瀏覽器環境下:
while(true){
巨集任務佇列.shift()
微任務佇列全部任務()
}
複製程式碼
Node環境下:
while(true){
loop.forEach((階段)=>{
階段全部任務()
nextTick全部任務()
microTask全部任務()
})
}
複製程式碼
60. React Fiber
原先React採用的是stack reconciler排程演算法。頁面可能會出現卡頓,因為setState之後React會立即開始調和過程,會遍歷找不同,所有virtual dom都遍歷完之後,才會渲染。在調和的過程時候,主執行緒被js佔用了,所以互動、佈局都會停止,就會卡頓
所以現在採用了Fiber reconciler來優化
- 任務拆分
- 更新暫停,終止和複用
- 設定優先順序
- render可以返回多個元素
Scheduler
排程是fiber調和的一個過程
如果UI要有不錯的表現,那就得考慮多點東西
- 並不是所有state都需要立刻更新,例如不在可視範圍內的
- 不是所有更新優先順序都是一樣的,例如使用者輸入的優先順序比請求資源展示要高
- 高優先順序的應該打斷低優先順序的更新
所以調和的過程是每次只做一個小小的任務,完成之後檢視還有沒有優先順序高的任務需要處理
Fiber資料結構
- return: 父節點
- child:第一個子節點
- sibling:兄弟節點
fiber {
stateNode: {},
child: {},
return: {},
sibling: {},
}
複製程式碼
任務拆分
Fiber拆分的單位是按照虛擬dom節點來拆,因為fiber tree是根據虛擬dom來構造出來的
任務暫停
Fiber利用瀏覽器的apirequestIdleCallback
來讓瀏覽器空閒的時候執行某種任務
function fiber(剩餘時間) {
if (剩餘時間 > 任務所需時間) {
做任務;
} else {
requestIdleCallback(fiber);
}
}
複製程式碼
使用requestAnimationFrame
來渲染動畫
優先順序
{
Synchronous: 1, // 同步任務,優先順序最高
Task: 2, // 當前排程正執行的任務
Animation 3, // 動畫
High: 4, // 高優先順序
Low: 5, // 低優先順序
Offscreen: 6, // 當前螢幕外的更新,優先順序最低
}
複製程式碼
61. webpack流程
- 初始化:從配置檔案讀取與合併引數,然後例項化外掛
new Plugin()
- 開始編譯:通過上一步獲取的引數,初始化一個
Complier
物件載入外掛,執行Compiler.run
開始編譯 - 確定入口:根據配置中
entry
找出所有入口檔案 - 編譯模組:從
entry
出發,呼叫配置的loader
,對模組進行轉換,同時找出模組依賴的模組,一直遞迴,一直到找到所有依賴的模組 - 完成模組編譯:這一步已經使用
loader
對所有模組進行了轉換,得到了轉換後的新內容以及依賴關係 - 輸出資源:根據入口與模組之間的依賴關係,組裝成
chunk
程式碼塊,生成檔案輸出列表 - 輸出成功:根據配置中的輸出路徑還有檔名,把檔案寫入系統,完成構建
62. webpack的熱更新
主要依賴webpack
, express
, websocket
- 使用
express
啟動本地服務,當瀏覽器訪問的時候做出相應 - 服務端和客戶端使用
websocket
實現長連線 webpack
監聽原始檔的變化- 每次編譯完成之後會生成hash值,已改動模組的json檔案,已改動模組程式碼的js檔案
- 編譯完成後通過
socket
向客戶端推送當前編譯的hash值
- 客戶端的
websocket
監聽到有檔案改動推送過來的hash值,會和上一次進行對比- 一致就走快取
- 不一致則通過ajax和jsonp獲取最新的資源
- 使用記憶體檔案系統去替換有修改的內容實現區域性更新
63. CommonJs 與 ES6模組化區別
- CommonJs支援動態匯入,ES6不支援
- CommonJs是同步匯入,ES6是非同步匯入
- CommonJs匯入的時候是值拷貝,如果匯出變了他也不會變,但是ES6是引用的,如果匯出變了,匯入的部分也會變了
用法
- 匯出:CommonJs是
module.exports
,ES6是export / export default
- 匯入:CommonJs是
const x = require('xx')
,ES6是import xx from 'xxx'
64. 箭頭函式和普通函式
普通函式
- 一般this指向全域性物件window
- 作為物件方法呼叫的時候,this指向物件
- 作為建構函式時候,this指代new的物件
- 可以通過call、apply來改變this
箭頭函式
- this總是指向詞法作用域
- 無法通過call、apply改變this
65. node讀取檔案轉換為buffer
const fs = require('fs')
const path = require('path')
const mimeType = require('mime-types') // 檔案型別
const filePath = path.resolve('./01.png')
const data = fs.readFileSync(filePath)
const buffer = new Buffer(data).toString('base64')
const base64 = `data:${mimeType.lookup(filePath)};base64,${buffer}`
console.log(base64)
複製程式碼
66. sort函式
- 數量小於10的陣列使用插入排序
- 數量大於10的陣列使用快排
67. 閉包 和 自執行函式
閉包
優點
- 可以將區域性變數一直存在記憶體中
- 可以避免使用全域性變數
缺點:
- 佔用記憶體變多,可能導致記憶體洩漏
自執行函式
- 避免作用域命名汙染
- 提升效能,減少對作用域的查詢
- 避免全域性命名衝突
- 儲存閉包狀態
區別
- 立即執行函式宣告完立刻執行,一般只用於一次
- 閉包主要是為了讓外部函式可以訪問內部變數,減少了全域性變數的使用
68. 0.1 + 0.2 !== 0.3
js採用IEEE 754的雙精度標準來進行計算,如果他碰到無法整除的小數的時候,就會取一個近似值,如果足夠接近就覺得是那個值了
69. React服務端渲染
服務端渲染就是React通過Node.js轉換成HTML再返回給瀏覽器,這個過程被稱為“同構”,因為應用程式的大部分程式碼都可以在伺服器和客戶端上執行
優點
與傳統的SPA單頁應用程式比,
- 更好的SEO,因為搜尋引擎等爬蟲工具可以直接檢視完全渲染的頁面
- 更好的使用者體驗,如果網路緩慢,或者執行緩慢的裝置,伺服器渲染就很好
弊端
- 由於瀏覽器跟服務端有區別,document,window等可能獲取不了,會報錯
- 伺服器會佔用更多的記憶體
70. 前端路由的原理
- hash模式,後面帶了個#,無論hash值怎麼變,服務端都只會收到#號前的url,通過監聽
hashchange
事件,根據hash來切換頁面 - history模式,主要使用history.pushState和history.replaceState來改變URL,改變url只會更新瀏覽器的歷史記錄,不會引起頁面更新,通過監聽
popstate
來改變頁面內容,同時禁止a標籤的預設事件。
hash模式不需要後端配置,相容性好,history模式需要後端配置才能使用
71. Mobx原理
特點:- 開發難度低
- 程式碼量少
- 渲染效能好
工作原理
- 定義狀態讓他變成可觀察的
- 建立衍生來響應狀態變化
- 使用action來改變狀態
設計原則
Mobx支援單向資料流,動作改變狀態,狀態更新改變檢視
- Mobx通過不快取資料,在需要的時候重新計算來保證所有衍生在一致的狀態
- 沒有參與反應的衍生,就會被簡單的垃圾回收
Mobx是同步執行的,有2個好處
- 不會獲得舊的衍生
- 追蹤堆疊會更簡單
主要利用一個autoRun
,這個函式可以讓被我們用到的屬性改變時候觸發回撥,而沒被使用的屬性發生改變則不會發生回撥。
72. Object.create()實現原理
其實就是新建一個物件,然後覆蓋物件的原型為傳入的原型物件(類似繼承)
function(constructor){
const F = function(){}
F.prototype = constructor
const res = new F()
return res
}
複製程式碼
73. 事件穿透
設定css3屬性pointer-events: none
74. 常見http狀態碼
- 200:成功,正常處理並返回
- 204:處理成功,但是返回的主體資訊沒有內容
- 301:永久重定向,請求的資源分配給另一個url
- 302:臨時重定向,希望本次訪問可以通過另一個url來獲取資源
- 303:應該使用get來訪問另一個url
- 304:表示協商快取可用
- 400:請求中有語法錯誤
- 401:未經許可,要驗證
- 403:拒絕訪問,許可權不夠
- 404:訪問資源不存在
- 500:請求異常,也可能是前端bug
- 503:伺服器停機維護,無法處理請求
75. js、css阻塞
js的async載入還有defer載入都不阻塞頁面渲染還有資源載入
- defer會按順序載入,async亂序
- defer是頁面都解析完了在執行,立即下載延遲執行
- async下載完成後立刻執行
- 同時指定的話,async優先順序高
內嵌和外部js
- 內嵌js會阻塞所有資源的載入
- 外部js會阻塞後面的內容呈現以及資源的下載
css
- css不會阻塞dom解析,會阻塞渲染,因為要dom樹跟style樹結合生成渲染樹才會渲染
- css會阻塞後續js語句執行
如果css後面跟著內嵌js,則會出現阻塞情況,因為瀏覽器會維持css跟js順序,樣式表必須在嵌入的js之前載入完。
載入順序
- js在head中會立即執行,阻塞後續資源下載與執行。因為js可能會修改dom,不阻塞的話,dom操作順序不可控。
- js的執行依賴css。只有前面的css全部下載完,才會執行js
注意
- css應放在head中,這樣可以儘快響應,渲染頁面,因為他不會阻塞資源下載
- js應該放在body底部,讓dom儘快渲染出來,避免dom被阻塞
- css會阻塞js執行,他不會阻塞js下載,但是js會等到css載入玩之後才會執行
JS 程式碼在執行前,瀏覽器必須保證在 JS 之前的所有 CSS 樣式都解析完成,不然不就亂套了,前面的 CSS 樣式可能會覆蓋 JS 檔案中定義的元素樣式,這是 CSS 阻塞後續 JS 執行的根本原因。
76. 隱式轉換
- 數字運算子
- 點號操作符
- if語句內
- = =
77. React首屏優化
- 使用瀏覽器快取
- webpack的JS壓縮,html壓縮
- 提取公共資源
- 將webpack開發環境修改為生產環境
- 使用雪碧圖
- 使用cdn
78. Object.assign
如果物件只有一層,則是深拷貝,如果是多層則是淺拷貝
const obj1 = {
a: 1,
b: {
c: 2
}
}
const obj2 = Object.assign({}, obj1)
console.log(obj1 == obj2)
console.log(obj1, obj2)
obj1.b = 2
console.log(obj1 == obj2)
console.log(obj1, obj2)
複製程式碼
79. innerHtml 和 outerHtml
- inner只是標籤內,outer是包括標籤
80. rem 和 em
- rem是根據根元素的字型大小定的
- em是根據自身元素的字型大小定的,如果自身沒有,那可以從父元素繼承字型大小
81. 垃圾回收機制 和 記憶體洩漏
垃圾回收
- 標記清除
最常用的垃圾回收方式,執行的時候他會給變數標記,如果環境中的變數已經無法訪問到這些變數,就清除
var m = 0,n = 19 // 把 m,n,add() 標記為進入環境。
add(m, n) // 把 a, b, c標記為進入環境。
console.log(n) // a,b,c標記為離開環境,等待垃圾回收。
function add(a, b) {
a++
var c = a + b
return c
}
複製程式碼
例如函式的區域性變數和引數,外部訪問不了,則就會被清除
- 引用計數
判斷資源被引用的次數
var arr = [1, 2, 3, 4];
arr = [2, 4, 5]
複製程式碼
例如一開始[1,2,3,4]
的引用次數為1,不會被清除,但是下面吧arr的引用換了,[1,2,3,4]
的引用數變成0,將被清除
記憶體洩漏
- 意外的全域性變數
function foo(arg) {
bar = "this is a hidden global variable";
}
複製程式碼
這樣子bar會宣告在全域性變數裡面,不會被釋放
-
計時器 如果使用
setInterval
而沒有關閉,則他會一直存在不會被釋放 -
閉包 閉包會維持區域性變數,就無法釋放了
-
沒有清理dom引用 dom元素的引用如果不清除,他就會一直存在
優化
- 陣列優化
var arr = [1,2,3]
arr.length = 0 // 這樣子優化可以不用新生成一個空陣列
複製程式碼
- 物件複用
var t = {}
while(){
t.a = 1
}
t = null
複製程式碼
儘量複用物件不要每次都生成,然後吼用完設定為null,垃圾回收
- 迴圈使用函式表示式
function t(){
}
while(){
t()
// var t = function(){} 這樣子就不用迴圈建立很多函式了
}
複製程式碼
82. 手寫Promise 和 Promise.all
Promise
以下為簡略版
function myPromise(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
}
};
let reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
myPromise.prototype.then = function (onFulfilled, onRejected) {
// 宣告返回的promise2
let promise2 = new Promise((resolve, reject) => {
if (this.state === 'fulfilled') {
onFulfilled(this.value)
};
if (this.state === 'rejected') {
onRejected(this.reason)
};
if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason)
})
}
});
// 返回promise,完成鏈式
return promise2;
}
new myPromise((resolve, reject) => {
console.log('1')
resolve(1)
console.log('2')
}).then((res) => {
console.log(res)
console.log('4')
return 2
})
複製程式碼
Promise.all
const myPromiseAll = function (promiseList){
const result = []
return new Promise((resolve, reject) => {
let index = 0
next()
function next(){
promiseList[index].then((res) => {
result.push(res)
index++
if(index === promiseList.length){
resolve(result)
} else {
next()
}
})
}
})
}
複製程式碼
83. post的方法
- application/x-www-form-urlencoded (url傳引數)
- multipart/form-data (上傳檔案)
- application/json (傳json)
- text/xml
84. http options預檢請求
正式跨域之前,會發起option請求,預檢一下。檢測傳送的請求是否安全,同時傳送請求地址,請求方法,伺服器判斷來源域名是否再許可名單內,請求方法支不支援,支援則允許請求
複雜請求才會傳送預檢,以下為複雜請求
- put/delete/patch/post的其中一種
- 傳送json格式(content-type: application/json)
- 請求中有自定義頭部
為什麼要進行預檢? 複雜請求可能會對伺服器產生副作用,資料修改等。所以要檢測一下請求來源在不在許可名單上
85. oop三大特點
- 封裝
- 繼承
- 多型
86. 手寫限制併發數
function limitPromise(taskList, limit) {
return new Promise((resolve, reject) => {
let index = 0
let active = 0
let finish = 0
let result = []
function next() {
if (finish >= taskList.length) {
resolve(result)
return
}
while (active < limit && index < taskList.length) {
active++
let cur = index
taskList[index].then((res) => {
active--
finish++
result[cur] = res
next()
})
index++
}
}
next()
})
}
複製程式碼
87. webpack的tree shaking
tree shaking是用於清除無用程式碼的方式,webpack3/4開始之後就自動整合
88. getComputedStyle 和 style
- getComputedStyle只讀,style可讀可寫
- style只能獲取內聯樣式
window.getComputedStyle(dom, null).getPropertyValue('display')
89. 怪異盒模型
-
標準盒模型:大小 = content + margin + padding + border
-
怪異盒模型:大小 = width(content + padding + border) + margin
-
content-box: 預設,標準盒模型
-
border-box:border和padding算入width中
.container {
display: flex;
width: 200px;
height: 50px;
}
.item1 {
width: 50%;
background: red;
border: 10px solid black;
}
.item2 {
width: 50%;
background: yellow;
}
<div class="container">
<div class="item1">1</div>
<div class="item2">2</div>
</div>
複製程式碼
效果如圖
假設現在兩個div一行放置,都是50%寬,假如我們再期中一個加上邊框,這樣另一個就會被擠壓
這時候我們可以將box-sizing設定為border-box
.item1 {
width: 50%;
background: red;
border: 10px solid black;
box-sizing: border-box;
}
複製程式碼
擠壓的問題就沒有啦
90. Object.defineProperty
Object.defineProperty(obj, prop, descriptor)
MDN描述:
obj要在其上定義屬性的物件。
prop要定義或修改的屬性的名稱。
descriptor將被定義或修改的屬性描述符。
複製程式碼
- value:設定值
- writable:是否可被重寫
- enumberable:是否可列舉(for..in, Object.keys)
- set/get
- configrable:是否可重定義
91. HTTP如何複用連線
設定http的header:Connection: keep-alive
,告訴伺服器,待會我還要請求,就可以避免再次三次握手了
92. HTTP和TCP的關係
- http是應用層,tcp是傳輸層
- http是基於tcp的基礎上實現的
- tcp只是單純的進行連線,http是會進行收發資料
93. node的優勢
- 完全是js語法
- 高併發能力很好
- 非阻塞I/O
- 開發週期短
- 單程式、單執行緒
94. HTTP報文結構
- 請求報文:請求行,請求頭,空行,請求資料
- 響應報文,狀態行,訊息頭,空行,響應正文
請求頭
- 請求方法:get/post
- host:請求主機
- connection:連線狀態
- cache-control:快取
- accept:能傳送的型別
- user-agent:代理
- reference:請求資源的url
- accept-encoding:支援的編碼方式
- cookie:cookie
響應頭
- date:響應時間
- last-modify:最後修改時間
- etag:資源標識
- connection:連線狀態
- content-type:資源型別
95. async / await
async
async總是返回promise
- 沒有顯示return,則返回promise.resolve(undefined)
- 返回不是promise的話,則返回promise.resolve(data)
- 返回promise就會得到promise
await
- await只可以用在async裡面
- 如果await後是非promise,則獲取裡面resolve的值
與generator的區別
async就是generator的語法糖
async對比generator的改進
- 內建執行器,不需要next來執行
- 更好的語義化
- 返回promise,可以繼續操作
96. 是否每一次請求都會三次握手
如果沒有快取的情況下,請求頭設定connection:keep-alive
則可以不重新握手
97. TCP的keepAlive和HTTP的keep-alive
-
TCP的keepAlive是側重於保持客戶端和服務端的連線,會不定時傳送心跳包驗證有沒有斷開連線,如果沒有這個機制的話,一方斷開,另一方不知道,就會對伺服器資源產生較大的影響
-
HTTP的keep-alive可以讓伺服器客戶端保持這個連線,不需要重新tcp進行連線了
98. TCP兩次握手可不可以呢
假如第一次發出連線請求,如果伺服器端沒有確認,客戶端再進行一次請求,然後他們開始連線,在這之後,如果第一次只是延遲了,然後現在傳送給客戶端,如果沒有三次握手的話,就直接開啟連線,然而客戶端並沒有資料要傳輸,則服務端會一直等待客戶端發資料
99. TCP三次握手中可以傳輸資料嗎
第一次第二次不可以,因為很容易被人攻擊,而第三次握手已經進入establish狀態了,已經確認雙方的收發能力,可以傳輸
100. 2MSL等待狀態
MSL是報文最大生存時間,客戶端進入timewait的狀態之後,需要等待2msl的時間,這樣可以讓最後傳送的ack不會丟失,保證連線正常關閉,time_wait就是為了防止最後ack重發可能丟失的情況
101. websocket
http連線具有被動型,只能主動去向伺服器請求看有沒有新訊息,一直輪詢。
websocket是一個持久化的協議,連線了就不會自動斷開。而且傳遞資訊的時候只需要傳遞一點點頭資訊即可。一次握手,持久連線
- 先進行TCP三次握手
- TCP連線建立成功後,瀏覽器通過http協議傳送websocket支援的版本號,地址等資訊給服務端
- 服務端接受後,如果協議版本匹配,資料包格式正確,則接受連線(Upgrade: websocket,Connection:Upgrade)
- 瀏覽器收到回覆後,觸發onopen事件,就可以通過send傳送資料了
102. hasOwnProperty
這個方法只會遍歷自身屬性,不會從原型鏈上遍歷,可以用來判斷屬性是否在物件上或者是原型鏈繼承來的
const obj = new Object()
obj.num = 1
Object.prototype.fn = function(){
console.log(2)
}
obj.hasOwnProperty(num) // true
obj.hasOwnProperty(fn) // false
複製程式碼
103. DNS域名解析
域名解析過程
- 檢查瀏覽器快取中有沒有該域名的ip地址
- 檢查本機快取中有沒有該域名的ip地址
- 向本地域名解析服務系統(LDNS)發起域名解析請求
- 向根域名解析伺服器發起域名解析請求
- 根域名伺服器返回gTLD域名解析伺服器地址
- 向gTLD伺服器發起解析請求
- gTLD伺服器返回Name Server伺服器
- Name Server伺服器返回IP地址給本地伺服器
- 本地域名伺服器快取解析結果
- 返回解析結果給使用者
域名解析方式
- A記錄:A代表
address
,用來指定域名對應的ip地址 - MX記錄:可以將某個域名下的郵件伺服器指向自己的Mail Server
- CNAME記錄:別名解析,可以將指定域名解析到其他域名上
- NS記錄:為某個域名指定特定的DNS伺服器去解析
- TXT記錄:為某個主機名或域名設定指定的說明
104. JWT原理
使用JWT
伺服器就不需要儲存session資料了,更容易擴充套件
資料結構
由三部分組成,中間以.
分隔
- header
- payload
- signature
header
{
"alg": "HS256",
"typ": "JWT"
}
複製程式碼
alg代表演算法,typ代表型別,最後用base64編碼轉換一下變成字串
payload
除了官方給的欄位,我們還可以自定義欄位,例如:
{
"name": "bb",
"age" : 21
}
複製程式碼
signature
這是一個簽名,用於防止前面的資料被篡改
我們需要自定義一個 金鑰 ,不能給使用者知道,然後生成簽名
使用方式及特點
使用方式
- 客戶端收到後可以存入cookie或localStorage
- 每次請求時候可以在自定義請求頭加上
Authorization
特點
- 預設不加密,可以自己通過金鑰再加密一下
- 不能儲存session狀態,在有效期前一直有效
- 儘量使用https防止洩露
105. 什麼是DOCTYPE及其作用
DOCTYPE是用來宣告文件型別和DTD規範的。主要用於驗證檔案合法性。如果要提高瀏覽器的相容性,需要設定一下<!DOCTYPE>
106. koa原理
koa是基於express來開發出來的,但是他更簡潔,自由度更高,十分輕量。功能都通過外掛來實現,這種拔插式的架構設計模式,很符合unix哲學
koa2不在使用generator,而是採用了async/await
與 express 的區別
express:
- 整合了更多功能
- 但是回撥函式十分不便
koa:
- 使用async/await更加方便
- 需要自己配置外掛中介軟體等
結構
application.js
這個是入口檔案,繼承了events可以執行事件監聽還有觸發事件。
- listen是通過對http.createServer的封裝,傳入一個回撥函式,包含了中介軟體、上下文還有res等
- use是收集中介軟體,將多箇中介軟體放進快取佇列中,用koa-compose組合
context.js
這個就是koa的應用上下文ctx了,可以通過他來訪問請求頭啊設定響應頭等等
request/response.js
對原生的req,res等做一下封裝,可以在這裡取到headers設定headers/body等
洋蔥模型
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "hello world";
console.log(4);
});
複製程式碼
輸出 1 2 3 4 5 6
意思就是說,先從上到下執行完await next()
之前的內容,然後再從下到上執行await next()
之後的內容
koa通過use函式,吧中介軟體push到佇列中,洋蔥模型可以讓所有中介軟體依次執行,每執行完一次,就通過next()
傳遞ctx引數
next()
就相當於一個接力棒,將引數等傳遞給下一個中介軟體。只需要執行一下next就可以將之後的中介軟體都執行了!
不同於express框架,修改響應需要等請求結束之後,但用koa就可以將修改響應的程式碼放到next後面就可以了
前期處理 -> 交給並等待其他中介軟體處理 -> 後期處理
107. babel基本原理
什麼是babel
babel是一個js的編譯器,用來將程式碼轉換成相容瀏覽器執行的程式碼
- 語法轉換
- 通過polyfill的方式新增缺失的特性
- js原始碼轉換
babel基本原理
核心就是AST抽象語法樹。
- parsing(解析):首先將原始碼轉成抽象語法樹,詞法分析,語法分析
- transforming(轉換):將抽象語法樹處理生成新的語法樹
- generating(生成):將新的語法樹生成js程式碼
108. npm版本號
版本號
版本號分為X.Y.Z三位
- 大變動,向下不相容,需要更新X
- 增加功能,向下相容,更新Y
- 修復bug,更新Z
具體符號
- ~: 大約匹配,固定Y,Z隨意變動
- ^: 相容某個版本,固定X, Y, Z變動
- x: 任意版本,例如
1.x.2
,就可以是1.4.2
,1.8.2
109. git rebase 和 merge
- rebase:會取消分支的每一個提交,然後更新到最新分支,找到兩個分支第一個相同的提交,然後提取之後所有記錄,新增到目標分支的後面,
- merge: 將兩個分支,合併提交為一個分支,並且提交兩個commit。新建一個commit物件,吧兩個分支以前的記錄都指向新commit物件,會保留之前所有的commit
110. React-Router 原理
主要依賴的是history
庫。
頂層router監聽history,history發生變化的時候,router呼叫setState把location向下傳遞。設定到RouterContext。router根據RouterContext決定是否顯示。
Link標籤禁用a標籤的預設事件,呼叫history.push方法來改變url。
111. document.readyState
- loading:dom還在載入
- interactive:dom載入完了,但是圖片,css等還在載入
- complete:全部載入完了
112. symbol
symbol函式會返回symbol型別的值,他不支援new。
- 解決屬性名衝突
- 私有屬性
113. Samsite cookie
cookie的屬性SameSite
有三個值
- strict:完全禁止第三方cookie,跨站點時,任何情況都不會傳送cookie。
- lax:大多數情況禁止第三方cookie,但是導航到目標網址的get請求除外(例如a標籤,預載入請求,get表單)
- none:關閉samesite屬性,但必須設定
secure
才能生效
114. async 和 promise的區別
- 更加簡潔
- 可以使用try/catch來進行錯誤處理
- 可以很方便的使用中間值傳遞給下一個promise,不需要像promise那樣一直then
- 容易追蹤錯誤,報錯會具體到哪一行
115. import 和 require如何在瀏覽器實現
import
需要用babel進行轉換,轉換成commonJS規範
export
例如export default
會被轉換成exports.default
// babel編譯前
export default {}
// babel編譯後
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = {};
/*
* exports
* {default: {} , __esModule: true}
*/
複製程式碼
import
使用_interopRequireDefault
來處理一遍,為了相容和區分commonjs和es6
// babel編譯前
import Test, {msg, fn} from 'test'
// babel編譯後
'use strict';
var _test = require('test');
var _test2 = _interopRequireDefault(_test);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
複製程式碼
webpack中會為
import
和export
新增一個__esModule: true
的標識,來標記這是es6的東東
116. target 和 currentTarget的區別
- target:返回觸發事件的源物件
- currentTarget:返回事件繫結的物件
document.getElementById('test').addEventListener('click', function (e) {
console.log(e.target)
console.log(e.currentTarget)
})
<div id="test">
<button>1</button>
</div>
複製程式碼
117. 為什麼javascript是單執行緒的
- 防止dom渲染衝突的問題
- html5中的web worker可以實現多執行緒
118. DNS預解析
- html原始碼下載完後,會解析頁面包含的連結的標籤,提前查詢對應的域名
- 對訪問過的頁面,會記錄一個域名列表,再次開啟下載html的時候會同時解析DNS
預解析某域名
<link rel="dns-prefetch" href="//img.alicdn.com">
強制開啟HTTPS下的DNS預解析
<meta http-equiv="x-dns-prefetch-control" content="on">
複製程式碼
119. 什麼是bundle、chunk、module
- bundle是指webpack打包出來的檔案
- chunk是指webpack進行模組依賴分析的時候,程式碼分隔出來的程式碼塊
- module是開發中的單個模組
120. 什麼是loader、plugin
- loader是webpack可以載入解析非js檔案的能力
- plugin可以擴充套件webpack得功能,更加靈活。
121. 元件懶載入原理
- 例如
react-loadable
主要利用import()
來返回一個promise的性質,進行loading的非同步操作。 - webpack通過
require.ensure()
來實現按需載入
require.ensure
require.ensure(dependencies,callback,errorCallback,chunkName)
他會返回一個promise
,先是判斷dependencies
是否被載入過,載入過則獲取快取值。沒有的話就生成一個promise
,然後快取起來。接著生成一個script
標籤,填充好資訊放到html檔案上,就完成了按需載入了。
require.ensure可以自定義檔名
122. HTML5離線快取
利用manifest
屬性,當瀏覽器發現頭部有manifest
屬性,就會請求manifest檔案,如果是第一次,則根據檔案內容下載響應的資源。如果已經離線快取過了,則直接使用離線的資源載入。
123. 點選劫持,如何防範
攻擊者通過iframe巢狀的方式,將iframe設為透明,誘導使用者點選。
可以通過http頭設定X-FRAME-OPTIONS
來防禦iframe巢狀的點選劫持攻擊
124. 觀察者模式 與 釋出訂閱模式
觀察者模式
function Subject() {
this.state = 0
this.observers = []
}
Subject.prototype.attach = function (observer) {
this.observers.push(observer)
}
Subject.prototype.getState = function () {
return this.state
}
Subject.prototype.setState = function (state) {
this.state = state
this.noticefyAllObservers()
}
Subject.prototype.noticefyAllObservers = function () {
this.observers.forEach(observer => {
observer.update()
})
}
function Observer(name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)
}
Observer.prototype.update = function () {
console.log(this.name, this.subject.getState())
}
const subject = new Subject()
const o1 = new Observer('o1', subject)
const o2 = new Observer('o2', subject)
const o3 = new Observer('o3', subject)
subject.setState(1)
subject.setState(2)
subject.setState(3)
複製程式碼
釋出訂閱模式
function Event() {
this.events = {}
}
Event.prototype.on = function (event, fn) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(fn)
}
Event.prototype.emit = function (event) {
const args = [...arguments].splice(1)
this.events[event].forEach(fn => {
fn.apply(this, args)
})
}
Event.prototype.off = function (event, fn) {
const currentEvent = this.events[event]
this.events[event] = currentEvent.filter(item => item !== fn)
}
const publisher = new Event()
const fn1 = function (e) {
console.log('a1')
console.log(e)
}
const fn2 = function (e) {
console.log('a2')
console.log(e)
}
publisher.on('a', fn1)
publisher.on('a', fn2)
publisher.emit('a', 'aowu')
publisher.off('a', fn2)
publisher.emit('a', 'aowu')
複製程式碼
區別
- 觀察者模式,觀察者是知道subject的,釋出訂閱模式中,釋出者跟訂閱者不知道對方的存在,只有通過訊息代理進行通訊
- 觀察者模式大多是同步的,釋出訂閱大多是非同步的
- 釋出訂閱模式大多是鬆散耦合的,觀察者反之
128. nginx負載均衡
129. redis
130. Set,Map,WeakSet,WeakMap
- Set:可以遍歷,元素唯一
- WeakSet:元素都是物件,元素都是弱引用,可以被垃圾回收,不容易造成記憶體洩露,不能遍歷
- Map:鍵值對,可以遍歷
- WeakMap:鍵名為物件,鍵名是弱引用,不能遍歷
131. token 與 cookie的區別
token
- 加密解密時間較長,佔用記憶體大
- 不僅僅支援瀏覽器,還支援移動端等
- 可以防止crsf攻擊
- token是無狀態的,很好擴充套件
cookie
- 自動傳送,方便
- app端無法使用cookie
- 不在https中使用容易遭受crsf攻擊
132. null 與 undefined
- null:物件型別,是物件原型鏈的終點,已定義,為空值
- undefined:表示沒有賦值,undefined型別,未被定義
133. react中使用persist
react的合成事件呼叫之後全都會被重用,所有屬性都無效了,如果要非同步訪問的話,就要用event.persist()
,這樣就會從事件池裡面刪除,允許保留事件的引用
134. position: relative會影響子元素offsetLeft等屬性
135. 小程式和H5有何區別
- 小程式渲染方式與H5不同,小程式一般通過Native原生渲染的,但是小程式也支援web渲染,可以初始化一個webview元件,在webview中載入H5頁面
小程式下,native方式效能比web方式好
- 小程式特有的雙執行緒設計。H5下,通常我們全部打包在一個檔案裡面。小程式編譯之後有兩個,分別是view層,和業務邏輯。執行的時候會有兩條執行緒分別處理兩個檔案,一個是主渲染執行緒,負責渲染view層的內容。另一個是service worker執行緒,他負責執行業務邏輯。
- 執行環境不同,h5大多用於web-view或者瀏覽器,小程式是使用一個基於瀏覽器重構的內建解析器,大大地優化了效能、快取等
- 效能不同,小程式依賴微信客戶端實現,對解析進行了很多優化,首次開啟會快取很多資源,因此比H5要流暢很多,底層優化技術使得小程式更接近於原生的APP
- 小程式開發成本低,不需要擔心莫名其妙的bug出現,可以呼叫官方的api
- 微信使用者更多,相比h5,微信流量更多
- 小程式無法使用dom
- 小程式能使用很多手機的許可權
136. 字串slice、substr、substring
- slice:正常的擷取,如果有負數則倒著數
- substr:第一個引數為開始下標,第二個引數代表擷取多少個字元
- substring:傳入兩個引數,較小的一個為起始下標
137. 基本包裝型別
基本型別不是物件,但是他們又有一些方法,為什麼呢?
因為後臺會對基本型別進行包裝,例如字串、整數、布林值會首先利用建構函式建立例項,使用完之後就銷燬
let s1 = 'hello'
let s2 = s1.substring(2)
// ↓ 後臺包裝
let s1 = new String('hello') // 包裝
let s2 = s1.substring(2) // 可以呼叫方法
s1 = null // 銷燬
複製程式碼
138. toString 與 valueOf
- null 與 undefined 沒有這倆方法
- toString:值型別返回字串形式,物件型別返回[ibject Object]形式
- valueOf:無論是值還是引用型別,基本都是原樣返回,是Date的時候,返回時間戳
- 進行字串轉換時候,優先toString,進行數值計算優先valueOf
- toString支援傳參,可以進行進位制轉換
139. es6的class實現私有變數
- 閉包
class A {
constructor (x) {
let _x = x
this.showX = function () {
return _x
}
}
}
let a = new A(1)
// 無法訪問
a._x // undefined
// 可以訪問
a.showX() // 1
複製程式碼
- 利用symbol
class A {
constructor(x) {
// 定義symbol
const _x = Symbol('x')
// 利用symbol宣告私有變數
this[_x] = x
}
showX() {
return this[_x]
}
}
let a = new A(1)
// 1. 第一種方式
a[_x] // 報錯 Uncaught ReferenceError: _x is not defined
// 2. 第二種方式
// 自行定義一個相同的Symbol
const x = Symbol('x')
a[x] // 無法訪問,undefined
// 3. 第三種方式,可以訪問(正解)
a.showX() //1
複製程式碼
140. TypeScript的type與interface
interface
- 同名
interface
自動聚合,也可以跟同名的class
自動聚合 - 只能表示
object
,function
,class
型別
type
- 不僅僅表示
object
,function
,class
型別 - 不能重名
- 支援複雜的型別操作
141. JavaScript的V8引擎
JavaScript引擎
JavaScript是一個解釋型
語言,跟編譯型
語言不同,js是一邊執行一邊解析的。為了提高效能引入了java虛擬機器和C++編譯器的一些東西
執行流程
原始碼 -> 抽象語法樹 -> 位元組碼 -> JIT -> 原生程式碼
V8引擎
相比其他的javascript引擎(轉換成位元組碼或解釋執行),V8引擎將程式碼直接編譯成原生機器碼,使用了內聯快取提高效能。
工作流程:原始碼先被解析成抽象語法樹然後用JIT編譯器直接從抽象語法樹生成程式碼,提高程式碼執行速度(不用轉換成位元組嗎了)
142. 堆與棧的區別
- 堆:主要存new等分配的記憶體塊,編譯器不會自動釋放他們,引用型別的放在堆裡面
- 棧: 主要存放函式引數,區域性變數等,編譯器自動分配和釋放,基本資料型別放在棧裡面
143. unicode utf8 utf16
- unicode是為了解決世界上編碼不統一的問題,包含了所有符號、文字的編碼集,每個字元由2個位元組表示,英文字元只需要1個位元組,所以儲存起來就會浪費空間
- utf8每次8個位傳輸,是變長的編碼方式,使用1~4個位元組表示一個符號,utf8中文字元一般用到3個位元組
- utf16每次16個位傳輸
區別
unicode:世界上所有符號的編碼集 utf8、utf16:是unicode的字符集,不同的編碼實現
144. class 和 function的區別
- class封裝的更加合理,方便維護和使用
- class沒有像function那樣狀態提升
- class不用寫原型方法那些方便一點
- class要使用super才能使用this