
一,JS基礎
1.如何在es5環境下實作let
對于這個問題,我們可以直接查看babel轉換前后的結果,看一下在回圈中通過let定義的變數是如何解決變數提升的問題
- 原始碼
//原始碼
for(let i=0; i<10; i++){
console.log(i)
}
console.log(i)
babel轉碼
for(var _i = 0; _i < 10; _i++){
console.log(_i)
}
console.log(i)
babel在let定義的變數前加了道下劃線,避免在塊級作用域外訪問到該變數,除了對變數名的轉換,我們也可以通過自執行函式來模擬塊級作用域,
(function(){
for(var i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
})();
console.log(i) // Uncaught ReferenceError: i is not defined
2、如何在ES5環境下實作const
實作const的關鍵在于Object.defineProperty()這個API,這個API用于在一個物件上增加或修改屬性,通過配置屬性描述符,可以精確地控制屬性行為,Object.defineProperty() 接收三個引數:
Object.defineProperty(obj, prop, desc)

- 對于const不可修改的特性,我們通過設定writable屬性來實作
function _const(key, value) {
const desc = {
value,
writable: false
}
Object.defineProperty(window, key, desc)
}
_const('obj', {a: 1}) //定義obj
obj.b = 2 //可以正常給obj的屬性賦值
obj = {} //拋出錯誤,提示物件read-only
參考資料:如何在 ES5 環境下實作一個const ?
手寫call()
call() 方法使用一個指定的 this 值和單獨給出的一個或多個引數來呼叫一個函式
語法:function.call(thisArg, arg1, arg2, …)
call()的原理比較簡單,由于函式的this指向它的直接呼叫者,我們變更呼叫者即完成this指向的變更:
//變更函式呼叫者示例
function foo() {
console.log(this.name)
}
// 測驗
const obj = {
name: '寫代碼像蔡徐抻'
}
obj.foo = foo // 變更foo的呼叫者
obj.foo() // '寫代碼像蔡徐抻'
- 基于以上原理, 我們兩句代碼就能實作call()
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this // this指向呼叫call的物件,即我們要改變this指向的函式
return thisArg.fn(...args) // 執行函式并return其執行結果
}
但是我們有一些細節需要處理:
Function.prototype.myCall = function(thisArg, ...args) {
const fn = Symbol('fn') // 宣告一個獨有的Symbol屬性, 防止fn覆寫已有屬性
thisArg = thisArg || window // 若沒有傳入this, 默認系結window物件
thisArg[fn] = this // this指向呼叫call的物件,即我們要改變this指向的函式
const result = thisArg[fn](...args) // 執行當前函式
delete thisArg[fn] // 洗掉我們宣告的fn屬性
return result // 回傳函式執行結果
}
//測驗
foo.myCall(obj) // 輸出'寫代碼像蔡徐抻'
4. 手寫apply()
apply() 方法呼叫一個具有給定this值的函式,以及作為一個陣列(或類似陣列物件)提供的引數,
apply()和call()類似,區別在于call()接收引數串列,而apply()接收一個引數陣列,所以我們在call()的實作上簡單改一下入參形式即可
Function.prototype.myApply = function(thisArg, args) {
const fn = Symbol('fn') // 宣告一個獨有的Symbol屬性, 防止fn覆寫已有屬性
thisArg = thisArg || window // 若沒有傳入this, 默認系結window物件
thisArg[fn] = this // this指向呼叫call的物件,即我們要改變this指向的函式
const result = thisArg[fn](...args) // 執行當前函式
delete thisArg[fn] // 洗掉我們宣告的fn屬性
return result // 回傳函式執行結果
}
//測驗
foo.myApply(obj, []) // 輸出'寫代碼像蔡徐抻'
5、手寫bind()
bind() 方法創建一個新的函式,在 bind() 被呼叫時,這個新函式的 this 被指定為 bind() 的第一個引數,而其余引數將作為新函式的引數,供呼叫時使用,
語法: function.bind(thisArg, arg1, arg2, …)
從用法上看,似乎給call/apply包一層function就實作了bind():
Function.prototype.myBind = function(thisArg, ...args) {
return () => {
this.apply(thisArg, args)
}
}
但我們忽略了三點:
- bind()除了this還接收其他引數,bind()回傳的函式也接收引數,這兩部分的引數都要傳給回傳的函式
- new的優先級:如果bind系結后的函式被new了,那么此時this指向就發生改變,此時的this就是當前函式的實體
- 沒有保留原函式在原型鏈上的屬性和方法
Function.prototype.myBind = function (thisArg, ...args) {
var self = this
// new優先級
var fbound = function () {
self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
}
// 繼承原型上的屬性和方法
fbound.prototype = Object.create(self.prototype);
return fbound;
}
//測驗
const obj = { name: '寫代碼像蔡徐抻' }
function foo() {
console.log(this.name)
console.log(arguments)
}
foo.myBind(obj, 'a', 'b', 'c')() //輸出寫代碼像蔡徐抻 ['a', 'b', 'c']
6、手寫一個防抖函式
防抖和節流的概念都比較簡單,所以我們就不在“防抖節流是什么”這個問題上浪費過多篇幅了,簡單點一下:
防抖,即短時間內大量觸發同一事件,只會執行一次函式,實作原理為設定一個定時器,約定在xx毫秒后再觸發事件處理,每次觸發事件都會重新設定計時器,直到xx毫秒內無第二次操作,防抖常用于搜索框/滾動條的監聽事件處理,如果不做防抖,每輸入一個字/滾動螢屏,都會觸發事件處理,造成性能浪費,
function debounce(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
7、手寫一個節流函式
防抖是延遲執行,而節流是間隔執行,函式節流即每隔一段時間就執行一次,實作原理為設定一個定時器,約定xx毫秒后執行事件,如果時間到了,那么執行函式并重置定時器,和防抖的區別在于,防抖每次觸發事件都重置定時器,而節流在定時器到時間后再清空定時器
方式一 延時器
function throttle(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
實作方式2:使用兩個時間戳prev舊時間戳和now新時間戳,每次觸發事件都判斷二者的時間差,如果到達規定時間,執行函式并重置舊時間戳
function throttle(func, wait) {
var prev = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}
陣列扁平化
對于[1, [1,2], [1,2,3]]這樣多層嵌套的陣列,我們如何將其扁平化為[1, 1, 2, 1, 2, 3]這樣的一維陣列呢:
- 1.ES6的flat()
const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity) // [1, 1, 2, 1, 2, 3]
- 2.序列化后正則
const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str) // [1, 1, 2, 1, 2, 3]
- 3.遞回
對于樹狀結構的資料,最直接的處理方式就是遞回
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
let result = []
for (const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
}
return result
}
flat(arr) // [1, 1, 2, 1, 2, 3]
- 4.reduce()遞回
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(cur instanceof Array ? flat(cur) : cur)
}, [])
}
flat(arr) // [1, 1, 2, 1, 2, 3]
- 5.迭代+展開運算子
let arr = [1, [1,2], [1,2,3]]
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
console.log(arr) // [1, 1, 2, 1, 2, 3]
9. 手寫一個Promise
異步編程二三事 | Promise/async/Generator實作原理決議 | 9k字
二、JS面向物件
在JS中一切皆物件,但JS并不是一種真正的面向物件(OOP)的語言,因為它缺少類(class)的概念,雖然ES6引入了class和extends,使我們能夠輕易地實作類和繼承,但JS并不存在真實的類,JS的類是通過函式以及原型鏈機制模擬的,本小節的就來探究如何在ES5環境下利用函式和原型鏈實作JS面向物件的特性,
在開始之前,我們先回顧一下原型鏈的知識,后續new和繼承等實作都是基于原型鏈機制,很多介紹原型鏈的資料都能寫上洋洋灑灑幾千字,但我覺得讀者們不需要把原型鏈想太復雜,容易把自己繞進去,其實在我看來,原型鏈的核心只需要記住三點:
- 每個物件都有__proto__屬性,該屬性指向其原型物件,在呼叫實體的方法和屬性時,如果在實體物件上找不到,就會往原型物件上找
- 建構式的prototype屬性也指向實體的原型物件
- 原型物件的constructor屬性指向建構式

