前端經典面試題(有答案)

腹黑的可樂發表於2023-03-06

程式碼輸出結果

var a = 10
var obj = {
  a: 20,
  say: () => {
    console.log(this.a)
  }
}
obj.say() 

var anotherObj = { a: 30 } 
obj.say.apply(anotherObj) 

輸出結果:10 10

我麼知道,箭頭函式時不繫結this的,它的this來自原其父級所處的上下文,所以首先會列印全域性中的 a 的值10。後面雖然讓say方法指向了另外一個物件,但是仍不能改變箭頭函式的特性,它的this仍然是指向全域性的,所以依舊會輸出10。

但是,如果是普通函式,那麼就會有完全不一樣的結果:

var a = 10  
var obj = {  
  a: 20,  
  say(){
    console.log(this.a)  
  }  
}  
obj.say()   
var anotherObj={a:30}   
obj.say.apply(anotherObj)

輸出結果:20 30

這時,say方法中的this就會指向他所在的物件,輸出其中的a的值。

Compositon api

Composition API也叫組合式API,是Vue3.x的新特性。

透過建立 Vue 元件,我們可以將介面的可重複部分及其功能提取到可重用的程式碼段中。僅此一項就可以使我們的應用程式在可維護性和靈活性方面走得更遠。然而,我們的經驗已經證明,光靠這一點可能是不夠的,尤其是當你的應用程式變得非常大的時候——想想幾百個元件。在處理如此大的應用程式時,共享和重用程式碼變得尤為重要
  • Vue2.0中,隨著功能的增加,元件變得越來越複雜,越來越難維護,而難以維護的根本原因是Vue的API設計迫使開發者使用watch,computed,methods選項組織程式碼,而不是實際的業務邏輯。
  • 另外Vue2.0缺少一種較為簡潔的低成本的機制來完成邏輯複用,雖然可以minxis完成邏輯複用,但是當mixin變多的時候,會使得難以找到對應的data、computed或者method來源於哪個mixin,使得型別推斷難以進行。
  • 所以Composition API的出現,主要是也是為了解決Option API帶來的問題,第一個是程式碼組織問題,Compostion API可以讓開發者根據業務邏輯組織自己的程式碼,讓程式碼具備更好的可讀性和可擴充套件性,也就是說當下一個開發者接觸這一段不是他自己寫的程式碼時,他可以更好的利用程式碼的組織反推出實際的業務邏輯,或者根據業務邏輯更好的理解程式碼。
  • 第二個是實現程式碼的邏輯提取與複用,當然mixin也可以實現邏輯提取與複用,但是像前面所說的,多個mixin作用在同一個元件時,很難看出property是來源於哪個mixin,來源不清楚,另外,多個mixinproperty存在變數命名衝突的風險。而Composition API剛好解決了這兩個問題。

通俗的講:

沒有Composition API之前vue相關業務的程式碼需要配置到option的特定的區域,中小型專案是沒有問題的,但是在大型專案中會導致後期的維護性比較複雜,同時程式碼可複用性不高。Vue3.x中的composition-api就是為了解決這個問題而生的

compositon api提供了以下幾個函式:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命週期的hooks

都說Composition API與React Hook很像,說說區別

從React Hook的實現角度看,React Hook是根據useState呼叫的順序來確定下一次重渲染時的state是來源於哪個useState,所以出現了以下限制
  • 不能在迴圈、條件、巢狀函式中呼叫Hook
  • 必須確保總是在你的React函式的頂層呼叫Hook
  • useEffect、useMemo等函式必須手動確定依賴關係
而Composition API是基於Vue的響應式系統實現的,與React Hook的相比
  • 宣告在setup函式內,一次元件例項化只呼叫一次setup,而React Hook每次重渲染都需要呼叫Hook,使得React的GC比Vue更有壓力,效能也相對於Vue來說也較慢
  • Compositon API的呼叫不需要顧慮呼叫順序,也可以在迴圈、條件、巢狀函式中使用
  • 響應式系統自動實現了依賴收集,進而元件的部分的效能最佳化由Vue內部自己完成,而React Hook需要手動傳入依賴,而且必須必須保證依賴的順序,讓useEffectuseMemo等函式正確的捕獲依賴變數,否則會由於依賴不正確使得元件效能下降。
雖然Compositon API看起來比React Hook好用,但是其設計思想也是借鑑React Hook的。

垃圾回收

  • 對於在JavaScript中的字串,物件,陣列是沒有固定大小的,只有當對他們進行動態分配儲存時,直譯器就會分配記憶體來儲存這些資料,當JavaScript的直譯器消耗完系統中所有可用的記憶體時,就會造成系統崩潰。
  • 記憶體洩漏,在某些情況下,不再使用到的變數所佔用記憶體沒有及時釋放,導致程式執行中,記憶體越佔越大,極端情況下可以導致系統崩潰,伺服器當機。
  • JavaScript有自己的一套垃圾回收機制,JavaScript的直譯器可以檢測到什麼時候程式不再使用這個物件了(資料),就會把它所佔用的記憶體釋放掉。
  • 針對JavaScript的來及回收機制有以下兩種方法(常用):標記清除,引用計數
  • 標記清除
v8 的垃圾回收機制基於分代回收機制,這個機制又基於世代假說,這個假說有兩個特點,一是新生的物件容易早死,另一個是不死的物件會活得更久。基於這個假說,v8 引擎將記憶體分為了新生代和老生代。
  • 新建立的物件或者只經歷過一次的垃圾回收的物件被稱為新生代。經歷過多次垃圾回收的物件被稱為老生代。
  • 新生代被分為 From 和 To 兩個空間,To 一般是閒置的。當 From 空間滿了的時候會執行 Scavenge 演算法進行垃圾回收。當我們執行垃圾回收演算法的時候應用邏輯將會停止,等垃圾回收結束後再繼續執行。

這個演算法分為三步:

  • 首先檢查 From 空間的存活物件,如果物件存活則判斷物件是否滿足晉升到老生代的條件,如果滿足條件則晉升到老生代。如果不滿足條件則移動 To 空間。
  • 如果物件不存活,則釋放物件的空間。
  • 最後將 From 空間和 To 空間角色進行交換。

新生代物件晉升到老生代有兩個條件:

  • 第一個是判斷是物件否已經經過一次 Scavenge 回收。若經歷過,則將物件從 From 空間複製到老生代中;若沒有經歷,則複製到 To 空間。
  • 第二個是 To 空間的記憶體使用佔比是否超過限制。當物件從 From 空間複製到 To 空間時,若 To 空間使用超過 25%,則物件直接晉升到老生代中。設定 25% 的原因主要是因為演算法結束後,兩個空間結束後會交換位置,如果 To 空間的記憶體太小,會影響後續的記憶體分配。
老生代採用了標記清除法和標記壓縮法。標記清除法首先會對記憶體中存活的物件進行標記,標記結束後清除掉那些沒有標記的物件。由於標記清除後會造成很多的記憶體碎片,不便於後面的記憶體分配。所以瞭解決記憶體碎片的問題引入了標記壓縮法。