1、模擬實作new
首先我們要知道new做了什么
- 創建一個新物件,并繼承其建構式的prototype,這一步是為了繼承建構式原型上的屬性和方法
- 執行建構式,方法內的this被指定為該新實體,這一步是為了執行建構式內的賦值操作
- 回傳新實體(規范規定,如果構造方法回傳了一個物件,那么回傳該物件,否則回傳第一步創建的新物件)
// new是關鍵字,這里我們用函式來模擬,new Foo(args) <=> myNew(Foo, args)
function myNew(foo, ...args) {
// 創建新物件,并繼承構造方法的prototype屬性, 這一步是為了把obj掛原型鏈上, 相當于obj.__proto__ = Foo.prototype
let obj = Object.create(foo.prototype)
// 執行構造方法, 并為其系結新this, 這一步是為了讓構造方法能進行this.name = name之類的操作, args是構造方法的入參, 因為這里用myNew模擬, 所以入參從myNew傳入
let result = foo.apply(obj, args)
// 如果構造方法已經return了一個物件, 那么就回傳該物件, 一般情況下,構造方法不會回傳新實體,但使用者可以選擇回傳新實體來覆寫new創建的物件 否則回傳myNew創建的新物件
return typeof result === 'object' && result !== null ? result : obj
}
function Foo(name) {
this.name = name
}
const newObj = myNew(Foo, 'zhangsan')
console.log(newObj) // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo) // true
2、ES5如何實作繼承
說到繼承,最容易想到的是ES6的extends,當然如果只回答這個肯定不合格,我們要從函式和原型鏈的角度上實作繼承,下面我們一步步地、遞進地實作一個合格的繼承
- 1). 原型鏈繼承
原型鏈繼承的原理很簡單,直接讓子類的原型物件指向父類實體,當子類實體找不到對應的屬性和方法時,就會往它的原型物件,也就是父類實體上找,從而實作對父類的屬性和方法的繼承
// 父類
function Parent() {
this.name = '寫代碼像蔡徐抻'
}
// 父類的原型方法
Parent.prototype.getName = function() {
return this.name
}
// 子類
function Child() {}
// 讓子類的原型物件指向父類實體, 這樣一來在Child實體中找不到的屬性和方法就會到原型物件(父類實體)上尋找
Child.prototype = new Parent()
Child.prototype.constructor = Child // 根據原型鏈的規則,順便系結一下constructor, 這一步不影響繼承, 只是在用到constructor時會需要
// 然后Child實體就能訪問到父類及其原型上的name屬性和getName()方法
const child = new Child()
child.name // '寫代碼像蔡徐抻'
child.getName() // '寫代碼像蔡徐抻'
原型繼承的缺點:
- 由于所有Child實體原型都指向同一個Parent實體, 因此對某個Child實體的父類參考型別變數修改會影響所有的Child實體
- 在創建子類實體時無法向父類構造傳參, 即沒有實作super()的功能
// 示例:
function Parent() {
this.name = ['寫代碼像蔡徐抻']
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor = Child
// 測驗
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['foo'] (預期是['寫代碼像蔡徐抻'], 對child1.name的修改引起了所有child實體的變化
- 2)、 建構式繼承
建構式繼承,即在子類的建構式中執行父類的建構式,并為其系結子類的this,讓父類的建構式把成員屬性和方法都掛到子類的this上去,這樣既能避免實體之間共享一個原型實體,又能向父類構造方法傳參
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this, 'zhangsan') // 執行父類構造方法并系結子類的this, 使得父類中的屬性能夠賦到子類的this上
}
//測驗
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // 報錯,找不到getName(), 建構式繼承的方式繼承不到父類原型上的屬性和方法
建構式繼承的缺點:
繼承不到父類原型上的屬性和方法
- 3)、 組合式繼承
既然原型鏈繼承和建構式繼承各有互補的優缺點, 那么我們為什么不組合起來使用呢, 所以就有了綜合二者的組合式繼承
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 建構式繼承
Parent.call(this, 'zhangsan')
}
//原型鏈繼承
Child.prototype = new Parent()
Child.prototype.constructor = Child
//測驗
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
組合式繼承的缺點:
-
每次創建子類實體都執行了兩次建構式(Parent.call()和new Parent()),雖然這并不影響對父類的繼承,但子類創建實體時,原型中會存在兩份相同的屬性和方法,這并不優雅
-
4)、寄生式組合繼承
為了解決建構式被執行兩次的問題, 我們將指向父類實體改為指向父類原型, 減去一次建構式的執行
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 建構式繼承
Parent.call(this, 'zhangsan')
}
//原型鏈繼承
// Child.prototype = new Parent()
Child.prototype = Parent.prototype //將`指向父類實體`改為`指向父類原型`
Child.prototype.constructor = Child
//測驗
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
但這種方式存在一個問題,由于子類原型和父類原型指向同一個物件,我們對子類原型的操作會影響到父類原型,例如給Child.prototype增加一個getName()方法,那么會導致Parent.prototype也增加或被覆寫一個getName()方法,為了解決這個問題,我們給Parent.prototype做一個淺拷貝
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 建構式繼承
Parent.call(this, 'zhangsan')
}
//原型鏈繼承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype) //將`指向父類實體`改為`指向父類原型`
Child.prototype.constructor = Child
//測驗
const child = new Child()
const parent = new Parent()
child.getName() // ['zhangsan']
parent.getName() // 報錯, 找不到getName()
到這里我們就完成了ES5環境下的繼承的實作,這種繼承方式稱為寄生組合式繼承,是目前最成熟的繼承方式,babel對ES6繼承的轉化也是使用了寄生組合式繼承
我們回顧一下實作程序:
一開始最容易想到的是原型鏈繼承,通過把子類實體的原型指向父類實體來繼承父類的屬性和方法,但原型鏈繼承的缺陷在于對子類實體繼承的參考型別的修改會影響到所有的實體物件以及無法向父類的構造方法傳參,
因此我們引入了建構式繼承, 通過在子類建構式中呼叫父類建構式并傳入子類this來獲取父類的屬性和方法,但建構式繼承也存在缺陷,建構式繼承不能繼承到父類原型鏈上的屬性和方法,
所以我們綜合了兩種繼承的優點,提出了組合式繼承,但組合式繼承也引入了新的問題,它每次創建子類實體都執行了兩次父類構造方法,我們通過將子類原型指向父類實體改為子類原型指向父類原型的淺拷貝來解決這一問題,也就是最終實作 —— 寄生組合式繼承,

三、V8引擎機制

- 預決議:檢查語法錯誤但不生成AST
- 生成AST:經過詞法/語法分析,生成抽象語法樹
- 生成位元組碼:基線編譯器(Ignition)將AST轉換成位元組碼
- 生成機器碼:優化編譯器(Turbofan)將位元組碼轉換成優化過的機器碼,此外在逐行執行位元組碼的程序中,如果一段代碼經常被執行,那么V8會將這段代碼直接轉換成機器碼保存起來,下一次執行就不必經過位元組碼,優化了執行速度
上面幾點只是V8執行機制的極簡總結,建議閱讀參考資料:
2、介紹一下參考計數和標記清除
- 參考計數:給一個變數賦值參考型別,則該物件的參考次數+1,如果這個變數變成了其他值,那么該物件的參考次數-1,垃圾回收器會回收參考次數為0的物件,但是當物件回圈參考時,會導致參考次數永遠無法歸零,造成記憶體無法釋放,
- 標記清除:垃圾收集器先給記憶體中所有物件加上標記,然后從根節點開始遍歷,去掉被參考的物件和運行環境中物件的標記,剩下的被標記的物件就是無法訪問的等待回收的物件,
3、 V8如何進行垃圾回收
JS引擎中對變數的存盤主要有兩種位置,堆疊記憶體和堆記憶體,堆疊記憶體存盤基本型別資料以及參考型別資料的記憶體地址,堆記憶體儲存參考型別的資料