由於在進行垃圾回收的時候會暫停應用的邏輯,對於新生代方法由於記憶體小,每次停頓的時間不會太長,但對於老生代來說每次垃圾回收的時間長,停頓會造成很大的影響。 為了解決這個問題 V8 引入了增量標記的方法,將一次停頓進行的過程分為了多步,每次執行完一小步就讓執行邏輯執行一會,就這樣交替執行

Proxy代理

proxy在目標物件的外層搭建了一層攔截,外界對目標物件的某些操作,必須透過這層攔截
var proxy = new Proxy(target, handler);
new Proxy()表示生成一個Proxy例項,target參數列示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為
var target = {
   name: 'poetries'
 };
 var logHandler = {
   get: function(target, key) {
     console.log(`${key} 被讀取`);
     return target[key];
   },
   set: function(target, key, value) {
     console.log(`${key} 被設定為 ${value}`);
     target[key] = value;
   }
 }
 var targetWithLog = new Proxy(target, logHandler);

 targetWithLog.name; // 控制檯輸出:name 被讀取
 targetWithLog.name = 'others'; // 控制檯輸出:name 被設定為 others

 console.log(target.name); // 控制檯輸出: others
  • targetWithLog 讀取屬性的值時,實際上執行的是 logHandler.get :在控制檯輸出資訊,並且讀取被代理物件 target 的屬性。
  • targetWithLog 設定屬性值時,實際上執行的是 logHandler.set :在控制檯輸出資訊,並且設定被代理物件 target 的屬性的值
// 由於攔截函式總是返回35,所以訪問任何屬性都得到35
var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

Proxy 例項也可以作為其他物件的原型物件

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35
proxy物件是obj物件的原型,obj物件本身並沒有time屬性,所以根據原型鏈,會在proxy物件上讀取該屬性,導致被攔截

Proxy的作用

對於代理模式 Proxy 的作用主要體現在三個方面
  • 攔截和監視外部對物件的訪問
  • 降低函式或類的複雜度
  • 在複雜操作前對操作進行校驗或對所需資源進行管理

Proxy所能代理的範圍--handler

實際上 handler 本身就是ES6所新設計的一個物件.它的作用就是用來 自定義代理物件的各種可代理操作 。它本身一共有13中方法,每種方法都可以代理一種操作.其13種方法如下
// 在讀取代理物件的原型時觸發該操作,比如在執行 Object.getPrototypeOf(proxy) 時。
handler.getPrototypeOf()

// 在設定代理物件的原型時觸發該操作,比如在執行 Object.setPrototypeOf(proxy, null) 時。
handler.setPrototypeOf()


// 在判斷一個代理物件是否是可擴充套件時觸發該操作,比如在執行 Object.isExtensible(proxy) 時。
handler.isExtensible()


// 在讓一個代理物件不可擴充套件時觸發該操作,比如在執行 Object.preventExtensions(proxy) 時。
handler.preventExtensions()

// 在獲取代理物件某個屬性的屬性描述時觸發該操作,比如在執行 Object.getOwnPropertyDescriptor(proxy, "foo") 時。
handler.getOwnPropertyDescriptor()


// 在定義代理物件某個屬性時的屬性描述時觸發該操作,比如在執行 Object.defineProperty(proxy, "foo", {}) 時。
andler.defineProperty()


// 在判斷代理物件是否擁有某個屬性時觸發該操作,比如在執行 "foo" in proxy 時。
handler.has()

// 在讀取代理物件的某個屬性時觸發該操作,比如在執行 proxy.foo 時。
handler.get()


// 在給代理物件的某個屬性賦值時觸發該操作,比如在執行 proxy.foo = 1 時。
handler.set()

// 在刪除代理物件的某個屬性時觸發該操作,比如在執行 delete proxy.foo 時。
handler.deleteProperty()

// 在獲取代理物件的所有屬性鍵時觸發該操作,比如在執行 Object.getOwnPropertyNames(proxy) 時。
handler.ownKeys()

// 在呼叫一個目標物件為函式的代理物件時觸發該操作,比如在執行 proxy() 時。
handler.apply()


// 在給一個目標物件為建構函式的代理物件構造例項時觸發該操作,比如在執行new proxy() 時。
handler.construct()

為何Proxy不能被Polyfill

  • 如class可以用function模擬;promise可以用callback模擬
  • 但是proxy不能用Object.defineProperty模擬
目前谷歌的polyfill只能實現部分的功能,如get、set https://github.com/GoogleChro...
// commonJS require
const proxyPolyfill = require('proxy-polyfill/src/proxy')();

// Your environment may also support transparent rewriting of commonJS to ES6:
import ProxyPolyfillBuilder from 'proxy-polyfill/src/proxy';
const proxyPolyfill = ProxyPolyfillBuilder();

// Then use...
const myProxy = new proxyPolyfill(...);