堆疊記憶體的回收:
- 堆疊記憶體呼叫堆疊背景關系切換后就被回收,比較簡單
V8的堆記憶體分為新生代記憶體和老生代記憶體,新生代記憶體是臨時分配的記憶體,存在時間短,老生代記憶體存在時間長,

- 新生代記憶體回識訓制:
新生代記憶體容量小,64位系統下僅有32M,新生代記憶體分為From、To兩部分,進行垃圾回收時,先掃描From,將非存活物件回收,將存活物件順序復制到To中,之后調換From/To,等待下一次回收 - 老生代記憶體回識訓制
- 晉升:如果新生代的變數經過多次回收依然存在,那么就會被放入老生代記憶體中
- 標記清除:老生代記憶體會先遍歷所有物件并打上標記,然后對正在使用或被強參考的物件取消標記,回收被標記的物件
- 整理記憶體碎片:把物件挪到記憶體的一端
參考資料:聊聊V8引擎的垃圾回收
4. JS相較于C++等語言為什么慢,V8做了哪些優化
1 JS的問題:
- 動態型別:導致每次存取屬性/尋求方法時候,都需要先檢查型別;此外動態型別也很難在編譯階段進行優化
- 屬性存取:C++/Java等語言中方法、屬性是存盤在陣列中的,僅需陣列位移就可以獲取,而JS存盤在物件中,每次獲取都要進行哈希查詢
2 V8的優化:
- 優化JIT(即時編譯):相較于C++/Java這類編譯型語言,JS一邊解釋一邊執行,效率低,V8對這個程序進行了優化:如果一段代碼被執行多次,那么V8會把這段代碼轉化為機器碼快取下來,下次運行時直接使用機器碼,
- 隱藏類:對于C++這類語言來說,僅需幾個指令就能通過偏移量獲取變數資訊,而JS需要進行字串匹配,效率低,V8借用了類和偏移位置的思想,將物件劃分成不同的組,即隱藏類
- 內嵌快取:即快取物件查詢的結果,常規查詢程序是:獲取隱藏類地址 -> 根據屬性名查找偏移值 -> 計算該屬性地址,內嵌快取就是對這一程序結果的快取
- 垃圾回收管理:上文已介紹

參考資料:為什么V8引擎這么快?
瀏覽器渲染機制
-
- 瀏覽器的渲染程序是怎樣的,

大體流程如下:
1 HTML和CSS經過各自決議,生成DOM樹和CSSOM樹
2 合并成為渲染樹
3 根據渲染樹進行布局
4 最后呼叫GPU進行繪制,顯示在螢屏上
- 瀏覽器的渲染程序是怎樣的,
2、 如何根據瀏覽器渲染機制加快首屏速度
- 優化檔案大小:HTML和CSS的加載和決議都會阻塞渲染樹的生成,從而影響首屏展示速度,因此我們可以通過優化檔案大小、減少CSS檔案層級的方法來加快首屏速度
- 避免資源下載阻塞檔案決議:瀏覽器決議到
3、什么是回流(重排),什么情況下會觸發回流
- 當元素的尺寸或者位置發生了變化,就需要重新計算渲染樹,這就是回流
- DOM元素的幾何屬性(width/height/padding/margin/border)發生變化時會觸發回流
- DOM元素移動或增加會觸發回流
- 讀寫offset/scroll/client等屬性時會觸發回流
- 呼叫window.getComputedStyle會觸發回流
4、什么是重繪,什么情況下會觸發重繪
DOM樣式發生了變化,但沒有影響DOM的幾何屬性時,會觸發重繪,而不會觸發回流,重繪由于DOM位置資訊不需要更新,省去了布局程序,因而性能上優于回流
5、 什么是GPU加速,如何使用GPU加速,GPU加速的缺點
- 優點:使用transform、opacity、filters等屬性時,會直接在GPU中完成處理,這些屬性的變化不會引起回流重繪
- 缺點:GPU渲染字體會導致字體模糊,過多的GPU處理會導致記憶體問題
6、 如何減少回流
- 使用class替代style,減少style的使用
- 使用resize、scroll時進行防抖和節流處理,這兩者會直接導致回流
- 使用visibility替換display: none,因為前者只會引起重繪,后者會引發回流
- 批量修改元素時,可以先讓元素脫離檔案流,等修改完畢后,再放入檔案流
- 避免觸發同步布局事件,我們在獲取offsetWidth這類屬性的值時,可以使用變數將查詢結果存起來,避免多次查詢,每次對offset/scroll/client等屬性進行查詢時都會觸發回流
- 對于復雜影片效果,使用絕對定位讓其脫離檔案流,復雜的影片效果會頻繁地觸發回流重繪,我們可以將影片元素設定絕對定位從而脫離檔案流避免反復回流重繪,