列舉幾個css中可繼承和不可繼承的元素

  • 不可繼承的:display、margin、border、padding、background、height、min-height、max-height、width、min-width、max-width、overflow、position、left、right、top、bottom、z-index、float、clear、table-layout、vertical-align
  • 所有元素可繼承:visibilitycursor
  • 內聯元素可繼承:letter-spacing、word-spacing、white-space、line-height、color、font、font-family、font-size、font-style、font-variant、font-weight、text-decoration、text-transform、direction
  • 終端塊狀元素可繼承:text-indent和text-align
  • 列表元素可繼承:list-style、list-style-type、list-style-position、list-style-image`。

transition和animation的區別

Animationtransition大部分屬性是相同的,他們都是隨時間改變元素的屬性值,他們的主要區別是transition需要觸發一個事件才能改變屬性,而animation不需要觸發任何事件的情況下才會隨時間改變屬性值,並且transition為2幀,從from .... to,而animation可以一幀一幀的

OSI七層模型

ISO為了更好的使網路應用更為普及,推出了OSI參考模型。

(1)應用層

OSI參考模型中最靠近使用者的一層,是為計算機使用者提供應用介面,也為使用者直接提供各種網路服務。我們常見應用層的網路服務協議有:HTTPHTTPSFTPPOP3SMTP等。

  • 在客戶端與伺服器中經常會有資料的請求,這個時候就是會用到http(hyper text transfer protocol)(超文字傳輸協議)或者https.在後端設計資料介面時,我們常常使用到這個協議。
  • FTP是檔案傳輸協議,在開發過程中,個人並沒有涉及到,但是我想,在一些資源網站,比如百度網盤`迅雷`應該是基於此協議的。
  • SMTPsimple mail transfer protocol(簡單郵件傳輸協議)。在一個專案中,在使用者郵箱驗證碼登入的功能時,使用到了這個協議。

(2)表示層

表示層提供各種用於應用層資料的編碼和轉換功能,確保一個系統的應用層傳送的資料能被另一個系統的應用層識別。如果必要,該層可提供一種標準表示形式,用於將計算機內部的多種資料格式轉換成通訊中採用的標準表示形式。資料壓縮和加密也是表示層可提供的轉換功能之一。

在專案開發中,為了方便資料傳輸,可以使用base64對資料進行編解碼。如果按功能來劃分,base64應該是工作在表示層。

(3)會話層

會話層就是負責建立、管理和終止表示層實體之間的通訊會話。該層的通訊由不同裝置中的應用程式之間的服務請求和響應組成。

(4)傳輸層

傳輸層建立了主機端到端的連結,傳輸層的作用是為上層協議提供端到端的可靠和透明的資料傳輸服務,包括處理差錯控制和流量控制等問題。該層向高層遮蔽了下層資料通訊的細節,使高層使用者看到的只是在兩個傳輸實體間的一條主機到主機的、可由使用者控制和設定的、可靠的資料通路。我們通常說的,TCP UDP就是在這一層。埠號既是這裡的“端”。

(5)網路層

本層透過IP定址來建立兩個節點之間的連線,為源端的運輸層送來的分組,選擇合適的路由和交換節點,正確無誤地按照地址傳送給目的端的運輸層。就是通常說的IP層。這一層就是我們經常說的IP協議層。IP協議是Internet的基礎。我們可以這樣理解,網路層規定了資料包的傳輸路線,而傳輸層則規定了資料包的傳輸方式。

(6)資料鏈路層

將位元組合成位元組,再將位元組組合成幀,使用鏈路層地址 (乙太網使用MAC地址)來訪問介質,並進行差錯檢測。
網路層與資料鏈路層的對比,透過上面的描述,我們或許可以這樣理解,網路層是規劃了資料包的傳輸路線,而資料鏈路層就是傳輸路線。不過,在資料鏈路層上還增加了差錯控制的功能。

(7)物理層

實際最終訊號的傳輸是透過物理層實現的。透過物理介質傳輸位元流。規定了電平、速度和電纜針腳。常用裝置有(各種物理裝置)集線器、中繼器、調變解調器、網線、雙絞線、同軸電纜。這些都是物理層的傳輸介質。

OSI七層模型通訊特點:對等通訊 對等通訊,為了使資料分組從源傳送到目的地,源端OSI模型的每一層都必須與目的端的對等層進行通訊,這種通訊方式稱為對等層通訊。在每一層通訊過程中,使用本層自己協議進行通訊。

參考 前端進階面試題詳細解答

定時器與requestAnimationFrame、requestIdleCallback

1. setTimeout

setTimeout的執行機制:執行該語句時,是立即把當前定時器程式碼推入事件佇列,當定時器在事件列表中滿足設定的時間值時將傳入的函式加入任務佇列,之後的執行就交給任務佇列負責。但是如果此時任務佇列不為空,則需等待,所以執行定時器內程式碼的時間可能會大於設定的時間
setTimeout(() => {
    console.log(1);
}, 0)
console.log(2);

輸出 2, 1;

setTimeout的第二個參數列示在執行程式碼前等待的毫秒數。上面程式碼中,設定為0,表面意思為 執行程式碼前等待的毫秒數為0,即立即執行。但實際上的執行結果我們也看到了,並不是表面上看起來的樣子,千萬不要被欺騙了。

實際上,上面的程式碼並不是立即執行的,這是因為setTimeout有一個最小執行時間,HTML5標準規定了setTimeout()的第二個引數的最小值(最短間隔)不得低於4毫秒。 當指定的時間低於該時間時,瀏覽器會用最小允許的時間作為setTimeout的時間間隔,也就是說即使我們把setTimeout的延遲時間設定為0,實際上可能為 4毫秒後才事件推入任務佇列

定時器程式碼在被推送到任務佇列前,會先被推入到事件列表中,當定時器在事件列表中滿足設定的時間值時會被推到任務佇列,但是如果此時任務佇列不為空,則需等待,所以執行定時器內程式碼的時間可能會大於設定的時間
setTimeout(() => {
    console.log(111);
}, 100);

上面程式碼表示100ms後執行console.log(111),但實際上實行的時間肯定是大於100ms後的, 100ms 只是表示 100ms 後將任務加入到"任務佇列"中,必須等到當前程式碼(執行棧)執行完,主執行緒才會去執行它指定的回撥函式。要是當前程式碼耗時很長,有可能要等很久,所以並沒有辦法保證,回撥函式一定會在setTimeout()指定的時間執行。

2. setTimeout 和 setInterval區別

  • setTimeout: 指定延期後呼叫函式,每次setTimeout計時到後就會去執行,然後執行一段時間後才繼續setTimeout,中間就多了誤差,(誤差多少與程式碼的執行時間有關)。
  • setInterval:以指定週期呼叫函式,而setInterval則是每次都精確的隔一段時間推入一個事件(但是,事件的執行時間不一定就不準確,還有可能是這個事件還沒執行完畢,下一個事件就來了).
btn.onclick = function(){
    setTimeout(function(){
        console.log(1);
    },250);
}
擊該按鈕後,首先將onclick事件處理程式加入佇列。該程式執行後才設定定時器,再有250ms後,指定的程式碼才被新增到佇列中等待執行。 如果上面程式碼中的onclick事件處理程式執行了300ms,那麼定時器的程式碼至少要在定時器設定之後的300ms後才會被執行。佇列中所有的程式碼都要等到javascript程式空閒之後才能執行,而不管它們是如何新增到佇列中的。

如圖所示,儘管在255ms處新增了定時器程式碼,但這時候還不能執行,因為onclick事件處理程式仍在執行。定時器程式碼最早能執行的時機是在300ms處,即onclick事件處理程式結束之後。

3. setInterval存在的一些問題:

JavaScript中使用 setInterval 開啟輪詢。定時器程式碼可能在程式碼再次被新增到佇列之前還沒有完成執行,結果導致定時器程式碼連續執行好幾次,而之間沒有任何停頓。而javascript引擎對這個問題的解決是:當使用setInterval()時,僅當沒有該定時器的任何其他程式碼例項時,才將定時器程式碼新增到佇列中。這確保了定時器程式碼加入到佇列中的最小時間間隔為指定間隔。

但是,這樣會導致兩個問題:

  • 某些間隔被跳過;
  • 多個定時器的程式碼執行之間的間隔可能比預期的小

假設,某個onclick事件處理程式使用setInterval()設定了200ms間隔的定時器。如果事件處理程式花了300ms多一點時間完成,同時定時器程式碼也花了差不多的時間,就會同時出現跳過某間隔的情況

例子中的第一個定時器是在205ms處新增到佇列中的,但是直到過了300ms處才能執行。當執行這個定時器程式碼時,在405ms處又給佇列新增了另一個副本。在下一個間隔,即605ms處,第一個定時器程式碼仍在執行,同時在佇列中已經有了一個定時器程式碼的例項。結果是,在這個時間點上的定時器程式碼不會被新增到佇列中

使用setTimeout構造輪詢能保證每次輪詢的間隔。

setTimeout(function () {
 console.log('我被呼叫了');
 setTimeout(arguments.callee, 100);
}, 100);
calleearguments 物件的一個屬性。它可以用於引用該函式的函式體內當前正在執行的函式。在嚴格模式下,第5版 ECMAScript (ES5) 禁止使用arguments.callee()。當一個函式必須呼叫自身的時候, 避免使用 arguments.callee(), 透過要麼給函式表示式一個名字,要麼使用一個函式宣告.
setTimeout(function fn(){
    console.log('我被呼叫了');
    setTimeout(fn, 100);
},100);

這個模式鏈式呼叫了setTimeout(),每次函式執行的時候都會建立一個新的定時器。第二個setTimeout()呼叫當前執行的函式,併為其設定另外一個定時器。這樣做的好處是,在前一個定時器程式碼執行完之前,不會向佇列插入新的定時器程式碼,確保不會有任何缺失的間隔。而且,它可以保證在下一次定時器程式碼執行之前,至少要等待指定的間隔,避免了連續的執行。

4. requestAnimationFrame

4.1 60fps與裝置重新整理率

目前大多數裝置的螢幕重新整理率為60次/秒,如果在頁面中有一個動畫或者漸變效果,或者使用者正在滾動頁面,那麼瀏覽器渲染動畫或頁面的每一幀的速率也需要跟裝置螢幕的重新整理率保持一致。

卡頓:其中每個幀的預算時間僅比16毫秒多一點(1秒/ 60 = 16.6毫秒)。但實際上,瀏覽器有整理工作要做,因此您的所有工作是需要在10毫秒內完成。如果無法符合此預算,幀率將下降,並且內容會在螢幕上抖動。此現象通常稱為卡頓,會對使用者體驗產生負面影響。

跳幀: 假如動畫切換在 16ms, 32ms, 48ms時分別切換,跳幀就是假如到了32ms,其他任務還未執行完成,沒有去執行動畫切幀,等到開始進行動畫的切幀,已經到了該執行48ms的切幀。就好比你玩遊戲的時候卡了,過了一會,你再看畫面,它不會停留你卡的地方,或者這時你的角色已經掛掉了。必須在下一幀開始之前就已經繪製完畢;

Chrome devtool 檢視實時 FPS, 開啟 More tools => Rendering, 勾選 FPS meter

4.2 requestAnimationFrame實現動畫

requestAnimationFrame是瀏覽器用於定時迴圈操作的一個介面,類似於setTimeout,主要用途是按幀對網頁進行重繪。

requestAnimationFrame 之前,主要藉助 setTimeout/ setInterval 來編寫 JS 動畫,而動畫的關鍵在於動畫幀之間的時間間隔設定,這個時間間隔的設定有講究,一方面要足夠小,這樣動畫幀之間才有連貫性,動畫效果才顯得平滑流暢;另一方面要足夠大,確保瀏覽器有足夠的時間及時完成渲染。

顯示器有固定的重新整理頻率(60Hz或75Hz),也就是說,每秒最多隻能重繪60次或75次,requestAnimationFrame的基本思想就是與這個重新整理頻率保持同步,利用這個重新整理頻率進行頁面重繪。此外,使用這個API,一旦頁面不處於瀏覽器的當前標籤,就會自動停止重新整理。這就節省了CPU、GPU和電力。

requestAnimationFrame 是在主執行緒上完成。這意味著,如果主執行緒非常繁忙,requestAnimationFrame的動畫效果會大打折扣。

requestAnimationFrame 使用一個回撥函式作為引數。這個回撥函式會在瀏覽器重繪之前呼叫。

requestID = window.requestAnimationFrame(callback); 

window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame       || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame    || 
            window.oRequestAnimationFrame      || 
            window.msRequestAnimationFrame     || 
            function( callback ){
            window.setTimeout(callback, 1000 / 60);
        };
})();

上面的程式碼按照1秒鐘60次(大約每16.7毫秒一次),來模擬requestAnimationFrame

5. requestIdleCallback()

MDN上的解釋:requestIdleCallback()方法將在瀏覽器的空閒時段內呼叫的函式排隊。這使開發者能夠在主事件迴圈上執行後臺和低優先順序工作,而不會影響延遲關鍵事件,如動畫和輸入響應。函式一般會按先進先呼叫的順序執行,然而,如果回撥函式指定了執行超時時間timeout,則有可能為了在超時前執行函式而打亂執行順序。

requestAnimationFrame會在每次螢幕重新整理的時候被呼叫,而requestIdleCallback則會在每次螢幕重新整理時,判斷當前幀是否還有多餘的時間,如果有,則會呼叫requestAnimationFrame的回撥函式,

圖片中是兩個連續的執行幀,大致可以理解為兩個幀的持續時間大概為16.67,圖中黃色部分就是空閒時間。所以,requestIdleCallback 中的回撥函式僅會在每次螢幕重新整理並且有空閒時間時才會被呼叫.

利用這個特性,我們可以在動畫執行的期間,利用每幀的空閒時間來進行資料傳送的操作,或者一些優先順序比較低的操作,此時不會使影響到動畫的效能,或者和requestAnimationFrame搭配,可以實現一些頁面效能方面的的最佳化,

react 的 fiber 架構也是基於 requestIdleCallback 實現的, 並且在不支援的瀏覽器中提供了 polyfill

總結

  • 單執行緒模型和任務佇列出發理解 setTimeout(fn, 0),並不是立即執行。
  • JS 動畫, 用requestAnimationFrame 會比 setInterval 效果更好
  • requestIdleCallback()常用來切割長任務,利用空閒時間執行,避免主執行緒長時間阻塞

ES6模組與CommonJS模組有什麼異同?

ES6 Module和CommonJS模組的區別:

  • CommonJS是對模組的淺拷⻉,ES6 Module是對模組的引⽤,即ES6 Module只存只讀,不能改變其值,也就是指標指向不能變,類似const;
  • import的接⼝是read-only(只讀狀態),不能修改其變數值。 即不能修改其變數的指標指向,但可以改變變數內部指標指向,可以對commonJS對重新賦值(改變指標指向),但是對ES6 Module賦值會編譯報錯。

ES6 Module和CommonJS模組的共同點:

  • CommonJS和ES6 Module都可以對引⼊的物件進⾏賦值,即對物件內部屬性的值進⾏改變。

選擇器權重計算方式

!important > 內聯樣式 = 外聯樣式 > ID選擇器 > 類選擇器 = 偽類選擇器 = 屬性選擇器 > 元素選擇器 = 偽元素選擇器 > 通配選擇器 = 後代選擇器 = 兄弟選擇器
  1. 屬性後面加!import會覆蓋頁面內任何位置定義的元素樣式
  2. 作為style屬性寫在元素內的樣式
  3. id選擇器
  4. 類選擇器
  5. 標籤選擇器
  6. 萬用字元選擇器(*
  7. 瀏覽器自定義或繼承

同一級別:後寫的會覆蓋先寫的

css選擇器的解析原則:選擇器定位DOM元素是從右往左的方向,這樣可以儘早的過濾掉一些不必要的樣式規則和元素

型別及檢測方式

1. JS內建型別

JavaScript 的資料型別有下圖所示

其中,前 7 種型別為基礎型別,最後 1 種(Object)為引用型別,也是你需要重點關注的,因為它在日常工作中是使用得最頻繁,也是需要關注最多技術細節的資料型別
  • JavaScript一共有8種資料型別,其中有7種基本資料型別:UndefinedNullBooleanNumberStringSymboles6新增,表示獨一無二的值)和BigIntes10新增);
  • 1種引用資料型別——Object(Object本質上是由一組無序的名值對組成的)。裡面包含 function、Array、Date等。JavaScript不支援任何建立自定義型別的機制,而所有值最終都將是上述 8 種資料型別之一。

    • 引用資料型別: 物件Object(包含普通物件-Object,陣列物件-Array,正則物件-RegExp,日期物件-Date,數學函式-Math,函式物件-Function
在這裡,我想先請你重點了解下面兩點,因為各種 JavaScript 的資料型別最後都會在初始化之後放在不同的記憶體中,因此上面的資料型別大致可以分成兩類來進行儲存:
  • 原始資料型別 :基礎型別儲存在棧記憶體,被引用或複製時,會建立一個完全相等的變數;佔據空間小、大小固定,屬於被頻繁使用資料,所以放入棧中儲存。
  • 引用資料型別 :引用型別儲存在堆記憶體,儲存的是地址,多個引用指向同一個地址,這裡會涉及一個“共享”的概念;佔據空間大、大小不固定。引用資料型別在棧中儲存了指標,該指標指向堆中該實體的起始地址。當直譯器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實體。

JavaScript 中的資料是如何儲存在記憶體中的?

在 JavaScript 中,原始型別的賦值會完整複製變數值,而引用型別的賦值是複製引用地址。

在 JavaScript 的執行過程中, 主要有三種型別記憶體空間,分別是程式碼空間棧空間堆空間。其中的程式碼空間主要是儲存可執行程式碼的,原始型別(Number、String、Null、Undefined、Boolean、Symbol、BigInt)的資料值都是直接儲存在“棧”中的,引用型別(Object)的值是存放在“堆”中的。因此在棧空間中(執行上下文),原始型別儲存的是變數的值,而引用型別儲存的是其在"堆空間"中的地址,當 JavaScript 需要訪問該資料的時候,是透過棧中的引用地址來訪問的,相當於多了一道轉手流程。

在編譯過程中,如果 JavaScript 引擎判斷到一個閉包,也會在堆空間建立換一個“closure(fn)”的物件(這是一個內部物件,JavaScript 是無法訪問的),用來儲存閉包中的變數。所以閉包中的變數是儲存在“堆空間”中的。

JavaScript 引擎需要用棧來維護程式執行期間上下文的狀態,如果棧空間大了話,所有的資料都存放在棧空間裡面,那麼會影響到上下文切換的效率,進而又影響到整個程式的執行效率。通常情況下,棧空間都不會設定太大,主要用來存放一些原始型別的小資料。而引用型別的資料佔用的空間都比較大,所以這一類資料會被存放到堆中,堆空間很大,能存放很多大的資料,不過缺點是分配記憶體和回收記憶體都會佔用一定的時間。因此需要“棧”和“堆”兩種空間。

題目一:初出茅廬
let a = {
  name: 'lee',
  age: 18
}
let b = a;
console.log(a.name);  //第一個console
b.name = 'son';
console.log(a.name);  //第二個console
console.log(b.name);  //第三個console
這道題比較簡單,我們可以看到第一個 console 打出來 name 是 'lee',這應該沒什麼疑問;但是在執行了 b.name='son' 之後,結果你會發現 a 和 b 的屬性 name 都是 'son',第二個和第三個列印結果是一樣的,這裡就體現了引用型別的“共享”的特性,即這兩個值都存在同一塊記憶體中共享,一個發生了改變,另外一個也隨之跟著變化。

你可以直接在 Chrome 控制檯敲一遍,深入理解一下這部分概念。下面我們再看一段程式碼,它是比題目一稍複雜一些的物件屬性變化問題。

題目二:漸入佳境
let a = {
  name: 'Julia',
  age: 20
}
function change(o) {
  o.age = 24;
  o = {
    name: 'Kath',
    age: 30
  }
  return o;
}
let b = change(a);     // 注意這裡沒有new,後面new相關會有專門文章講解
console.log(b.age);    // 第一個console
console.log(a.age);    // 第二個console

這道題涉及了 function,你透過上述程式碼可以看到第一個 console 的結果是 30b 最後列印結果是 {name: "Kath", age: 30};第二個 console 的返回結果是 24,而 a 最後的列印結果是 {name: "Julia", age: 24}

是不是和你預想的有些區別?你要注意的是,這裡的 functionreturn 帶來了不一樣的東西。

原因在於:函式傳參進來的 o,傳遞的是物件在堆中的記憶體地址值,透過呼叫 o.age = 24(第 7 行程式碼)確實改變了 a 物件的 age 屬性;但是第 12 行程式碼的 return 卻又把 o 變成了另一個記憶體地址,將 {name: "Kath", age: 30} 存入其中,最後返回 b 的值就變成了 {name: "Kath", age: 30}。而如果把第 12 行去掉,那麼 b 就會返回 undefined

2. 資料型別檢測

(1)typeof

typeof 對於原始型別來說,除了 null 都可以顯示正確的型別
console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object     []陣列的資料型別在 typeof 中被解釋為 object
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object     null 的資料型別被 typeof 解釋為 object
typeof 對於物件來說,除了函式都會顯示 object,所以說 typeof 並不能準確判斷變數到底是什麼型別,所以想判斷一個物件的正確型別,這時候可以考慮使用 instanceof

(2)instanceof

instanceof 可以正確的判斷物件的型別,因為內部機制是透過判斷物件的原型鏈中是不是能找到型別的 prototype
console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true    
// console.log(undefined instanceof Undefined);
// console.log(null instanceof Null);
  • instanceof 可以準確地判斷複雜引用資料型別,但是不能正確判斷基礎資料型別;
  • typeof 也存在弊端,它雖然可以判斷基礎資料型別(null 除外),但是引用資料型別中,除了 function 型別以外,其他的也無法判斷
// 我們也可以試著實現一下 instanceof
function _instanceof(left, right) {
    // 由於instance要檢測的是某物件,需要有一個前置判斷條件
    //基本資料型別直接返回false
    if(typeof left !== 'object' || left === null) return false;

    // 獲得型別的原型
    let prototype = right.prototype
    // 獲得物件的原型
    left = left.__proto__
    // 判斷物件的型別是否等於型別的原型
    while (true) {
        if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true

(3)constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
這裡有一個坑,如果我建立一個物件,更改它的原型,constructor就會變得不可靠了
function Fn(){};

Fn.prototype=new Array();

var f=new Fn();

console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true 

(4)Object.prototype.toString.call()

toString()Object 的原型方法,呼叫該方法,可以統一返回格式為 “[object Xxx]” 的字串,其中 Xxx 就是物件的型別。對於 Object 物件,直接呼叫 toString() 就能返回 [object Object];而對於其他物件,則需要透過 call 來呼叫,才能返回正確的型別資訊。我們來看一下程式碼。
Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({})  // 同上結果,加上call也ok
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('1')  // "[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString.call(function(){})  // "[object Function]"
Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(document)  //"[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"

// 從上面這段程式碼可以看出,Object.prototype.toString.call() 可以很好地判斷引用型別,甚至可以把 document 和 window 都區分開來。
實現一個全域性通用的資料型別判斷方法,來加深你的理解,程式碼如下
function getType(obj){
  let type  = typeof obj;
  if (type !== "object") {    // 先進行typeof判斷,如果是基礎資料型別,直接返回
    return type;
  }
  // 對於typeof返回結果是object的,再進行如下的判斷,正則返回結果
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');  // 注意正則中間有個空格
}
/* 程式碼驗證,需要注意大小寫,哪些是typeof判斷,哪些是toString判斷?思考下 */
getType([])     // "Array" typeof []是object,因此toString返回
getType('123')  // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null)   // "Null"首字母大寫,typeof null是object,需toString來判斷
getType(undefined)   // "undefined" typeof 直接返回
getType()            // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判斷,因此首字母小寫
getType(/123/g)      //"RegExp" toString返回

小結

  • typeof

    • 直接在計算機底層基於資料型別的值(二進位制)進行檢測
    • typeof nullobject 原因是物件存在在計算機中,都是以000開始的二進位制儲存,所以檢測出來的結果是物件
    • typeof 普通物件/陣列物件/正則物件/日期物件 都是object
    • typeof NaN === 'number'
  • instanceof

    • 檢測當前例項是否屬於這個類的
    • 底層機制:只要當前類出現在例項的原型上,結果都是true
    • 不能檢測基本資料型別
  • constructor

    • 支援基本型別
    • constructor可以隨便改,也不準
  • Object.prototype.toString.call([val])

    • 返回當前例項所屬類資訊
判斷 Target 的型別,單單用 typeof 並無法完全滿足,這其實並不是 bug,本質原因是 JS 的萬物皆物件的理論。因此要真正完美判斷時,我們需要區分對待:
  • 基本型別(null): 使用 String(null)
  • 基本型別(string / number / boolean / undefined) + function: - 直接使用 typeof即可
  • 其餘引用型別(Array / Date / RegExp Error): 呼叫toString後根據[object XXX]進行判斷

3. 資料型別轉換

我們先看一段程式碼,瞭解下大致的情況。

'123' == 123   // false or true?
'' == null    // false or true?
'' == 0        // false or true?
[] == 0        // false or true?
[] == ''       // false or true?
[] == ![]      // false or true?
null == undefined //  false or true?
Number(null)     // 返回什麼?
Number('')      // 返回什麼?
parseInt('');    // 返回什麼?
{}+10           // 返回什麼?
let obj = {
    [Symbol.toPrimitive]() {
        return 200;
    },
    valueOf() {
        return 300;
    },
    toString() {
        return 'Hello';
    }
}
console.log(obj + 200); // 這裡列印出來是多少?
首先我們要知道,在 JS 中型別轉換隻有三種情況,分別是:
  • 轉換為布林值
  • 轉換為數字
  • 轉換為字串

轉Boolean

在條件判斷時,除了 undefinednullfalseNaN''0-0,其他所有值都轉為 true,包括所有物件
Boolean(0)          //false
Boolean(null)       //false
Boolean(undefined)  //false
Boolean(NaN)        //false
Boolean(1)          //true
Boolean(13)         //true
Boolean('12')       //true

物件轉原始型別

物件在轉換型別的時候,會呼叫內建的 [[ToPrimitive]] 函式,對於該函式來說,演算法邏輯一般來說如下
  • 如果已經是原始型別了,那就不需要轉換了
  • 呼叫 x.valueOf(),如果轉換為基礎型別,就返回轉換的值
  • 呼叫 x.toString(),如果轉換為基礎型別,就返回轉換的值
  • 如果都沒有返回原始型別,就會報錯
當然你也可以重寫 Symbol.toPrimitive,該方法在轉原始型別時呼叫優先順序最高。
let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  },
  [Symbol.toPrimitive]() {
    return 2
  }
}
1 + a // => 3

四則運算子

它有以下幾個特點:
  • 運算中其中一方為字串,那麼就會把另一方也轉換為字串
  • 如果一方不是字串或者數字,那麼會將它轉換為數字或者字串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
  • 對於第一行程式碼來說,觸發特點一,所以將數字 1 轉換為字串,得到結果 '11'
  • 對於第二行程式碼來說,觸發特點二,所以將 true 轉為數字 1
  • 對於第三行程式碼來說,觸發特點二,所以將陣列透過 toString轉為字串 1,2,3,得到結果 41,2,3
另外對於加法還需要注意這個表示式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
  • 因為 + 'b' 等於 NaN,所以結果為 "aNaN",你可能也會在一些程式碼中看到過 + '1'的形式來快速獲取 number 型別。
  • 那麼對於除了加法的運算子來說,只要其中一方是數字,那麼另一方就會被轉為數字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比較運算子

  • 如果是物件,就透過 toPrimitive 轉換物件
  • 如果是字串,就透過 unicode 字元索引來比較
let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  }
}
a > -1 // true
在以上程式碼中,因為 a 是物件,所以會透過 valueOf 轉換為原始型別再比較值。

強制型別轉換

強制型別轉換方式包括 Number()parseInt()parseFloat()toString()String()Boolean(),這幾種方法都比較類似
  • Number() 方法的強制轉換規則
  • 如果是布林值,truefalse 分別被轉換為 10
  • 如果是數字,返回自身;
  • 如果是 null,返回 0
  • 如果是 undefined,返回 NaN
  • 如果是字串,遵循以下規則:如果字串中只包含數字(或者是 0X / 0x 開頭的十六進位制數字字串,允許包含正負號),則將其轉換為十進位制;如果字串中包含有效的浮點格式,將其轉換為浮點數值;如果是空字串,將其轉換為 0;如果不是以上格式的字串,均返回 NaN;
  • 如果是 Symbol,丟擲錯誤;
  • 如果是物件,並且部署了 [Symbol.toPrimitive] ,那麼呼叫此方法,否則呼叫物件的 valueOf() 方法,然後依據前面的規則轉換返回的值;如果轉換的結果是 NaN ,則呼叫物件的 toString() 方法,再次依照前面的順序轉換返回對應的值。
Number(true);        // 1
Number(false);       // 0
Number('0111');      //111
Number(null);        //0
Number('');          //0
Number('1a');        //NaN
Number(-0X11);       //-17
Number('0X11')       //17

Object 的轉換規則

物件轉換的規則,會先呼叫內建的 [ToPrimitive] 函式,其規則邏輯如下:
  • 如果部署了 Symbol.toPrimitive 方法,優先呼叫再返回;
  • 呼叫 valueOf(),如果轉換為基礎型別,則返回;
  • 呼叫 toString(),如果轉換為基礎型別,則返回;
  • 如果都沒有返回基礎型別,會報錯。
var obj = {
  value: 1,
  valueOf() {
    return 2;
  },
  toString() {
    return '3'
  },
  [Symbol.toPrimitive]() {
    return 4
  }
}
console.log(obj + 1); // 輸出5
// 因為有Symbol.toPrimitive,就優先執行這個;如果Symbol.toPrimitive這段程式碼刪掉,則執行valueOf列印結果為3;如果valueOf也去掉,則呼叫toString返回'31'(字串拼接)
// 再看兩個特殊的case:
10 + {}
// "10[object Object]",注意:{}會預設呼叫valueOf是{},不是基礎型別繼續轉換,呼叫toString,返回結果"[object Object]",於是和10進行'+'運算,按照字串拼接規則來,參考'+'的規則C
[1,2,undefined,4,5] + 10
// "1,2,,4,510",注意[1,2,undefined,4,5]會預設先呼叫valueOf結果還是這個陣列,不是基礎資料型別繼續轉換,也還是呼叫toString,返回"1,2,,4,5",然後再和10進行運算,還是按照字串拼接規則,參考'+'的第3條規則

'==' 的隱式型別轉換規則

  • 如果型別相同,無須進行型別轉換;
  • 如果其中一個操作值是 null 或者 undefined,那麼另一個運算子必須為 null 或者 undefined,才會返回 true,否則都返回 false
  • 如果其中一個是 Symbol 型別,那麼返回 false
  • 兩個操作值如果為 string 和 number 型別,那麼就會將字串轉換為 number
  • 如果一個操作值是 boolean,那麼轉換成 number
  • 如果一個操作值為 object 且另一方為 stringnumber 或者 symbol,就會把 object 轉為原始型別再進行判斷(呼叫 objectvalueOf/toString 方法進行轉換)。
null == undefined       // true  規則2
null == 0               // false 規則2
'' == null              // false 規則2
'' == 0                 // true  規則4 字串轉隱式轉換成Number之後再對比
'123' == 123            // true  規則4 字串轉隱式轉換成Number之後再對比
0 == false              // true  e規則 布林型隱式轉換成Number之後再對比
1 == true               // true  e規則 布林型隱式轉換成Number之後再對比
var a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  }
};
// 注意這裡a又可以等於1、2、3
console.log(a == 1 && a == 2 && a ==3);  //true f規則 Object隱式轉換
// 注:但是執行過3遍之後,再重新執行a==3或之前的數字就是false,因為value已經加上去了,這裡需要注意一下

'+' 的隱式型別轉換規則

'+' 號運算子,不僅可以用作數字相加,還可以用作字串拼接。僅當 '+' 號兩邊都是數字時,進行的是加法運算;如果兩邊都是字串,則直接拼接,無須進行隱式型別轉換。
  • 如果其中有一個是字串,另外一個是 undefinednull 或布林型,則呼叫 toString() 方法進行字串拼接;如果是純物件、陣列、正則等,則預設呼叫物件的轉換方法會存在優先順序,然後再進行拼接。
  • 如果其中有一個是數字,另外一個是 undefinednull、布林型或數字,則會將其轉換成數字進行加法運算,物件的情況還是參考上一條規則。
  • 如果其中一個是字串、一個是數字,則按照字串規則進行拼接
1 + 2        // 3  常規情況
'1' + '2'    // '12' 常規情況
// 下面看一下特殊情況
'1' + undefined   // "1undefined" 規則1,undefined轉換字串
'1' + null        // "1null" 規則1,null轉換字串
'1' + true        // "1true" 規則1,true轉換字串
'1' + 1n          // '11' 比較特殊字串和BigInt相加,BigInt轉換為字串
1 + undefined     // NaN  規則2,undefined轉換數字相加NaN
1 + null          // 1    規則2,null轉換為0
1 + true          // 2    規則2,true轉換為1,二者相加為2
1 + 1n            // 錯誤  不能把BigInt和Number型別直接混合相加
'1' + 3           // '13' 規則3,字串拼接
整體來看,如果資料中有字串,JavaScript 型別轉換還是更傾向於轉換成字串,因為第三條規則中可以看到,在字串和數字相加的過程中最後返回的還是字串,這裡需要關注一下

null 和 undefined 的區別?

  • 首先 UndefinedNull 都是基本資料型別,這兩個基本資料型別分別都只有一個值,就是 undefinednull
  • undefined 代表的含義是未定義, null 代表的含義是空物件(其實不是真的物件,請看下面的注意!)。一般變數宣告瞭但還沒有定義的時候會返回 undefinednull 主要用於賦值給一些可能會返回物件的變數,作為初始化。
其實 null 不是物件,雖然 typeof null 會輸出 object,但是這只是 JS 存在的一個悠久 Bug。在 JS 的最初版本中使用的是 32 位系統,為了效能考慮使用低位儲存變數的型別資訊,000 開頭代表是物件,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部型別判斷程式碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。
  • undefined 在 js 中不是一個保留字,這意味著我們可以使用 undefined 來作為一個變數名,這樣的做法是非常危險的,它會影響我們對 undefined 值的判斷。但是我們可以透過一些方法獲得安全的 undefined 值,比如說 void 0
  • 當我們對兩種型別使用 typeof 進行判斷的時候,Null 型別化會返回 “object”,這是一個歷史遺留的問題。當我們使用雙等號對兩種型別的值進行比較時會返回 true,使用三個等號時會返回 false。

TCP/IP五層協議

TCP/IP五層協議和OSI的七層協議對應關係如下:

  • 應用層 (application layer):直接為應用程式提供服務。應用層協議定義的是應用程式間通訊和互動的規則,不同的應用有著不同的應用層協議,如 HTTP協議(全球資訊網服務)、FTP協議(檔案傳輸)、SMTP協議(電子郵件)、DNS(域名查詢)等。
  • 傳輸層 (transport layer):有時也譯為運輸層,它負責為兩臺主機中的程式提供通訊服務。該層主要有以下兩種協議:

    • 傳輸控制協議 (Transmission Control Protocol,TCP):提供面向連線的、可靠的資料傳輸服務,資料傳輸的基本單位是報文段(segment);
    • 使用者資料包協議 (User Datagram Protocol,UDP):提供無連線的、盡最大努力的資料傳輸服務,但不保證資料傳輸的可靠性,資料傳輸的基本單位是使用者資料包。
  • 網路層 (internet layer):有時也譯為網際層,它負責為兩臺主機提供通訊服務,並透過選擇合適的路由將資料傳遞到目標主機。
  • 資料鏈路層 (data link layer):負責將網路層交下來的 IP 資料包封裝成幀,並在鏈路的兩個相鄰節點間傳送幀,每一幀都包含資料和必要的控制資訊(如同步資訊、地址資訊、差錯控制等)。
  • 物理層 (physical Layer):確保資料可以在各種物理媒介上進行傳輸,為資料的傳輸提供可靠的環境。

從上圖中可以看出,TCP/IP模型比OSI模型更加簡潔,它把應用層/表示層/會話層全部整合為了應用層

在每一層都工作著不同的裝置,比如我們常用的交換機就工作在資料鏈路層的,一般的路由器是工作在網路層的。 在每一層實現的協議也各不同,即每一層的服務也不同,下圖列出了每層主要的傳輸協議:

同樣,TCP/IP五層協議的通訊方式也是對等通訊:

TCP和UDP的使用場景

  • TCP應用場景: 效率要求相對低,但對準確性要求相對高的場景。因為傳輸中需要對資料確認、重發、排序等操作,相比之下效率沒有UDP高。例如:檔案傳輸(準確高要求高、但是速度可以相對慢)、接受郵件、遠端登入。
  • UDP應用場景: 效率要求相對高,對準確性要求相對低的場景。例如:QQ聊天、線上影片、網路語音電話(即時通訊,速度要求高,但是出現偶爾斷續不是太大問題,並且此處完全不可以使用重發機制)、廣播通訊(廣播、多播)。

左右兩邊定寬,中間自適應

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;
}

intanceof 運算子的實現原理及實現

instanceof 運算子用於判斷建構函式的 prototype 屬性是否出現在物件的原型鏈中的任何位置。

function myInstanceof(left, right) {
  // 獲取物件的原型
  let proto = Object.getPrototypeOf(left)
  // 獲取建構函式的 prototype 物件
  let prototype = right.prototype; 

  // 判斷建構函式的 prototype 物件是否在物件的原型鏈上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果沒有找到,就繼續從其原型上找,Object.getPrototypeOf方法用來獲取指定物件的原型
    proto = Object.getPrototypeOf(proto);
  }
}

UDP協議為什麼不可靠?

UDP在傳輸資料之前不需要先建立連線,遠地主機的運輸層在接收到UDP報文後,不需要確認,提供不可靠交付。總結就以下四點:

  • 不保證訊息交付:不確認,不重傳,無超時
  • 不保證交付順序:不設定包序號,不重排,不會發生隊首阻塞
  • 不跟蹤連線狀態:不必建立連線或重啟狀態機
  • 不進行擁塞控制:不內建客戶端或網路反饋機制

說一下 HTML5 drag API

  • dragstart:事件主體是被拖放元素,在開始拖放被拖放元素時觸發。
  • darg:事件主體是被拖放元素,在正在拖放被拖放元素時觸發。
  • dragenter:事件主體是目標元素,在被拖放元素進入某元素時觸發。
  • dragover:事件主體是目標元素,在被拖放在某元素內移動時觸發。
  • dragleave:事件主體是目標元素,在被拖放元素移出目標元素是觸發。
  • drop:事件主體是目標元素,在目標元素完全接受被拖放元素時觸發。
  • dragend:事件主體是被拖放元素,在整個拖放操作結束時觸發。

實現一個三角形

CSS繪製三角形主要用到的是border屬性,也就是邊框。

平時在給盒子設定邊框時,往往都設定很窄,就可能誤以為邊框是由矩形組成的。實際上,border屬性是右三角形組成的,下面看一個例子:

div {
    width: 0;
    height: 0;
    border: 100px solid;
    border-color: orange blue red green;
}

將元素的長寬都設定為0

(1)三角1

div {    width: 0;    height: 0;    border-top: 50px solid red;    border-right: 50px solid transparent;    border-left: 50px solid transparent;}

(2)三角2

div {
    width: 0;
    height: 0;
    border-bottom: 50px solid red;
    border-right: 50px solid transparent;
    border-left: 50px solid transparent;
}

(3)三角3

div {
    width: 0;
    height: 0;
    border-left: 50px solid red;
    border-top: 50px solid transparent;
    border-bottom: 50px solid transparent;
}

(4)三角4

div {
    width: 0;
    height: 0;
    border-right: 50px solid red;
    border-top: 50px solid transparent;
    border-bottom: 50px solid transparent;
}

(5)三角5

div {
    width: 0;
    height: 0;
    border-top: 100px solid red;
    border-right: 100px solid transparent;
}

還有很多,就不一一實現了,總體的原則就是透過上下左右邊框來控制三角形的方向,用邊框的寬度比來控制三角形的角度。

對this物件的理解

this 是執行上下文中的一個屬性,它指向最後一次呼叫這個方法的物件。在實際開發中,this 的指向可以透過四種呼叫模式來判斷。

  • 第一種是函式呼叫模式,當一個函式不是一個物件的屬性時,直接作為函式來呼叫時,this 指向全域性物件。
  • 第二種是方法呼叫模式,如果一個函式作為一個物件的方法來呼叫時,this 指向這個物件。
  • 第三種是構造器呼叫模式,如果一個函式用 new 呼叫時,函式執行前會新建立一個物件,this 指向這個新建立的物件。
  • 第四種是 apply 、 call 和 bind 呼叫模式,這三個方法都可以顯示的指定呼叫函式的 this 指向。其中 apply 方法接收兩個引數:一個是 this 繫結的物件,一個是引數陣列。call 方法接收的引數,第一個是 this 繫結的物件,後面的其餘引數是傳入函式執行的引數。也就是說,在使用 call() 方法時,傳遞給函式的引數必須逐個列舉出來。bind 方法透過傳入一個物件,返回一個 this 繫結了傳入物件的新函式。這個函式的 this 指向除了使用 new 時會被改變,其他情況下都不會改變。

這四種方式,使用構造器呼叫模式的優先順序最高,然後是 apply、call 和 bind 呼叫模式,然後是方法呼叫模式,然後是函式呼叫模式。

new運算子的實現原理

new運算子的執行過程:

(1)首先建立了一個新的空物件

(2)設定原型,將物件的原型設定為函式的 prototype 物件。

(3)讓函式的 this 指向這個物件,執行建構函式的程式碼(為這個新物件新增屬性)

(4)判斷函式的返回值型別,如果是值型別,返回建立的物件。如果是引用型別,就返回這個引用型別的物件。

具體實現:

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判斷引數是否是一個函式
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一個空物件,物件的原型為建構函式的 prototype 物件
  newObject = Object.create(constructor.prototype);
  // 將 this 指向新建物件,並執行函式
  result = constructor.apply(newObject, arguments);
  // 判斷返回物件
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判斷返回結果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(建構函式, 初始化引數);

箭頭函式的this指向哪⾥?

箭頭函式不同於傳統JavaScript中的函式,箭頭函式並沒有屬於⾃⼰的this,它所謂的this是捕獲其所在上下⽂的 this 值,作為⾃⼰的 this 值,並且由於沒有屬於⾃⼰的this,所以是不會被new調⽤的,這個所謂的this也不會被改變。

可以⽤Babel理解⼀下箭頭函式:

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); 
    }; 
  } 
}

轉化後:

// ES5,由 Babel 轉譯
var obj = { 
   getArrow: function getArrow() { 
     var _this = this; 
     return function () { 
        console.log(_this === obj); 
     }; 
   } 
};

相關文章