參考資料:必須明白的瀏覽器渲染機制
四、瀏覽器快取策略
1、介紹一下瀏覽器快取位置和優先級
1 Service Worker
2 Memory Cache(記憶體快取)
3 Disk Cache(硬碟快取)
4 Push Cache(推送快取)
5 以上快取都沒命中就會進行網路請求
2、 說說不同快取間的差別
- Service Worker
和Web Worker類似,是獨立的執行緒,我們可以在這個執行緒中快取檔案,在主執行緒需要的時候讀取這里的檔案,Service Worker使我們可以自由選擇快取哪些檔案以及檔案的匹配、讀取規則,并且快取是持續性的 - Memory Cache
即記憶體快取,記憶體快取不是持續性的,快取會隨著行程釋放而釋放 - Disk Cache
即硬碟快取,相較于記憶體快取,硬碟快取的持續性和容量更優,它會根據HTTP header的欄位判斷哪些資源需要快取 - Push Cache
即推送快取,是HTTP/2的內容,目前應用較少
強快取(不要向服務器詢問的快取)
設定Expires
即過期時間,例如「Expires: Thu, 26 Dec 2019 10:30:42 GMT」表示快取會在這個時間后失效,這個過期日期是絕對日期,如果修改了本地日期,或者本地日期與服務器日期不一致,那么將導致快取過期時間錯誤,
設定Cache-Control
HTTP/1.1新增欄位,Cache-Control可以通過max-age欄位來設定過期時間,例如「Cache-Control:max-age=3600」除此之外Cache-Control還能設定private/no-cache等多種欄位
協商快取(需要向服務器詢問快取是否已經過期)
Last-Modified
即最后修改時間,瀏覽器第一次請求資源時,服務器會在回應頭上加上Last-Modified ,當瀏覽器再次請求該資源時,瀏覽器會在請求頭中帶上If-Modified-Since 欄位,欄位的值就是之前服務器回傳的最后修改時間,服務器對比這兩個時間,若相同則回傳304,否則回傳新資源,并更新Last-Modified
ETag
HTTP/1.1新增欄位,表示檔案唯一標識,只要檔案內容改動,ETag就會重新計算,快取流程和 Last-Modified 一樣:服務器發送 ETag 欄位 -> 瀏覽器再次請求時發送 If-None-Match -> 如果ETag值不匹配,說明檔案已經改變,回傳新資源并更新ETag,若匹配則回傳304
兩者對比
- ETag 比 Last-Modified 更準確:如果我們打開檔案但并沒有修改,Last-Modified 也會改變,并且 Last-Modified 的單位時間為一秒,如果一秒內修改完了檔案,那么還是會命中快取
- 如果什么快取策略都沒有設定,那么瀏覽器會取回應頭中的 Date 減去 Last-Modified 值的 10% 作為快取時間

參考資料:瀏覽器快取機制剖析
五、網路相關
1、講講網路OSI七層模型,TCP/IP和HTTP分別位于哪一層


2、 常見HTTP狀態碼有哪些
- 2xx 開頭(請求成功)
200 OK:客戶端發送給服務器的請求被正常處理并回傳 - 3xx 開頭(重定向)
301 Moved Permanently:永久重定向,請求的網頁已永久移動到新位置,服務器回傳此回應時,會自動將請求者轉到新位置
302 Moved Permanently:臨時重定向,請求的網頁已臨時移動到新位置,服務器目前從不同位置的網頁回應請求,但請求者應繼續使用原有位置來進行以后的請求
304 Not Modified:未修改,自從上次請求后,請求的網頁未修改過,服務器回傳此回應時,不會回傳網頁內容, - 4xx 開頭(客戶端錯誤)
400 Bad Request:錯誤請求,服務器不理解請求的語法,常見于客戶端傳參錯誤
401 Unauthorized:未授權,表示發送的請求需要有通過 HTTP 認證的認證資訊,常見于客戶端未登錄
403 Forbidden:禁止,服務器拒絕請求,常見于客戶端權限不足
404 Not Found:未找到,服務器找不到對應資源 - 5xx 開頭(服務端錯誤)
500 Inter Server Error:服務器內部錯誤,服務器遇到錯誤,無法完成請求
501 Not Implemented:尚未實施,服務器不具備完成請求的功能
502 Bad Gateway:作為網關或者代理作業的服務器嘗試執行請求時,從上游服務器接收到無效的回應,
503 service unavailable:服務不可用,服務器目前無法使用(處于超載或停機維護狀態),通常是暫時狀態,
3、GET請求和POST請求有何區別
標準答案:
GET請求引數放在URL上,POST請求引數放在請求體里
GET請求引數長度有限制,POST請求引數長度可以非常大
POST請求相較于GET請求安全一點點,因為GET請求的引數在URL上,且有歷史記錄
GET請求能快取,POST不能
更進一步:
其實HTTP協議并沒有要求GET/POST請求引數必須放在URL上或請求體里,也沒有規定GET請求的長度,目前對URL的長度限制,是各家瀏覽器設定的限制,GET和POST的根本區別在于:GET請求是冪等性的,而POST請求不是
冪等性,指的是對某一資源進行一次或多次請求都具有相同的副作用,例如搜索就是一個冪等的操作,而洗掉、新增則不是一個冪等操作,
由于GET請求是冪等的,在網路不好的環境中,GET請求可能會重復嘗試,造成重復操作資料的風險,因此,GET請求用于無副作用的操作(如搜索),新增/洗掉等操作適合用POST
4、HTTP的請求報文由哪幾部分組成
一個HTTP請求報文由請求行(request line)、請求頭(header)、空行和請求資料4個部分組成

回應報文和請求報文結構類似,不再贅述
5、HTTP常見請求/回應頭及其含義
- 通用頭(請求頭和回應頭都有的首部)

- 請求頭

- 回應頭

物體頭(針對請求報文和回應報文的物體部分使用首部)

HTTP首部當然不止這么幾個,但為了避免寫太多大家記不住(主要是別的我也沒去看),這里只介紹了一些常用的,詳細的可以看MDN的檔案,
6、HTTP/1.0和HTTP/1.1有什么區別
- 長連接: HTTP/1.1支持長連接和請求的流水線,在一個TCP連接上可以傳送多個HTTP請求,避免了因為多次建立TCP連接的時間消耗和延時
- 快取處理: HTTP/1.1引入Entity tag,If-Unmodified-Since, If-Match, If-None-Match等新的請求頭來控制快取,詳見瀏覽器快取小節
- 帶寬優化及網路連接的使用: HTTP1.1則在請求頭引入了range頭域,支持斷點續傳功能
- Host頭處理: 在HTTP/1.0中認為每臺服務器都有唯一的IP地址,但隨著虛擬主機技術的發展,多個主機共享一個IP地址愈發普遍,HTTP1.1的請求訊息和回應訊息都應支持Host頭域,且請求訊息中如果沒有Host頭域會400錯誤
7、 介紹一下HTTP/2.0新特性
- 多路復用: 即多個請求都通過一個TCP連接并發地完成
- 服務端推送: 服務端能夠主動把資源推送給客戶端
- 新的二進制格式: HTTP/2采用二進制格式傳輸資料,相比于HTTP/1.1的文本格式,二進制格式具有更好的決議性和拓展性
- header壓縮: HTTP/2壓縮訊息頭,減少了傳輸資料的大小,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/306440.html
標籤:其他
上一篇:手把手教你發個包
下一篇:Echarts空氣質量地圖效果
