最近在整理 JavaScript 的時候發現遇到了很多面試中常見的面試題,本部分主要是作者在 Github 等各大論壇收錄的 JavaScript 相關知識和一些相關面試題時所做的筆記,分享這份總結給大家,對大家對 JavaScript 的可以來一次全方位的檢漏和排查,感謝原作者 CavsZhouyou 的付出,原文連結放在文章最下方,如果出現錯誤,希望大家共同指出!

1. 介紹 js 的基本資料型別。

js 一共有六種基本資料型別,分別是 Undefined、Null、Boolean、Number、String,還有在 ES6 中新增的 Symbol 型別,
代表建立後獨一無二且不可變的資料型別,它的出現我認為主要是為了解決可能出現的全域性變數衝突的問題。

2. JavaScript 有幾種型別的值?你能畫一下他們的記憶體圖嗎?

涉及知識點:

  • 棧:原始資料型別(Undefined、Null、Boolean、Number、String)
  • 堆:引用資料型別(物件、陣列和函式)
兩種型別的區別是:儲存位置不同。
原始資料型別直接儲存在棧(stack)中的簡單資料段,佔據空間小、大小固定,屬於被頻繁使用資料,所以放入棧中儲存。 引用資料型別儲存在堆(heap)中的物件,佔據空間大、大小不固定。如果儲存在棧中,將會影響程式執行的效能;引用資料型別在
棧中儲存了指標,該指標指向堆中該實體的起始地址。當直譯器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實
體。

回答:

js 可以分為兩種型別的值,一種是基本資料型別,一種是複雜資料型別。

基本資料型別....(參考1)

複雜資料型別指的是 Object 型別,所有其他的如 Array、Date 等資料型別都可以理解為 Object 型別的子類。

兩種型別間的主要區別是它們的儲存位置不同,基本資料型別的值直接儲存在棧中,而複雜資料型別的值儲存在堆中,通過使用在棧中
儲存對應的指標來獲取堆中的值。

詳細資料可以參考:

《JavaScript 有幾種型別的值?》

《JavaScript 有幾種型別的值?能否畫一下它們的記憶體圖;》

3. 什麼是堆?什麼是棧?它們之間有什麼區別和聯絡?

堆和棧的概念存在於資料結構中和作業系統記憶體中。

在資料結構中,棧中資料的存取方式為先進後出。而堆是一個優先佇列,是按優先順序來進行排序的,優先順序可以按照大小來規定。完全
二叉樹是堆的一種實現方式。 在作業系統中,記憶體被分為棧區和堆區。 棧區記憶體由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。 堆區記憶體一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由垃圾回收機制回收。

詳細資料可以參考:

《什麼是堆?什麼是棧?他們之間有什麼區別和聯絡?》

4. 內部屬性 [[Class]] 是什麼?

所有 typeof 返回值為 "object" 的物件(如陣列)都包含一個內部屬性 [[Class]](我們可以把它看作一個內部的分類,而非
傳統的面向物件意義上的類)。這個屬性無法直接訪問,一般通過 Object.prototype.toString(..) 來檢視。例如: Object.prototype.toString.call( [1,2,3] );
// "[object Array]" Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"

5. 介紹 js 有哪些內建物件?

涉及知識點:

全域性的物件( global objects )或稱標準內建物件,不要和 "全域性物件(global object)" 混淆。這裡說的全域性的物件是說在
全域性作用域裡的物件。全域性作用域中的其他物件可以由使用者的指令碼建立或由宿主程式提供。 標準內建物件的分類 (1)值屬性,這些全域性屬性返回一個簡單值,這些值沒有自己的屬性和方法。 例如 Infinity、NaN、undefined、null 字面量 (2)函式屬性,全域性函式可以直接呼叫,不需要在呼叫時指定所屬物件,執行結束後會將結果直接返回給呼叫者。 例如 eval()、parseFloat()、parseInt() 等 (3)基本物件,基本物件是定義或使用其他物件的基礎。基本物件包括一般物件、函式物件和錯誤物件。 例如 Object、Function、Boolean、Symbol、Error 等 (4)數字和日期物件,用來表示數字、日期和執行數學計算的物件。 例如 Number、Math、Date (5)字串,用來表示和操作字串的物件。 例如 String、RegExp (6)可索引的集合物件,這些物件表示按照索引值來排序的資料集合,包括陣列和型別陣列,以及類陣列結構的物件。例如 Array (7)使用鍵的集合物件,這些集合物件在儲存資料時會使用到鍵,支援按照插入順序來迭代元素。 例如 Map、Set、WeakMap、WeakSet (8)向量集合,SIMD 向量集合中的資料會被組織為一個數據序列。 例如 SIMD 等 (9)結構化資料,這些物件用來表示和操作結構化的緩衝區資料,或使用 JSON 編碼的資料。 例如 JSON 等 (10)控制抽象物件 例如 Promise、Generator 等 (11)反射 例如 Reflect、Proxy (12)國際化,為了支援多語言處理而加入 ECMAScript 的物件。 例如 Intl、Intl.Collator 等 (13)WebAssembly (14)其他 例如 arguments

回答:

js 中的內建物件主要指的是在程式執行前存在全域性作用域裡的由 js 定義的一些全域性值屬性、函式和用來例項化其他物件的構造函
數物件。一般我們經常用到的如全域性變數值 NaN、undefined,全域性函式如 parseInt()、parseFloat() 用來例項化物件的構
造函式如 Date、Object 等,還有提供數學計算的單體內建物件如 Math 物件。

詳細資料可以參考:

《標準內建物件的分類》

《JS 所有內建物件屬性和方法彙總》

6. undefined 與 undeclared 的區別?

已在作用域中宣告但還沒有賦值的變數,是 undefined 的。相反,還沒有在作用域中宣告過的變數,是 undeclared 的。

對於 undeclared 變數的引用,瀏覽器會報引用錯誤,如 ReferenceError: b is not defined 。但是我們可以使用 typ
eof 的安全防範機制來避免報錯,因為對於 undeclared(或者 not defined )變數,typeof 會返回 "undefined"。

7. null 和 undefined 的區別?

首先 Undefined 和 Null 都是基本資料型別,這兩個基本資料型別分別都只有一個值,就是 undefined 和 null。

undefined 代表的含義是未定義,null 代表的含義是空物件。一般變數聲明瞭但還沒有定義的時候會返回 undefined,null
主要用於賦值給一些可能會返回物件的變數,作為初始化。 undefined 在 js 中不是一個保留字,這意味著我們可以使用 undefined 來作為一個變數名,這樣的做法是非常危險的,它
會影響我們對 undefined 值的判斷。但是我們可以通過一些方法獲得安全的 undefined 值,比如說 void 0。 當我們對兩種型別使用 typeof 進行判斷的時候,Null 型別化會返回 “object”,這是一個歷史遺留的問題。當我們使用雙等
號對兩種型別的值進行比較時會返回 true,使用三個等號時會返回 false。

詳細資料可以參考:

《JavaScript 深入理解之 undefined 與 null》

8. 如何獲取安全的 undefined 值?

因為 undefined 是一個識別符號,所以可以被當作變數來使用和賦值,但是這樣會影響 undefined 的正常判斷。

表示式 void ___ 沒有返回值,因此返回結果是 undefined。void 並不改變表示式的結果,只是讓表示式不返回值。

按慣例我們用 void 0 來獲得 undefined。

9. 說幾條寫 JavaScript 的基本規範?

在平常專案開發中,我們遵守一些這樣的基本規範,比如說:

(1)一個函式作用域中所有的變數宣告應該儘量提到函式首部,用一個 var 宣告,不允許出現兩個連續的 var 宣告,宣告時
如果變數沒有值,應該給該變數賦值對應型別的初始值,便於他人閱讀程式碼時,能夠一目瞭然的知道變數對應的型別值。 (2)程式碼中出現地址、時間等字串時需要使用常量代替。 (3)在進行比較的時候吧,儘量使用'===', '!=='代替'==', '!='。 (4)不要在內建物件的原型上新增方法,如 Array, Date。 (5)switch 語句必須帶有 default 分支。 (6)for 迴圈必須使用大括號。 (7)if 語句必須使用大括號。

10. JavaScript 原型,原型鏈? 有什麼特點?

在 js 中我們是使用建構函式來新建一個物件的,每一個建構函式的內部都有一個 prototype 屬性值,這個屬性值是一個對
象,這個物件包含了可以由該建構函式的所有例項共享的屬性和方法。當我們使用建構函式新建一個物件後,在這個物件的內部
將包含一個指標,這個指標指向建構函式的 prototype 屬性對應的值,在 ES5 中這個指標被稱為物件的原型。一般來說我們
是不應該能夠獲取到這個值的,但是現在瀏覽器中都實現了 __proto__ 屬性來讓我們訪問這個屬性,但是我們最好不要使用這
個屬性,因為它不是規範中規定的。ES5 中新增了一個 Object.getPrototypeOf() 方法,我們可以通過這個方法來獲取對
象的原型。 當我們訪問一個物件的屬性時,如果這個物件內部不存在這個屬性,那麼它就會去它的原型物件裡找這個屬性,這個原型物件又
會有自己的原型,於是就這樣一直找下去,也就是原型鏈的概念。原型鏈的盡頭一般來說都是 Object.prototype 所以這就
是我們新建的物件為什麼能夠使用 toString() 等方法的原因。 特點: JavaScript 物件是通過引用來傳遞的,我們建立的每個新物件實體中並沒有一份屬於自己的原型副本。當我們修改原型時,與
之相關的物件也會繼承這一改變。

詳細資料可以參考:

《JavaScript 深入理解之原型與原型鏈》

11. js 獲取原型的方法?

  • p.proto
  • p.constructor.prototype
  • Object.getPrototypeOf(p)

12. 在 js 中不同進位制數字的表示方式

  • 以 0X、0x 開頭的表示為十六進位制。

  • 以 0、0O、0o 開頭的表示為八進位制。

  • 以 0B、0b 開頭的表示為二進位制格式。

13. js 中整數的安全範圍是多少?

安全整數指的是,在這個範圍內的整數轉化為二進位制儲存的時候不會出現精度丟失,能夠被“安全”呈現的最大整數是 2^53 - 1,
即9007199254740991,在 ES6 中被定義為 Number.MAX_SAFE_INTEGER。最小整數是-9007199254740991,在 ES6 中
被定義為 Number.MIN_SAFE_INTEGER。 如果某次計算的結果得到了一個超過 JavaScript 數值範圍的值,那麼這個值會被自動轉換為特殊的 Infinity 值。如果某次
計算返回了正或負的 Infinity 值,那麼該值將無法參與下一次的計算。判斷一個數是不是有窮的,可以使用 isFinite 函式
來判斷。

14. typeof NaN 的結果是什麼?

NaN 意指“不是一個數字”(not a number),NaN 是一個“警戒值”(sentinel value,有特殊用途的常規值),用於指出
數字型別中的錯誤情況,即“執行數學運算沒有成功,這是失敗後返回的結果”。 typeof NaN; // "number" NaN 是一個特殊值,它和自身不相等,是唯一一個非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN
為 true。

15. isNaN 和 Number.isNaN 函式的區別?

函式 isNaN 接收引數後,會嘗試將這個引數轉換為數值,任何不能被轉換為數值的的值都會返回 true,因此非數字值傳入也會
返回 true ,會影響 NaN 的判斷。 函式 Number.isNaN 會首先判斷傳入引數是否為數字,如果是數字再繼續判斷是否為 NaN ,這種方法對於 NaN 的判斷更為
準確。

16. Array 建構函式只有一個引數值時的表現?

Array 建構函式只帶一個數字引數的時候,該引數會被作為陣列的預設長度(length),而非只充當陣列中的一個元素。這樣
創建出來的只是一個空陣列,只不過它的 length 屬性被設定成了指定的值。 建構函式 Array(..) 不要求必須帶 new 關鍵字。不帶時,它會被自動補上。

17. 其他值到字串的轉換規則?

規範的 9.8 節中定義了抽象操作 ToString ,它負責處理非字串到字串的強制型別轉換。

(1)Null 和 Undefined 型別 ,null 轉換為 "null",undefined 轉換為 "undefined",

(2)Boolean 型別,true 轉換為 "true",false 轉換為 "false"。

(3)Number 型別的值直接轉換,不過那些極小和極大的數字會使用指數形式。

(4)Symbol 型別的值直接轉換,但是隻允許顯式強制型別轉換,使用隱式強制型別轉換會產生錯誤。

(3)對普通物件來說,除非自行定義 toString() 方法,否則會呼叫 toString()(Object.prototype.toString())
來返回內部屬性 [[Class]] 的值,如"[object Object]"。如果物件有自己的 toString() 方法,字串化時就會
呼叫該方法並使用其返回值。

18. 其他值到數字值的轉換規則?

有時我們需要將非數字值當作數字來使用,比如數學運算。為此 ES5 規範在 9.3 節定義了抽象操作 ToNumber。

(1)Undefined 型別的值轉換為 NaN。

(2)Null 型別的值轉換為 0。

(3)Boolean 型別的值,true 轉換為 1,false 轉換為 0。

(4)String 型別的值轉換如同使用 Number() 函式進行轉換,如果包含非數字值則轉換為 NaN,空字串為 0。

(5)Symbol 型別的值不能轉換為數字,會報錯。

(6)物件(包括陣列)會首先被轉換為相應的基本型別值,如果返回的是非數字的基本型別值,則再遵循以上規則將其強制轉換為數字。

為了將值轉換為相應的基本型別值,抽象操作 ToPrimitive 會首先(通過內部操作 DefaultValue)檢查該值是否有valueOf() 方法。如果有並且返回基本型別值,就使用該值進行強制型別轉換。如果沒有就使用 toString() 的返回值(如果存在)來進行強制型別轉換。

如果 valueOf() 和 toString() 均不返回基本型別值,會產生 TypeError 錯誤。

19. 其他值到布林型別的值的轉換規則?

ES5 規範 9.2 節中定義了抽象操作 ToBoolean,列舉了布林強制型別轉換所有可能出現的結果。

以下這些是假值:
• undefined
• null
• false
• +0、-0 和 NaN
• "" 假值的布林強制型別轉換結果為 false。從邏輯上說,假值列表以外的都應該是真值。

20. {} 和 [] 的 valueOf 和 toString 的結果是什麼?

{} 的 valueOf 結果為 {} ,toString 的結果為 "[object Object]"

[] 的 valueOf 結果為 [] ,toString 的結果為 ""

21. 什麼是假值物件?

瀏覽器在某些特定情況下,在常規 JavaScript 語法基礎上自己建立了一些外來值,這些就是“假值物件”。假值物件看起來和
普通物件並無二致(都有屬性,等等),但將它們強制型別轉換為布林值時結果為 false 最常見的例子是 document.all,它
是一個類陣列物件,包含了頁面上的所有元素,由 DOM(而不是 JavaScript 引擎)提供給 JavaScript 程式使用。

22. ~ 操作符的作用?

~ 返回 2 的補碼,並且 ~ 會將數字轉換為 32 位整數,因此我們可以使用 ~ 來進行取整操作。

~x 大致等同於 -(x+1)。

23. 解析字串中的數字和將字串強制型別轉換為數字的返回結果都是數字,它們之間的區別是什麼?

解析允許字串(如 parseInt() )中含有非數字字元,解析按從左到右的順序,如果遇到非數字字元就停止。而轉換(如 Nu
mber ())不允許出現非數字字元,否則會失敗並返回 NaN。

24. + 操作符什麼時候用於字串的拼接?

根據 ES5 規範 11.6.1 節,如果某個運算元是字串或者能夠通過以下步驟轉換為字串的話,+ 將進行拼接操作。如果其
中一個運算元是物件(包括陣列),則首先對其呼叫 ToPrimitive 抽象操作,該抽象操作再呼叫 [[DefaultValue]],以
數字作為上下文。如果不能轉換為字串,則會將其轉換為數字型別來進行計算。 簡單來說就是,如果 + 的其中一個運算元是字串(或者通過以上步驟最終得到字串),則執行字串拼接,否則執行數字
加法。 那麼對於除了加法的運算子來說,只要其中一方是數字,那麼另一方就會被轉為數字。

25. 什麼情況下會發生布爾值的隱式強制型別轉換?

(1) if (..) 語句中的條件判斷表示式。
(2) for ( .. ; .. ; .. ) 語句中的條件判斷表示式(第二個)。
(3) while (..) 和 do..while(..) 迴圈中的條件判斷表示式。
(4) ? : 中的條件判斷表示式。
(5) 邏輯運算子 ||(邏輯或)和 &&(邏輯與)左邊的運算元(作為條件判斷表示式)。

26. || 和 && 操作符的返回值?

|| 和 && 首先會對第一個運算元執行條件判斷,如果其不是布林值就先進行 ToBoolean 強制型別轉換,然後再執行條件
判斷。 對於 || 來說,如果條件判斷結果為 true 就返回第一個運算元的值,如果為 false 就返回第二個運算元的值。 && 則相反,如果條件判斷結果為 true 就返回第二個運算元的值,如果為 false 就返回第一個運算元的值。 || 和 && 返回它們其中一個運算元的值,而非條件判斷的結果

27. Symbol 值的強制型別轉換?

ES6 允許從符號到字串的顯式強制型別轉換,然而隱式強制型別轉換會產生錯誤。

Symbol 值不能夠被強制型別轉換為數字(顯式和隱式都會產生錯誤),但可以被強制型別轉換為布林值(顯式和隱式結果
都是 true )。

28. == 操作符的強制型別轉換規則?

(1)字串和數字之間的相等比較,將字串轉換為數字之後再進行比較。

(2)其他型別和布林型別之間的相等比較,先將布林值轉換為數字後,再應用其他規則進行比較。

(3)null 和 undefined 之間的相等比較,結果為真。其他值和它們進行比較都返回假值。

(4)物件和非物件之間的相等比較,物件先呼叫 ToPrimitive 抽象操作後,再進行比較。

(5)如果一個操作值為 NaN ,則相等比較返回 false( NaN 本身也不等於 NaN )。

(6)如果兩個操作值都是物件,則比較它們是不是指向同一個物件。如果兩個運算元都指向同一個物件,則相等操作符返回 true,否則,返回 false。

詳細資料可以參考:

《JavaScript 字串間的比較》

29. 如何將字串轉化為數字,例如 '12.3b'?

(1)使用 Number() 方法,前提是所包含的字串不包含不合法字元。

(2)使用 parseInt() 方法,parseInt() 函式可解析一個字串,並返回一個整數。還可以設定要解析的數字的基數。當基數的值為 0,或沒有設定該引數時,parseInt() 會根據 string 來判斷數字的基數。

(3)使用 parseFloat() 方法,該函式解析一個字串引數並返回一個浮點數。

(4)使用 + 操作符的隱式轉換。

詳細資料可以參考:

《詳解 JS 中 Number()、parseInt() 和 parseFloat() 的區別》

30. 如何將浮點數點左邊的數每三位新增一個逗號,如 12000000.11 轉化為『12,000,000.11』?

function format(number) {
return number && number.replace(/(?!^)(?=(\d{3})+\.)/g, ",");
}

31. 常用正則表示式

// (1)匹配 16 進位制顏色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; // (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; // (3)匹配 qq 號
var regex = /^[1-9][0-9]{4,10}$/g; // (4)手機號碼正則
var regex = /^1[34578]\d{9}$/g; // (5)使用者名稱正則
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;

詳細資料可以參考:

《前端表單驗證常用的 15 個 JS 正則表示式》

《JS 常用正則彙總》

32. 生成隨機數的各種方法?

《JS - 生成隨機數的方法彙總(不同範圍、型別的隨機數)》

33. 如何實現陣列的隨機排序?

// (1)使用陣列 sort 方法對陣列元素隨機排序,讓 Math.random() 出來的數與 0.5 比較,如果大於就返回 1 交換位置,如果小於就返回 -1,不交換位置。

function randomSort(a, b) {
return Math.random() > 0.5 ? -1 : 1;
} // 缺點:每個元素被派到新陣列的位置不是隨機的,原因是 sort() 方法是依次比較的。 // (2)隨機從原陣列抽取一個元素,加入到新陣列 function randomSort(arr) {
var result = []; while (arr.length > 0) {
var randomIndex = Math.floor(Math.random() * arr.length);
result.push(arr[randomIndex]);
arr.splice(randomIndex, 1);
} return result;
} // (3)隨機交換陣列內的元素(洗牌演算法類似) function randomSort(arr) {
var index,
randomIndex,
temp,
len = arr.length; for (index = 0; index < len; index++) {
randomIndex = Math.floor(Math.random() * (len - index)) + index; temp = arr[index];
arr[index] = arr[randomIndex];
arr[randomIndex] = temp;
} return arr;
} // es6
function randomSort(array) {
let length = array.length; if (!Array.isArray(array) || length <= 1) return; for (let index = 0; index < length - 1; index++) {
let randomIndex = Math.floor(Math.random() * (length - index)) + index; [array[index], array[randomIndex]] = [array[randomIndex], array[index]];
} return array;
}

詳細資料可以參考:

《Fisher and Yates 的原始版》

《javascript 實現陣列隨機排序?》

《JavaScript 學習筆記:陣列隨機排序》

34. javascript 建立物件的幾種方式?

我們一般使用字面量的形式直接建立物件,但是這種建立方式對於建立大量相似物件的時候,會產生大量的重複程式碼。但 js
和一般的面向物件的語言不同,在 ES6 之前它沒有類的概念。但是我們可以使用函式來進行模擬,從而產生出可複用的物件
建立方式,我瞭解到的方式有這麼幾種: (1)第一種是工廠模式,工廠模式的主要工作原理是用函式來封裝建立物件的細節,從而通過呼叫函式來達到複用的目的。但是它有一個很大的問題就是創建出來的物件無法和某個型別聯絡起來,它只是簡單的封裝了複用程式碼,而沒有建立起物件和型別間的關係。 (2)第二種是建構函式模式。js 中每一個函式都可以作為建構函式,只要一個函式是通過 new 來呼叫的,那麼我們就可以把它稱為建構函式。執行建構函式首先會建立一個物件,然後將物件的原型指向建構函式的 prototype 屬性,然後將執行上下文中的 this 指向這個物件,最後再執行整個函式,如果返回值不是物件,則返回新建的物件。因為 this 的值指向了新建的物件,因此我們可以使用 this 給物件賦值。建構函式模式相對於工廠模式的優點是,所建立的物件和建構函式建立起了聯絡,因此我們可以通過原型來識別物件的型別。但是建構函式存在一個缺點就是,造成了不必要的函式物件的建立,因為在 js 中函式也是一個物件,因此如果物件屬性中如果包含函式的話,那麼每次我們都會新建一個函式物件,浪費了不必要的記憶體空間,因為函式是所有的例項都可以通用的。 (3)第三種模式是原型模式,因為每一個函式都有一個 prototype 屬性,這個屬性是一個物件,它包含了通過建構函式建立的所有例項都能共享的屬性和方法。因此我們可以使用原型物件來新增公用屬性和方法,從而實現程式碼的複用。這種方式相對於建構函式模式來說,解決了函式物件的複用問題。但是這種模式也存在一些問題,一個是沒有辦法通過傳入引數來初始化值,另一個是如果存在一個引用型別如 Array 這樣的值,那麼所有的例項將共享一個物件,一個例項對引用型別值的改變會影響所有的例項。 (4)第四種模式是組合使用建構函式模式和原型模式,這是建立自定義型別的最常見方式。因為建構函式模式和原型模式分開使用都存在一些問題,因此我們可以組合使用這兩種模式,通過建構函式來初始化物件的屬性,通過原型物件來實現函式方法的複用。這種方法很好的解決了兩種模式單獨使用時的缺點,但是有一點不足的就是,因為使用了兩種不同的模式,所以對於程式碼的封裝性不夠好。 (5)第五種模式是動態原型模式,這一種模式將原型方法賦值的建立過程移動到了建構函式的內部,通過對屬性是否存在的判斷,可以實現僅在第一次呼叫函式時對原型物件賦值一次的效果。這一種方式很好地對上面的混合模式進行了封裝。 (6)第六種模式是寄生建構函式模式,這一種模式和工廠模式的實現基本相同,我對這個模式的理解是,它主要是基於一個已有的型別,在例項化時對例項化的物件進行擴充套件。這樣既不用修改原來的建構函式,也達到了擴充套件物件的目的。它的一個缺點和工廠模式一樣,無法實現物件的識別。 嗯我目前瞭解到的就是這麼幾種方式。

詳細資料可以參考:

《JavaScript 深入理解之物件建立》

35. JavaScript 繼承的幾種實現方式?

我瞭解的 js 中實現繼承的幾種方式有:

(1)第一種是以原型鏈的方式來實現繼承,但是這種實現方式存在的缺點是,在包含有引用型別的資料時,會被所有的例項物件所共享,容易造成修改的混亂。還有就是在建立子型別的時候不能向超型別傳遞引數。

(2)第二種方式是使用借用建構函式的方式,這種方式是通過在子型別的函式中呼叫超型別的建構函式來實現的,這一種方法解決了不能向超型別傳遞引數的缺點,但是它存在的一個問題就是無法實現函式方法的複用,並且超型別原型定義的方法子型別也沒有辦法訪問到。

(3)第三種方式是組合繼承,組合繼承是將原型鏈和借用建構函式組合起來使用的一種方式。通過借用建構函式的方式來實現型別的屬性的繼承,通過將子型別的原型設定為超型別的例項來實現方法的繼承。這種方式解決了上面的兩種模式單獨使用時的問題,但是由於我們是以超型別的例項來作為子型別的原型,所以呼叫了兩次超類的建構函式,造成了子型別的原型中多了很多不必要的屬性。

(4)第四種方式是原型式繼承,原型式繼承的主要思路就是基於已有的物件來建立新的物件,實現的原理是,向函式中傳入一個物件,然後返回一個以這個物件為原型的物件。這種繼承的思路主要不是為了實現創造一種新的型別,只是對某個物件實現一種簡單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實現。缺點與原型鏈方式相同。

(5)第五種方式是寄生式繼承,寄生式繼承的思路是建立一個用於封裝繼承過程的函式,通過傳入一個物件,然後複製一個物件的副本,然後物件進行擴充套件,最後返回這個物件。這個擴充套件的過程就可以理解是一種繼承。這種繼承的優點就是對一個簡單物件實現繼承,如果這個物件不是我們的自定義型別時。缺點是沒有辦法實現函式的複用。

(6)第六種方式是寄生式組合繼承,組合繼承的缺點就是使用超型別的例項做為子型別的原型,導致添加了不必要的原型屬性。寄生式組合繼承的方式是使用超型別的原型的副本來作為子型別的原型,這樣就避免了建立不必要的屬性。

詳細資料可以參考:

《JavaScript 深入理解之繼承》

36. 寄生式組合繼承的實現?

function Person(name) {
this.name = name;
} Person.prototype.sayName = function() {
console.log("My name is " + this.name + ".");
}; function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
} Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; Student.prototype.sayMyGrade = function() {
console.log("My grade is " + this.grade + ".");
};

37. Javascript 的作用域鏈?

作用域鏈的作用是保證對執行環境有權訪問的所有變數和函式的有序訪問,通過作用域鏈,我們可以訪問到外層環境的變數和
函式。 作用域鏈的本質上是一個指向變數物件的指標列表。變數物件是一個包含了執行環境中所有變數和函式的物件。作用域鏈的前
端始終都是當前執行上下文的變數物件。全域性執行上下文的變數物件(也就是全域性物件)始終是作用域鏈的最後一個物件。 當我們查詢一個變數時,如果當前執行環境中沒有找到,我們可以沿著作用域鏈向後查詢。 作用域鏈的建立過程跟執行上下文的建立有關....

詳細資料可以參考:

《JavaScript 深入理解之作用域鏈》

38. 談談 This 物件的理解。

this 是執行上下文中的一個屬性,它指向最後一次呼叫這個方法的物件。在實際開發中,this 的指向可以通過四種呼叫模
式來判斷。
  • 1.第一種是函式呼叫模式,當一個函式不是一個物件的屬性時,直接作為函式來呼叫時,this 指向全域性物件。

  • 2.第二種是方法呼叫模式,如果一個函式作為一個物件的方法來呼叫時,this 指向這個物件。

  • 3.第三種是構造器呼叫模式,如果一個函式用 new 呼叫時,函式執行前會新建立一個物件,this 指向這個新建立的物件。

  • 4.第四種是 apply 、 call 和 bind 呼叫模式,這三個方法都可以顯示的指定呼叫函式的 this 指向。其中 apply 方法接收兩個引數:一個是 this 繫結的物件,一個是引數陣列。call 方法接收的引數,第一個是 this 繫結的物件,後面的其餘引數是傳入函式執行的引數。也就是說,在使用 call() 方法時,傳遞給函式的引數必須逐個列舉出來。bind 方法通過傳入一個物件,返回一個 this 綁定了傳入物件的新函式。這個函式的 this 指向除了使用 new 時會被改變,其他情況下都不會改變。

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

《JavaScript 深入理解之 this 詳解》

39. eval 是做什麼的?

它的功能是把對應的字串解析成 JS 程式碼並執行。

應該避免使用 eval,不安全,非常耗效能(2次,一次解析成 js 語句,一次執行)。

詳細資料可以參考:

《eval()》

40. 什麼是 DOM 和 BOM?

DOM 指的是文件物件模型,它指的是把文件當做一個物件來對待,這個物件主要定義了處理網頁內容的方法和介面。

BOM 指的是瀏覽器物件模型,它指的是把瀏覽器當做一個物件來對待,這個物件主要定義了與瀏覽器進行互動的法和介面。BOM
的核心是 window,而 window 物件具有雙重角色,它既是通過 js 訪問瀏覽器視窗的一個介面,又是一個 Global(全域性)
物件。這意味著在網頁中定義的任何物件,變數和函式,都作為全域性物件的一個屬性或者方法存在。window 物件含有 locati
on 物件、navigator 物件、screen 物件等子物件,並且 DOM 的最根本的物件 document 物件也是 BOM 的 window 對
象的子物件。

詳細資料可以參考:

《DOM, DOCUMENT, BOM, WINDOW 有什麼區別?》

《Window 物件》

《DOM 與 BOM 分別是什麼,有何關聯?》

《JavaScript 學習總結(三)BOM 和 DOM 詳解》

41. 寫一個通用的事件偵聽器函式。

const EventUtils = {
// 視能力分別使用dom0||dom2||IE方式 來繫結事件
// 新增事件
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
}, // 移除事件
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
}, // 獲取事件目標
getTarget: function(event) {
return event.target || event.srcElement;
}, // 獲取 event 物件的引用,取到事件的所有資訊,確保隨時能使用 event
getEvent: function(event) {
return event || window.event;
}, // 阻止事件(主要是事件冒泡,因為 IE 不支援事件捕獲)
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}, // 取消事件的預設行為
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};

詳細資料可以參考:

《JS 事件模型》

42. 事件是什麼?IE 與火狐的事件機制有什麼區別? 如何阻止冒泡?

  • 1.事件是使用者操作網頁時發生的互動動作,比如 click/move, 事件除了使用者觸發的動作外,還可以是文件載入,視窗滾動和大小調整。事件被封裝成一個 event 物件,包含了該事件發生時的所有相關資訊( event 的屬性)以及可以對事件進行的操作( event 的方法)。

  • 2.事件處理機制:IE 支援事件冒泡、Firefox 同時支援兩種事件模型,也就是:事件冒泡和事件捕獲。

  • 3.event.stopPropagation() 或者 ie 下的方法 event.cancelBubble = true;

詳細資料可以參考:

《Javascript 事件模型系列(一)事件及事件的三種模型》

《Javascript 事件模型:事件捕獲和事件冒泡》

43. 三種事件模型是什麼?

事件是使用者操作網頁時發生的互動動作或者網頁本身的一些操作,現代瀏覽器一共有三種事件模型。

第一種事件模型是最早的 DOM0 級模型,這種模型不會傳播,所以沒有事件流的概念,但是現在有的瀏覽器支援以冒泡的方式實
現,它可以在網頁中直接定義監聽函式,也可以通過 js 屬性來指定監聽函式。這種方式是所有瀏覽器都相容的。 第二種事件模型是 IE 事件模型,在該事件模型中,一次事件共有兩個過程,事件處理階段,和事件冒泡階段。事件處理階段會首先執行目標元素繫結的監聽事件。然後是事件冒泡階段,冒泡指的是事件從目標元素冒泡到 document,依次檢查經過的節點是否綁定了事件監聽函式,如果有則執行。這種模型通過 attachEvent 來新增監聽函式,可以新增多個監聽函式,會按順序依次執行。 第三種是 DOM2 級事件模型,在該事件模型中,一次事件共有三個過程,第一個過程是事件捕獲階段。捕獲指的是事件從 document 一直向下傳播到目標元素,依次檢查經過的節點是否綁定了事件監聽函式,如果有則執行。後面兩個階段和 IE 事件模型的兩個階段相同。這種事件模型,事件繫結的函式是 addEventListener,其中第三個引數可以指定事件是否在捕獲階段執行。

詳細資料可以參考:

《一個 DOM 元素繫結多個事件時,先執行冒泡還是捕獲》

44. 事件委託是什麼?

事件委託本質上是利用了瀏覽器事件冒泡的機制。因為事件在冒泡過程中會上傳到父節點,並且父節點可以通過事件物件獲取到
目標節點,因此可以把子節點的監聽函式定義在父節點上,由父節點的監聽函式統一處理多個子元素的事件,這種方式稱為事件代理。 使用事件代理我們可以不必要為每一個子元素都繫結一個監聽事件,這樣減少了記憶體上的消耗。並且使用事件代理我們還可以實現事件的動態繫結,比如說新增了一個子節點,我們並不需要單獨地為它新增一個監聽事件,它所發生的事件會交給父元素中的監聽函式來處理。

詳細資料可以參考:

《JavaScript 事件委託詳解》

45. ["1", "2", "3"].map(parseInt) 答案是多少?

parseInt() 函式能解析一個字串,並返回一個整數,需要兩個引數 (val, radix),其中 radix 表示要解析的數字的基數。(該值介於 2 ~ 36 之間,並且字串中的數字不能大於 radix 才能正確返回數字結果值)。

此處 map 傳了 3 個引數 (element, index, array),預設第三個引數被忽略掉,因此三次傳入的引數分別為 "1-0", "2-1", "3-2"

因為字串的值不能大於基數,因此後面兩次呼叫均失敗,返回 NaN ,第一次基數為 0 ,按十進位制解析返回 1。

詳細資料可以參考:

《為什麼 ["1", "2", "3"].map(parseInt) 返回 [1,NaN,NaN]?》

46. 什麼是閉包,為什麼要用它?

閉包是指有權訪問另一個函式作用域中變數的函式,建立閉包的最常見的方式就是在一個函式內建立另一個函式,建立的函式可以
訪問到當前函式的區域性變數。 閉包有兩個常用的用途。 閉包的第一個用途是使我們在函式外部能夠訪問到函式內部的變數。通過使用閉包,我們可以通過在外部呼叫閉包函式,從而在外
部訪問到函式內部的變數,可以使用這種方法來建立私有變數。 函式的另一個用途是使已經執行結束的函式上下文中的變數物件繼續留在記憶體中,因為閉包函式保留了這個變數物件的引用,所以
這個變數物件不會被回收。 其實閉包的本質就是作用域鏈的一個特殊的應用,只要瞭解了作用域鏈的建立過程,就能夠理解閉包的實現原理。

詳細資料可以參考:

《JavaScript 深入理解之閉包》

47. javascript 程式碼中的 "use strict"; 是什麼意思 ? 使用它區別是什麼?

相關知識點:

use strict 是一種 ECMAscript5 新增的(嚴格)執行模式,這種模式使得 Javascript 在更嚴格的條件下執行。

設立"嚴格模式"的目的,主要有以下幾個:
  • 消除 Javascript 語法的一些不合理、不嚴謹之處,減少一些怪異行為;
  • 消除程式碼執行的一些不安全之處,保證程式碼執行的安全;
  • 提高編譯器效率,增加執行速度;
  • 為未來新版本的 Javascript 做好鋪墊。

區別:

  • 1.禁止使用 with 語句。
  • 2.禁止 this 關鍵字指向全域性物件。
  • 3.物件不能有重名的屬性。

回答:

use strict 指的是嚴格執行模式,在這種模式對 js 的使用添加了一些限制。比如說禁止 this 指向全域性物件,還有禁止使
用 with 語句等。設立嚴格模式的目的,主要是為了消除程式碼使用中的一些不安全的使用方式,也是為了消除 js 語法本身的一
些不合理的地方,以此來減少一些執行時的怪異的行為。同時使用嚴格執行模式也能夠提高編譯的效率,從而提高程式碼的執行速度。
我認為嚴格模式代表了 js 一種更合理、更安全、更嚴謹的發展方向。

詳細資料可以參考:

《Javascript 嚴格模式詳解》

48. 如何判斷一個物件是否屬於某個類?

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

第二種方式可以通過物件的 constructor 屬性來判斷,物件的 constructor 屬性指向該物件的建構函式,但是這種方式不是很安全,因為 constructor 屬性可以被改寫。

第三種方式,如果需要判斷的是某個內建的引用型別的話,可以使用 Object.prototype.toString() 方法來列印物件的
[[Class]] 屬性來進行判斷。

詳細資料可以參考:

《js 判斷一個物件是否屬於某一類》

49. instanceof 的作用?

// instanceof 運算子用於判斷建構函式的 prototype 屬性是否出現在物件的原型鏈中的任何位置。
// 實現: function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left), // 獲取物件的原型
prototype = right.prototype; // 獲取建構函式的 prototype 物件 // 判斷建構函式的 prototype 物件是否在物件的原型鏈上
while (true) {
if (!proto) return false;
if (proto === prototype) return true; proto = Object.getPrototypeOf(proto);
}
}

詳細資料可以參考:

《instanceof》

50. new 操作符具體幹了什麼呢?如何實現?

// (1)首先建立了一個新的空物件
// (2)設定原型,將物件的原型設定為函式的 prototype 物件。
// (3)讓函式的 this 指向這個物件,執行建構函式的程式碼(為這個新物件新增屬性)
// (4)判斷函式的返回值型別,如果是值型別,返回建立的物件。如果是引用型別,就返回這個引用型別的物件。 // 實現: function objectFactory() {
let newObject = null,
constructor = Array.prototype.shift.call(arguments),
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(建構函式, 初始化引數);

詳細資料可以參考:

《new 操作符具體幹了什麼?》

《JavaScript 深入之 new 的模擬實現》

51. Javascript 中,有一個函式,執行時物件查詢時,永遠不會去查詢原型,這個函式是?

hasOwnProperty

所有繼承了 Object 的物件都會繼承到 hasOwnProperty 方法。這個方法可以用來檢測一個物件是否含有特定的自身屬性,和
in 運算子不同,該方法會忽略掉那些從原型鏈上繼承到的屬性。

詳細資料可以參考:

《Object.prototype.hasOwnProperty()》

52. 對於 JSON 的瞭解?

相關知識點:

JSON 是一種資料交換格式,基於文字,優於輕量,用於交換資料。

JSON 可以表示數字、布林值、字串、null、陣列(值的有序序列),以及由這些值(或陣列、物件)所組成的物件(字串與
值的對映)。 JSON 使用 JavaScript 語法,但是 JSON 格式僅僅是一個文字。文字可以被任何程式語言讀取及作為資料格式傳遞。

回答:

JSON 是一種基於文字的輕量級的資料交換格式。它可以被任何的程式語言讀取和作為資料格式來傳遞。

在專案開發中,我們使用 JSON 作為前後端資料交換的方式。在前端我們通過將一個符合 JSON 格式的資料結構序列化為 JSON 字串,然後將它傳遞到後端,後端通過 JSON 格式的字串解析後生成對應的資料結構,以此來實現前後端資料的一個傳遞。

因為 JSON 的語法是基於 js 的,因此很容易將 JSON 和 js 中的物件弄混,但是我們應該注意的是 JSON 和 js 中的物件不是一回事,JSON 中物件格式更加嚴格,比如說在 JSON 中屬性值不能為函式,不能出現 NaN 這樣的屬性值等,因此大多數的 js 物件是不符合 JSON 物件的格式的。

在 js 中提供了兩個函式來實現 js 資料結構和 JSON 格式的轉換處理,一個是 JSON.stringify 函式,通過傳入一個符合 JSON 格式的資料結構,將其轉換為一個 JSON 字串。如果傳入的資料結構不符合 JSON 格式,那麼在序列化的時候會對這些值進行對應的特殊處理,使其符合規範。在前端向後端傳送資料時,我們可以呼叫這個函式將資料物件轉化為 JSON 格式的字串。

另一個函式 JSON.parse() 函式,這個函式用來將 JSON 格式的字串轉換為一個 js 資料結構,如果傳入的字串不是標準的 JSON 格式的字串的話,將會丟擲錯誤。當我們從後端接收到 JSON 格式的字串時,我們可以通過這個方法來將其解析為一個 js 資料結構,以此來進行資料的訪問。

詳細資料可以參考:

《深入瞭解 JavaScript 中的 JSON 》

53. [].forEach.call($$(""),function(a){a.style.outline="1px solid #"+(~~(Math.random()(1<<24))).toString(16)}) 能解釋一下這段程式碼的意思嗎?

(1)選取頁面所有 DOM 元素。在瀏覽器的控制檯中可以使用$$()方法來獲取頁面中相應的元素,這是現代瀏覽器提供的一個命令列 API 相當於 document.querySelectorAll 方法。

(2)迴圈遍歷 DOM 元素

(3)給元素新增 outline 。由於渲染的 outline 是不在 CSS 盒模型中的,所以為元素新增 outline 並不會影響元素的大小和頁面的佈局。

(4)生成隨機顏色函式。Math.random()*(1<<24) 可以得到 0~2^24 - 1 之間的隨機數,因為得到的是一個浮點數,但我們只需要整數部分,使用取反操作符 ~ 連續兩次取反獲得整數部分,然後再用 toString(16) 的方式,轉換為一個十六進位制的字串。

詳細資料可以參考:

《通過一行程式碼學 JavaScript》

54. js 延遲載入的方式有哪些?

相關知識點:

js 延遲載入,也就是等頁面載入完成之後再載入 JavaScript 檔案。 js 延遲載入有助於提高頁面載入速度。

一般有以下幾種方式:

  • defer 屬性
  • async 屬性
  • 動態建立 DOM 方式
  • 使用 setTimeout 延遲方法
  • 讓 JS 最後載入

回答:

js 的載入、解析和執行會阻塞頁面的渲染過程,因此我們希望 js 指令碼能夠儘可能的延遲載入,提高頁面的渲染速度。

我瞭解到的幾種方式是:

第一種方式是我們一般採用的是將 js 指令碼放在文件的底部,來使 js 指令碼儘可能的在最後來載入執行。

第二種方式是給 js 指令碼新增 defer 屬性,這個屬性會讓指令碼的載入與文件的解析同步解析,然後在文件解析完成後再執行這個指令碼檔案,這樣的話就能使頁面的渲染不被阻塞。多個設定了 defer 屬性的指令碼按規範來說最後是順序執行的,但是在一些瀏覽器中可能不是這樣。

第三種方式是給 js 指令碼新增 async 屬性,這個屬性會使指令碼非同步載入,不會阻塞頁面的解析過程,但是當指令碼載入完成後立即執行 js 指令碼,這個時候如果文件沒有解析完成的話同樣會阻塞。多個 async 屬性的指令碼的執行順序是不可預測的,一般不會按照程式碼的順序依次執行。

第四種方式是動態建立 DOM 標籤的方式,我們可以對文件的載入事件進行監聽,當文件載入完成後再動態的建立 script 標籤來引入 js 指令碼。

詳細資料可以參考:

《JS 延遲載入的幾種方式》

《HTML 5 <script> async 屬性》

55. Ajax 是什麼? 如何建立一個 Ajax?

相關知識點:

2005 年 2 月,AJAX 這個詞第一次正式提出,它是 Asynchronous JavaScript and XML 的縮寫,指的是通過 JavaScript 的

非同步通訊,從伺服器獲取 XML 文件從中提取資料,再更新當前網頁的對應部分,而不用重新整理整個網頁。

具體來說,AJAX 包括以下幾個步驟。

  • 1.建立 XMLHttpRequest 物件,也就是建立一個非同步呼叫物件
  • 2.建立一個新的 HTTP 請求,並指定該 HTTP 請求的方法、URL 及驗證資訊
  • 3.設定響應 HTTP 請求狀態變化的函式
  • 4.傳送 HTTP 請求
  • 5.獲取非同步呼叫返回的資料
  • 6.使用 JavaScript 和 DOM 實現區域性重新整理

一般實現:

const SERVER_URL = "/server";

let xhr = new XMLHttpRequest();

// 建立 Http 請求
xhr.open("GET", SERVER_URL, true); // 設定狀態監聽函式
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return; // 當請求成功時
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
}; // 設定請求失敗時的監聽函式
xhr.onerror = function() {
console.error(this.statusText);
}; // 設定請求頭資訊
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json"); // 傳送 Http 請求
xhr.send(null); // promise 封裝實現: function getJSON(url) {
// 建立一個 promise 物件
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest(); // 新建一個 http 請求
xhr.open("GET", url, true); // 設定狀態的監聽函式
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return; // 當請求成功或失敗時,改變 promise 的狀態
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
}; // 設定錯誤監聽函式
xhr.onerror = function() {
reject(new Error(this.statusText));
}; // 設定響應的資料型別
xhr.responseType = "json"; // 設定請求頭資訊
xhr.setRequestHeader("Accept", "application/json"); // 傳送 http 請求
xhr.send(null);
}); return promise;
}

回答:

我對 ajax 的理解是,它是一種非同步通訊的方法,通過直接由 js 指令碼向伺服器發起 http 通訊,然後根據伺服器返回的資料,更新網頁的相應部分,而不用重新整理整個頁面的一種方法。

建立一個 ajax 有這樣幾個步驟

首先是建立一個 XMLHttpRequest 物件。

然後在這個物件上使用 open 方法建立一個 http 請求,open 方法所需要的引數是請求的方法、請求的地址、是否非同步和使用者的認證資訊。

在發起請求前,我們可以為這個物件新增一些資訊和監聽函式。比如說我們可以通過 setRequestHeader 方法來為請求新增頭資訊。我們還可以為這個物件新增一個狀態監聽函式。一個 XMLHttpRequest 物件一共有 5 個狀態,當它的狀態變化時會觸發onreadystatechange 事件,我們可以通過設定監聽函式,來處理請求成功後的結果。當物件的 readyState 變為 4 的時候,代表伺服器返回的資料接收完成,這個時候我們可以通過判斷請求的狀態,如果狀態是 2xx 或者 304 的話則代表返回正常。這個時候我們就可以通過 response 中的資料來對頁面進行更新了。

當物件的屬性和監聽函式設定完成後,最後我們呼叫 sent 方法來向伺服器發起請求,可以傳入引數作為傳送的資料體。

詳細資料可以參考:

《XMLHttpRequest 物件》

《從 ajax 到 fetch、axios》

《Fetch 入門》

《傳統 Ajax 已死,Fetch 永生》

56. 談一談瀏覽器的快取機制?

瀏覽器的快取機制指的是通過在一段時間內保留已接收到的 web 資源的一個副本,如果在資源的有效時間內,發起了對這個資源的再一次請求,那麼瀏覽器會直接使用快取的副本,而不是向伺服器發起請求。使用 web 快取可以有效地提高頁面的開啟速度,減少不必要的網路頻寬的消耗。

web 資源的快取策略一般由伺服器來指定,可以分為兩種,分別是強快取策略和協商快取策略。

使用強快取策略時,如果快取資源有效,則直接使用快取資源,不必再向伺服器發起請求。強快取策略可以通過兩種方式來設定,分別是 http 頭資訊中的 Expires 屬性和 Cache-Control 屬性。

伺服器通過在響應頭中新增 Expires 屬性,來指定資源的過期時間。在過期時間以內,該資源可以被快取使用,不必再向伺服器傳送請求。這個時間是一個絕對時間,它是伺服器的時間,因此可能存在這樣的問題,就是客戶端的時間和伺服器端的時間不一致,或者使用者可以對客戶端時間進行修改的情況,這樣就可能會影響快取命中的結果。

Expires 是 http1.0 中的方式,因為它的一些缺點,在 http 1.1 中提出了一個新的頭部屬性就是 Cache-Control 屬性,
它提供了對資源的快取的更精確的控制。它有很多不同的值,常用的比如我們可以通過設定 max-age 來指定資源能夠被快取的時間
的大小,這是一個相對的時間,它會根據這個時間的大小和資源第一次請求時的時間來計算出資源過期的時間,因此相對於 Expires
來說,這種方式更加有效一些。常用的還有比如 private ,用來規定資源只能被客戶端快取,不能夠代理伺服器所快取。還有如 n
o-store ,用來指定資源不能夠被快取,no-cache 代表該資源能夠被快取,但是立即失效,每次都需要向伺服器發起請求。 一般來說只需要設定其中一種方式就可以實現強快取策略,當兩種方式一起使用時,Cache-Control 的優先順序要高於 Expires 。 使用協商快取策略時,會先向伺服器傳送一個請求,如果資源沒有發生修改,則返回一個 304 狀態,讓瀏覽器使用本地的快取副本。
如果資源發生了修改,則返回修改後的資源。協商快取也可以通過兩種方式來設定,分別是 http 頭資訊中的 Etag 和 Last-Modified 屬性。 伺服器通過在響應頭中新增 Last-Modified 屬性來指出資源最後一次修改的時間,當瀏覽器下一次發起請求時,會在請求頭中新增一個 If-Modified-Since 的屬性,屬性值為上一次資源返回時的 Last-Modified 的值。當請求傳送到伺服器後伺服器會通過這個屬性來和資源的最後一次的修改時間來進行比較,以此來判斷資源是否做了修改。如果資源沒有修改,那麼返回 304 狀態,讓客戶端使用本地的快取。如果資源已經被修改了,則返回修改後的資源。使用這種方法有一個缺點,就是 Last-Modified 標註的最後修改時間只能精確到秒級,如果某些檔案在1秒鐘以內,被修改多次的話,那麼檔案已將改變了但是 Last-Modified 卻沒有改變,
這樣會造成快取命中的不準確。 因為 Last-Modified 的這種可能發生的不準確性,http 中提供了另外一種方式,那就是 Etag 屬性。伺服器在返回資源的時候,在頭資訊中添加了 Etag 屬性,這個屬性是資源生成的唯一識別符號,當資源發生改變的時候,這個值也會發生改變。在下一次資源請求時,瀏覽器會在請求頭中新增一個 If-None-Match 屬性,這個屬性的值就是上次返回的資源的 Etag 的值。服務接收到請求後會根據這個值來和資源當前的 Etag 的值來進行比較,以此來判斷資源是否發生改變,是否需要返回資源。通過這種方式,比 Last-Modified 的方式更加精確。 當 Last-Modified 和 Etag 屬性同時出現的時候,Etag 的優先順序更高。使用協商快取的時候,伺服器需要考慮負載平衡的問題,因此多個伺服器上資源的 Last-Modified 應該保持一致,因為每個伺服器上 Etag 的值都不一樣,因此在考慮負載平衡時,最好不要設定 Etag 屬性。 強快取策略和協商快取策略在快取命中時都會直接使用本地的快取副本,區別只在於協商快取會向伺服器傳送一次請求。它們快取不命中時,都會向伺服器傳送請求來獲取資源。在實際的快取機制中,強快取策略和協商快取策略是一起合作使用的。瀏覽器首先會根據請求的資訊判斷,強快取是否命中,如果命中則直接使用資源。如果不命中則根據頭資訊向伺服器發起請求,使用協商快取,如果協商快取命中的話,則伺服器不返回資源,瀏覽器直接使用本地資源的副本,如果協商快取不命中,則瀏覽器返回最新的資源給瀏覽器。

詳細資料可以參考:

《淺談瀏覽器快取》

《前端優化:瀏覽器快取技術介紹》

《請求頭中的 Cache-Control》

《Cache-Control 欄位值詳解》

57. Ajax 解決瀏覽器快取問題?

  • 1.在 ajax 傳送請求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since","0")。

  • 2.在 ajax 傳送請求前加上 anyAjaxObj.setRequestHeader("Cache-Control","no-cache")。

  • 3.在 URL 後面加上一個隨機數: "fresh=" + Math.random();。

  • 4.在 URL 後面加上時間戳:"nowtime=" + new Date().getTime();。

  • 5.如果是使用 jQuery,直接這樣就可以了$.ajaxSetup({cache:false})。這樣頁面的所有 ajax 都會執行這條語句就是不需要儲存快取記錄。

詳細資料可以參考:

《Ajax 中瀏覽器的快取問題解決方法》

《淺談瀏覽器快取》

58. 同步和非同步的區別?

相關知識點:

同步,可以理解為在執行完一個函式或方法之後,一直等待系統返回值或訊息,這時程式是處於阻塞的,只有接收到返回的值或訊息後才往下執行其他的命令。  

非同步,執行完函式或方法後,不必阻塞性地等待返回值或訊息,只需要向系統委託一個非同步過程,那麼當系統接收到返回值或訊息時,系統會自動觸發委託的非同步過程,從而完成一個完整的流程。 

回答:

同步指的是當一個程序在執行某個請求的時候,如果這個請求需要等待一段時間才能返回,那麼這個程序會一直等待下去,直到訊息返
回為止再繼續向下執行。 非同步指的是當一個程序在執行某個請求的時候,如果這個請求需要等待一段時間才能返回,這個時候程序會繼續往下執行,不會阻塞等
待訊息的返回,當訊息返回時系統再通知程序進行處理。

詳細資料可以參考:

《同步和非同步的區別》

59. 什麼是瀏覽器的同源政策?

我對瀏覽器的同源政策的理解是,一個域下的 js 指令碼在未經允許的情況下,不能夠訪問另一個域的內容。這裡的同源的指的是兩個
域的協議、域名、埠號必須相同,否則則不屬於同一個域。 同源政策主要限制了三個方面 第一個是當前域下的 js 指令碼不能夠訪問其他域下的 cookie、localStorage 和 indexDB。 第二個是當前域下的 js 指令碼不能夠操作訪問操作其他域下的 DOM。 第三個是當前域下 ajax 無法傳送跨域請求。 同源政策的目的主要是為了保證使用者的資訊保安,它只是對 js 指令碼的一種限制,並不是對瀏覽器的限制,對於一般的 img、或者
script 指令碼請求都不會有跨域的限制,這是因為這些操作都不會通過響應結果來進行可能出現安全問題的操作。

60. 如何解決跨域問題?

相關知識點:

  • 1.通過 jsonp 跨域
  • 2.document.domain + iframe 跨域
  • 3.location.hash + iframe
  • 4.window.name + iframe 跨域
  • 5.postMessage 跨域
  • 6.跨域資源共享(CORS)
  • 7.nginx 代理跨域
  • 8.nodejs 中介軟體代理跨域
  • 9.WebSocket 協議跨域

回答:

解決跨域的方法我們可以根據我們想要實現的目的來劃分。

首先我們如果只是想要實現主域名下的不同子域名的跨域操作,我們可以使用設定 document.domain 來解決。

(1)將 document.domain 設定為主域名,來實現相同子域名的跨域操作,這個時候主域名下的 cookie 就能夠被子域名所訪問。同時如果文件中含有主域名相同,子域名不同的 iframe 的話,我們也可以對這個 iframe 進行操作。

如果是想要解決不同跨域視窗間的通訊問題,比如說一個頁面想要和頁面的中的不同源的 iframe 進行通訊的問題,我們可以使用 location.hash 或者 window.name 或者 postMessage 來解決。

(2)使用 location.hash 的方法,我們可以在主頁面動態的修改 iframe 視窗的 hash 值,然後在 iframe 窗口裡實現監聽函式來實現這樣一個單向的通訊。因為在 iframe 是沒有辦法訪問到不同源的父級視窗的,所以我們不能直接修改父級視窗的 hash 值來實現通訊,我們可以在 iframe 中再加入一個 iframe ,這個 iframe 的內容是和父級頁面同源的,所以我們可以 window.parent.parent 來修改最頂級頁面的 src,以此來實現雙向通訊。

(3)使用 window.name 的方法,主要是基於同一個視窗中設定了 window.name 後不同源的頁面也可以訪問,所以不同源的子頁面可以首先在 window.name 中寫入資料,然後跳轉到一個和父級同源的頁面。這個時候級頁面就可以訪問同源的子頁面中 window.name 中的資料了,這種方式的好處是可以傳輸的資料量大。

(4)使用 postMessage 來解決的方法,這是一個 h5 中新增的一個 api。通過它我們可以實現多視窗間的資訊傳遞,通過獲取到指定視窗的引用,然後呼叫 postMessage 來發送資訊,在視窗中我們通過對 message 資訊的監聽來接收資訊,以此來實現不同源間的資訊交換。

如果是像解決 ajax 無法提交跨域請求的問題,我們可以使用 jsonp、cors、websocket 協議、伺服器代理來解決問題。

(5)使用 jsonp 來實現跨域請求,它的主要原理是通過動態構建 script  標籤來實現跨域請求,因為瀏覽器對 script 標籤的引入沒有跨域的訪問限制 。通過在請求的 url 後指定一個回撥函式,然後伺服器在返回資料的時候,構建一個 json 資料的包裝,這個包裝就是回撥函式,然後返回給前端,前端接收到資料後,因為請求的是指令碼檔案,所以會直接執行,這樣我們先前定義好的回撥函式就可以被呼叫,從而實現了跨域請求的處理。這種方式只能用於 get 請求。

(6)使用 CORS 的方式,CORS 是一個 W3C 標準,全稱是"跨域資源共享"。CORS 需要瀏覽器和伺服器同時支援。目前,所有瀏覽器都支援該功能,因此我們只需要在伺服器端配置就行。瀏覽器將 CORS 請求分成兩類:簡單請求和非簡單請求。對於簡單請求,瀏覽器直接發出 CORS 請求。具體來說,就是會在頭資訊之中,增加一個 Origin 欄位。Origin 欄位用來說明本次請求來自哪個源。伺服器根據這個值,決定是否同意這次請求。對於如果 Origin 指定的源,不在許可範圍內,伺服器會返回一個正常的 HTTP 迴應。瀏覽器發現,這個迴應的頭資訊沒有包含 Access-Control-Allow-Origin 欄位,就知道出錯了,從而丟擲一個錯誤,ajax 不會收到響應資訊。如果成功的話會包含一些以 Access-Control- 開頭的欄位。

非簡單請求,瀏覽器會先發出一次預檢請求,來判斷該域名是否在伺服器的白名單中,如果收到肯定回覆後才會發起請求。

(7)使用 websocket 協議,這個協議沒有同源限制。

(8)使用伺服器來代理跨域的訪問請求,就是有跨域的請求操作時傳送請求給後端,讓後端代為請求,然後最後將獲取的結果發返回。

詳細資料可以參考:

《前端常見跨域解決方案(全)》

《瀏覽器同源政策及其規避方法》

《跨域,你需要知道的全在這裡》

《為什麼 form 表單提交沒有跨域問題,但 ajax 提交有跨域問題?》

61. 伺服器代理轉發時,該如何處理 cookie?

詳細資料可以參考:

《深入淺出 Nginx》

62. 簡單談一下 cookie ?

我的理解是 cookie 是伺服器提供的一種用於維護會話狀態資訊的資料,通過伺服器傳送到瀏覽器,瀏覽器儲存在本地,當下一次有同源的請求時,將儲存的 cookie 值新增到請求頭部,傳送給服務端。這可以用來實現記錄使用者登入狀態等功能。cookie 一般可以儲存 4k 大小的資料,並且只能夠被同源的網頁所共享訪問。

伺服器端可以使用 Set-Cookie 的響應頭部來配置 cookie 資訊。一條cookie 包括了5個屬性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的時間,domain 是域名、path是路徑,domain 和 path 一起限制了 cookie 能夠被哪些 url 訪問。secure 規定了 cookie 只能在確保安全的情況下傳輸,HttpOnly 規定了這個 cookie 只能被伺服器訪問,不能使用 js 指令碼訪問。

在發生 xhr 的跨域請求的時候,即使是同源下的 cookie,也不會被自動新增到請求頭部,除非顯示地規定。

詳細資料可以參考:

《HTTP cookies》

《聊一聊 cookie》

63. 模組化開發怎麼做?

我對模組的理解是,一個模組是實現一個特定功能的一組方法。在最開始的時候,js 只實現一些簡單的功能,所以並沒有模組的概念
,但隨著程式越來越複雜,程式碼的模組化開發變得越來越重要。 由於函式具有獨立作用域的特點,最原始的寫法是使用函式來作為模組,幾個函式作為一個模組,但是這種方式容易造成全域性變數的汙
染,並且模組間沒有聯絡。 後面提出了物件寫法,通過將函式作為一個物件的方法來實現,這樣解決了直接使用函式作為模組的一些缺點,但是這種辦法會暴露所
有的所有的模組成員,外部程式碼可以修改內部屬性的值。 現在最常用的是立即執行函式的寫法,通過利用閉包來實現模組私有作用域的建立,同時不會對全域性作用域造成汙染。

詳細資料可以參考:

《淺談模組化開發》

《Javascript 模組化程式設計(一):模組的寫法》

《前端模組化:CommonJS,AMD,CMD,ES6》

《Module 的語法》

64. js 的幾種模組規範?

js 中現在比較成熟的有四種模組載入方案。

第一種是 CommonJS 方案,它通過 require 來引入模組,通過 module.exports 定義模組的輸出介面。這種模組載入方案是
伺服器端的解決方案,它是以同步的方式來引入模組的,因為在服務端檔案都儲存在本地磁碟,所以讀取非常快,所以以同步的方式
載入沒有問題。但如果是在瀏覽器端,由於模組的載入是使用網路請求,因此使用非同步載入的方式更加合適。 第二種是 AMD 方案,這種方案採用非同步載入的方式來載入模組,模組的載入不影響後面語句的執行,所有依賴這個模組的語句都定
義在一個回撥函式裡,等到載入完成後再執行回撥函式。require.js 實現了 AMD 規範。 第三種是 CMD 方案,這種方案和 AMD 方案都是為了解決非同步模組載入的問題,sea.js 實現了 CMD 規範。它和 require.js
的區別在於模組定義時對依賴的處理不同和對依賴模組的執行時機的處理不同。參考60 第四種方案是 ES6 提出的方案,使用 import 和 export 的形式來匯入匯出模組。這種方案和上面三種方案都不同。參考 61。

65. AMD 和 CMD 規範的區別?

它們之間的主要區別有兩個方面。

(1)第一個方面是在模組定義時對依賴的處理不同。AMD 推崇依賴前置,在定義模組的時候就要宣告其依賴的模組。而 CMD 推崇

就近依賴,只有在用到某個模組的時候再去 require。

(2)第二個方面是對依賴模組的執行時機處理不同。首先 AMD 和 CMD 對於模組的載入方式都是非同步載入,不過它們的區別在於

模組的執行時機,AMD 在依賴模組載入完成後就直接執行依賴模組,依賴模組的執行順序和我們書寫的順序不一定一致。而 CMD

在依賴模組載入完成後並不執行,只是下載而已,等到所有的依賴模組都載入好後,進入回撥函式邏輯,遇到 require 語句

的時候才執行對應的模組,這樣模組的執行順序就和我們書寫的順序保持一致了。

// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此處略去 100 行
var b = require("./b"); // 依賴可以就近書寫
b.doSomething();
// ...
}); // AMD 預設推薦
define(["./a", "./b"], function(a, b) {
// 依賴必須一開始就寫好
a.doSomething();
// 此處略去 100 行
b.doSomething();
// ...
});

詳細資料可以參考:

《前端模組化,AMD 與 CMD 的區別》

66. ES6 模組與 CommonJS 模組、AMD、CMD 的差異。

  • 1.CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。ES6 模組的執行機制與 CommonJS 不一樣。JS 引擎對指令碼靜態分析的時候,遇到模組載入命令 import,就會生成一個只讀引用。等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。

  • 2.CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。CommonJS 模組就是物件,即在輸入時是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為“執行時載入”。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。

67. requireJS 的核心原理是什麼?(如何動態載入的?如何避免多次載入的?如何 快取的?)

require.js 的核心原理是通過動態建立 script 指令碼來非同步引入模組,然後對每個指令碼的 load 事件進行監聽,如果每個指令碼都載入完成了,再呼叫回撥函式。

詳細資料可以參考:

《requireJS 的用法和原理分析》

《requireJS 的核心原理是什麼?》

《從 RequireJs 原始碼剖析指令碼載入原理》

《requireJS 原理分析》

68. JS 模組載入器的輪子怎麼造,也就是如何實現一個模組載入器?

詳細資料可以參考:

《JS 模組載入器載入原理是怎麼樣的?》

69. ECMAScript6 怎麼寫 class,為什麼會出現 class 這種東西?

在我看來 ES6 新新增的 class 只是為了補充 js 中缺少的一些面嚮物件語言的特性,但本質上來說它只是一種語法糖,不是一個新的東西,其背後還是原型繼承的思想。通過加入 class 可以有利於我們更好的組織程式碼。

在 class 中新增的方法,其實是新增在類的原型上的。

詳細資料可以參考:

《ECMAScript 6 實現了 class,對 JavaScript 前端開發有什麼意義?》

《Class 的基本語法》

70. documen.write 和 innerHTML 的區別?

document.write 的內容會代替整個文件內容,會重寫整個頁面。

innerHTML 的內容只是替代指定元素的內容,只會重寫頁面中的部分內容。

詳細資料可以參考:

《簡述 document.write 和 innerHTML 的區別。》

71. DOM 操作——怎樣新增、移除、移動、複製、建立和查詢節點?

(1)建立新節點

createDocumentFragment(node);
createElement(node);
createTextNode(text);

(2)新增、移除、替換、插入

appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)

(3)查詢

getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();

(4)屬性操作

getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);

詳細資料可以參考:

《DOM 概述》

《原生 JavaScript 的 DOM 操作彙總》

《原生 JS 中 DOM 節點相關 API 合集》

72. innerHTML 與 outerHTML 的區別?

對於這樣一個 HTML 元素:<div>content<br/></div>。

innerHTML:內部 HTML,content<br/>;
outerHTML:外部 HTML,<div>content<br/></div>;
innerText:內部文字,content ;
outerText:內部文字,content ;

73. .call() 和 .apply() 的區別?

它們的作用一模一樣,區別僅在於傳入引數的形式的不同。

apply 接受兩個引數,第一個引數指定了函式體內 this 物件的指向,第二個引數為一個帶下標的集合,這個集合可以為陣列,也可以為類陣列,apply 方法把這個集合中的元素作為引數傳遞給被呼叫的函式。

call 傳入的引數數量不固定,跟 apply 相同的是,第一個引數也是代表函式體內的 this 指向,從第二個引數開始往後,每個引數被依次傳入函式。

詳細資料可以參考:

《apply、call 的區別和用途》

74. JavaScript 類陣列物件的定義?

一個擁有 length 屬性和若干索引屬性的物件就可以被稱為類陣列物件,類陣列物件和陣列類似,但是不能呼叫陣列的方法。

常見的類陣列物件有 arguments 和 DOM 方法的返回結果,還有一個函式也可以被看作是類陣列物件,因為它含有 length
屬性值,代表可接收的引數個數。

常見的類陣列轉換為陣列的方法有這樣幾種:

(1)通過 call 呼叫陣列的 slice 方法來實現轉換

Array.prototype.slice.call(arrayLike);

(2)通過 call 呼叫陣列的 splice 方法來實現轉換

Array.prototype.splice.call(arrayLike, 0);

(3)通過 apply 呼叫陣列的 concat 方法來實現轉換

Array.prototype.concat.apply([], arrayLike);

(4)通過 Array.from 方法來實現轉換

Array.from(arrayLike);

詳細的資料可以參考:

《JavaScript 深入之類陣列物件與 arguments》

《javascript 類陣列》

《深入理解 JavaScript 類陣列》

75. 陣列和物件有哪些原生方法,列舉一下?

陣列和字串的轉換方法:toString()、toLocalString()、join() 其中 join() 方法可以指定轉換為字串時的分隔符。

陣列尾部操作的方法 pop() 和 push(),push 方法可以傳入多個引數。

陣列首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以傳入一個函式來進行比較,傳入前後兩個值,如果返回值為正數,則交換兩個引數的位置。

陣列連線的方法 concat() ,返回的是拼接好的陣列,不影響原陣列。

陣列擷取辦法 slice(),用於擷取陣列中的一部分返回,不影響原陣列。

陣列插入方法 splice(),影響原陣列查詢特定項的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法

陣列歸併方法 reduce() 和 reduceRight() 方法

詳細資料可以參考:

《JavaScript 深入理解之 Array 型別詳解》

76. 陣列的 fill 方法?

fill() 方法用一個固定值填充一個數組中從起始索引到終止索引內的全部元素。不包括終止索引。
fill 方法接受三個引數 value,start 以及 end,start 和 end 引數是可選的,其預設值分別為 0 和 this 物件的 length 屬性值。

詳細資料可以參考:

《Array.prototype.fill()》

77. [,,,] 的長度?

尾後逗號 (有時叫做“終止逗號”)在向 JavaScript 程式碼新增元素、引數、屬性時十分有用。如果你想要新增新的屬性,並且上一行已經使用了尾後逗號,你可以僅僅新增新的一行,而不需要修改上一行。這使得版本控制更加清晰,以及程式碼維護麻煩更少。

JavaScript 一開始就支援陣列字面值中的尾後逗號,隨後向物件字面值(ECMAScript 5)中添加了尾後逗號。最近(ECMAS
cript 2017),又將其新增到函式引數中。但是 JSON 不支援尾後逗號。 如果使用了多於一個尾後逗號,會產生間隙。 帶有間隙的陣列叫做稀疏陣列(密緻陣列沒有間隙)。稀疏陣列的長度為逗號的數
量。

詳細資料可以參考:

《尾後逗號》

78. JavaScript 中的作用域與變數宣告提升?

變數提升的表現是,無論我們在函式中何處位置宣告的變數,好像都被提升到了函式的首部,我們可以在變數宣告前訪問到而不會報錯。

造成變數宣告提升的本質原因是 js 引擎在程式碼執行前有一個解析的過程,建立了執行上下文,初始化了一些程式碼執行時需要用到的物件。當我們訪問一個變數時,我們會到當前執行上下文中的作用域鏈中去查詢,而作用域鏈的首端指向的是當前執行上下文的變數物件,這個變數物件是執行上下文的一個屬性,它包含了函式的形參、所有的函式和變數宣告,這個物件的是在程式碼解析的時候建立的。這就是會出現變數宣告提升的根本原因。

詳細資料可以參考:

《JavaScript 深入理解之變數物件》

79. 如何編寫高效能的 Javascript ?

  • 1.使用位運算代替一些簡單的四則運算。
  • 2.避免使用過深的巢狀迴圈。
  • 3.不要使用未定義的變數。
  • 4.當需要多次訪問陣列長度時,可以用變數儲存起來,避免每次都會去進行屬性查詢。

詳細資料可以參考:

《如何編寫高效能的 Javascript?》

80. 簡單介紹一下 V8 引擎的垃圾回收機制

v8 的垃圾回收機制基於分代回收機制,這個機制又基於世代假說,這個假說有兩個特點,一是新生的物件容易早死,另一個是不死的物件會活得更久。基於這個假說,v8 引擎將記憶體分為了新生代和老生代。

新建立的物件或者只經歷過一次的垃圾回收的物件被稱為新生代。經歷過多次垃圾回收的物件被稱為老生代。

新生代被分為 From 和 To 兩個空間,To 一般是閒置的。當 From 空間滿了的時候會執行 Scavenge 演算法進行垃圾回收。當我們執行垃圾回收演算法的時候應用邏輯將會停止,等垃圾回收結束後再繼續執行。這個演算法分為三步:

(1)首先檢查 From 空間的存活物件,如果物件存活則判斷物件是否滿足晉升到老生代的條件,如果滿足條件則晉升到老生代。如果不滿足條件則移動 To 空間。

(2)如果物件不存活,則釋放物件的空間。

(3)最後將 From 空間和 To 空間角色進行交換。

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

(1)第一個是判斷是物件否已經經過一次 Scavenge 回收。若經歷過,則將物件從 From 空間複製到老生代中;若沒有經歷,則複製到 To 空間。

(2)第二個是 To 空間的記憶體使用佔比是否超過限制。當物件從 From 空間複製到 To 空間時,若 To 空間使用超過 25%,則物件直接晉升到老生代中。設定 25% 的原因主要是因為演算法結束後,兩個空間結束後會交換位置,如果 To 空間的記憶體太小,會影響後續的記憶體分配。

老生代採用了標記清除法和標記壓縮法。標記清除法首先會對記憶體中存活的物件進行標記,標記結束後清除掉那些沒有標記的物件。由於標記清除後會造成很多的記憶體碎片,不便於後面的記憶體分配。所以瞭解決記憶體碎片的問題引入了標記壓縮法。

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

詳細資料可以參考:

《深入理解 V8 的垃圾回收原理》

《JavaScript 中的垃圾回收》

81. 哪些操作會造成記憶體洩漏?

相關知識點:

  • 1.意外的全域性變數
  • 2.被遺忘的計時器或回撥函式
  • 3.脫離 DOM 的引用
  • 4.閉包

回答:

第一種情況是我們由於使用未宣告的變數,而意外的建立了一個全域性變數,而使這個變數一直留在記憶體中無法被回收。

第二種情況是我們設定了 setInterval 定時器,而忘記取消它,如果迴圈函式有對外部變數的引用的話,那麼這個變數會被一直留
在記憶體中,而無法被回收。 第三種情況是我們獲取一個 DOM 元素的引用,而後面這個元素被刪除,由於我們一直保留了對這個元素的引用,所以它也無法被回
收。 第四種情況是不合理的使用閉包,從而導致某些變數一直被留在記憶體當中。

詳細資料可以參考:

《JavaScript 記憶體洩漏教程》

《4 類 JavaScript 記憶體洩漏及如何避免》

《杜絕 js 中四種記憶體洩漏型別的發生》

《javascript 典型記憶體洩漏及 chrome 的排查方法》

82. 需求:實現一個頁面操作不會整頁重新整理的網站,並且能在瀏覽器前進、後退時正確響應。給出你的技術實現方案?

通過使用 pushState + ajax 實現瀏覽器無重新整理前進後退,當一次 ajax 呼叫成功後我們將一條 state 記錄加入到 history
物件中。一條 state 記錄包含了 url、title 和 content 屬性,在 popstate 事件中可以獲取到這個 state 物件,我們可
以使用 content 來傳遞資料。最後我們通過對 window.onpopstate 事件監聽來響應瀏覽器的前進後退操作。 使用 pushState 來實現有兩個問題,一個是開啟首頁時沒有記錄,我們可以使用 replaceState 來將首頁的記錄替換,另一個問
題是當一個頁面重新整理的時候,仍然會向伺服器端請求資料,因此如果請求的 url 需要後端的配合將其重定向到一個頁面。

詳細資料可以參考:

《pushState + ajax 實現瀏覽器無重新整理前進後退》

《Manipulating the browser history》

83. 如何判斷當前指令碼執行在瀏覽器還是 node 環境中?(阿里)

this === window ? 'browser' : 'node';

通過判斷 Global 物件是否為 window,如果不為 window,當前指令碼沒有執行在瀏覽器中。

84. 把 script 標籤放在頁面的最底部的 body 封閉之前和封閉之後有什麼區別?瀏覽器會如何解析它們?

詳細資料可以參考:

《為什麼把 script 標籤放在 body 結束標籤之後 html 結束標籤之前?》

《從 Chrome 原始碼看瀏覽器如何載入資源》

85. 移動端的點選事件的有延遲,時間是多久,為什麼會有? 怎麼解決這個延時?

移動端點選有 300ms 的延遲是因為移動端會有雙擊縮放的這個操作,因此瀏覽器在 click 之後要等待 300ms,看使用者有沒有下一次點選,來判斷這次操作是不是雙擊。

有三種辦法來解決這個問題:

  • 1.通過 meta 標籤禁用網頁的縮放。
  • 2.通過 meta 標籤將網頁的 viewport 設定為 ideal viewport。
  • 3.呼叫一些 js 庫,比如 FastClick
click 延時問題還可能引起點選穿透的問題,就是如果我們在一個元素上註冊了 touchStart 的監聽事件,這個事件會將這個元素隱藏掉,我們發現當這個元素隱藏後,觸發了這個元素下的一個元素的點選事件,這就是點選穿透。

詳細資料可以參考:

《移動端 300ms 點選延遲和點選穿透》

86. 什麼是“前端路由”?什麼時候適合使用“前端路由”?“前端路由”有哪些優點和缺點?

(1)什麼是前端路由?

前端路由就是把不同路由對應不同的內容或頁面的任務交給前端來做,之前是通過服務端根據 url 的不同返回不同的頁面實現的。

(2)什麼時候使用前端路由?

在單頁面應用,大部分頁面結構不變,只改變部分內容的使用

(3)前端路由有什麼優點和缺點?

優點:使用者體驗好,不需要每次都從伺服器全部獲取,快速展現給使用者

缺點:單頁面無法記住之前滾動的位置,無法在前進,後退的時候記住滾動的位置

前端路由一共有兩種實現方式,一種是通過 hash 的方式,一種是通過使用 pushState 的方式。

詳細資料可以參考:

《什麼是“前端路由”》

《淺談前端路由》

《前端路由是什麼東西?》

87. 如何測試前端程式碼麼? 知道 BDD, TDD, Unit Test 麼? 知道怎麼測試你的前端工程麼(mocha, sinon, jasmin, qUnit..)?

詳細資料可以參考:

《淺談前端單元測試》

88. 檢測瀏覽器版本版本有哪些方式?

檢測瀏覽器版本一共有兩種方式:

一種是檢測 window.navigator.userAgent 的值,但這種方式很不可靠,因為 userAgent 可以被改寫,並且早期的瀏覽器如 ie,會通過偽裝自己的 userAgent 的值為 Mozilla 來躲過伺服器的檢測。

第二種方式是功能檢測,根據每個瀏覽器獨有的特性來進行判斷,如 ie 下獨有的 ActiveXObject。

詳細資料可以參考:

《JavaScript 判斷瀏覽器型別》

89. 什麼是 Polyfill ?

Polyfill 指的是用於實現瀏覽器並不支援的原生 API 的程式碼。

比如說 querySelectorAll 是很多現代瀏覽器都支援的原生 Web API,但是有些古老的瀏覽器並不支援,那麼假設有人寫了一段程式碼來實現這個功能使這些瀏覽器也支援了這個功能,那麼這就可以成為一個 Polyfill。

一個 shim 是一個庫,有自己的 API,而不是單純實現原生不支援的 API。

詳細資料可以參考:

《Web 開發中的“黑話”》

《Polyfill 為何物》

90. 使用 JS 實現獲取副檔名?

// String.lastIndexOf() 方法返回指定值(本例中的'.')在呼叫該方法的字串中最後出現的位置,如果沒找到則返回 -1。

// 對於 'filename' 和 '.hiddenfile' ,lastIndexOf 的返回值分別為 0 和 -1 無符號右移操作符(>>>) 將 -1 轉換為 4294967295 ,將 -2 轉換為 4294967294 ,這個方法可以保證邊緣情況時檔名不變。

// String.prototype.slice() 從上面計算的索引處提取檔案的副檔名。如果索引比檔名的長度大,結果為""。
function getFileExtension(filename) {
return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2);
}

詳細資料可以參考:

《如何更有效的獲取副檔名》

91. 介紹一下 js 的節流與防抖?

相關知識點:

// 函式防抖: 在事件被觸發 n 秒後再執行回撥,如果在這 n 秒內事件又被觸發,則重新計時。

// 函式節流: 規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回撥函式執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。

// 函式防抖的實現
function debounce(fn, wait) {
var timer = null; return function() {
var context = this,
args = arguments; // 如果此時存在定時器的話,則取消之前的定時器重新記時
if (timer) {
clearTimeout(timer);
timer = null;
} // 設定定時器,使事件間隔指定事件後執行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
} // 函式節流的實現;
function throttle(fn, delay) {
var preTime = Date.now(); return function() {
var context = this,
args = arguments,
nowTime = Date.now(); // 如果兩次時間間隔超過了指定時間,則執行函式。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}

回答:

函式防抖是指在事件被觸發 n 秒後再執行回撥,如果在這 n 秒內事件又被觸發,則重新計時。這可以使用在一些點選請求的事件上,避免因為使用者的多次點擊向後端傳送多次請求。

函式節流是指規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回撥函式執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。節流可以使用在 scroll 函式的事件監聽上,通過事件節流來降低事件呼叫的頻率。

詳細資料可以參考:

《輕鬆理解 JS 函式節流和函式防抖》

《JavaScript 事件節流和事件防抖》

《JS 的防抖與節流》

92. Object.is() 與原來的比較操作符 “=”、“” 的區別?

相關知識點:

兩等號判等,會在比較時進行型別轉換。
三等號判等(判斷嚴格),比較時不進行隱式型別轉換,(型別不同則會返回false)。 Object.is 在三等號判等的基礎上特別處理了 NaN 、-0 和 +0 ,保證 -0 和 +0 不再相同,但 Object.is(NaN, NaN) 會返回 true. Object.is 應被認為有其特殊的用途,而不能用它認為它比其它的相等對比更寬鬆或嚴格。

回答:

使用雙等號進行相等判斷時,如果兩邊的型別不一致,則會進行強制型別轉化後再進行比較。

使用三等號進行相等判斷時,如果兩邊的型別不一致時,不會做強制型別準換,直接返回 false。

使用 Object.is 來進行相等判斷時,一般情況下和三等號的判斷相同,它處理了一些特殊的情況,比如 -0 和 +0 不再相等,兩個 NaN 認定為是相等的。

93. escape,encodeURI,encodeURIComponent 有什麼區別?

相關知識點:

escape 和 encodeURI 都屬於 Percent-encoding,基本功能都是把 URI 非法字元轉化成合法字元,轉化後形式類似「%*」。
它們的根本區別在於,escape 在處理 0xff 之外字元的時候,是直接使用字元的 unicode 在前面加上一個「%u」,而 encode URI 則是先進行 UTF-8,再在 UTF-8 的每個位元組碼前加上一個「%」;在處理 0xff 以內字元時,編碼方式是一樣的(都是「%XX」,XX 為字元的 16 進位制 unicode,同時也是字元的 UTF-8),只是範圍(即哪些字元編碼哪些字元不編碼)不一樣。

回答:

encodeURI 是對整個 URI 進行轉義,將 URI 中的非法字元轉換為合法字元,所以對於一些在 URI 中有特殊意義的字元不會進行轉義。

encodeURIComponent 是對 URI 的組成部分進行轉義,所以一些特殊字元也會得到轉義。

escape 和 encodeURI 的作用相同,不過它們對於 unicode 編碼為 0xff 之外字元的時候會有區別,escape 是直接在字元的 unicode 編碼前加上 %u,而 encodeURI 首先會將字元轉換為 UTF-8 的格式,再在每個位元組前加上 %。

詳細資料可以參考:

《escape,encodeURI,encodeURIComponent 有什麼區別?》

94. Unicode 和 UTF-8 之間的關係?

Unicode 是一種字元集合,現在可容納 100 多萬個字元。每個字元對應一個不同的 Unicode 編碼,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼在計算機中如何編碼傳輸。

UTF-8 是一種對 Unicode 的編碼方式,它是一種變長的編碼方式,可以用 1~4 個位元組來表示一個字元。

詳細資料可以參考:

《字元編碼詳解》

《字元編碼筆記:ASCII,Unicode 和 UTF-8》

95. js 的事件迴圈是什麼?

相關知識點:

事件佇列是一個儲存著待執行任務的佇列,其中的任務嚴格按照時間先後順序執行,排在隊頭的任務將會率先執行,而排在隊尾的任務會最後執行。事件佇列每次僅執行一個任務,在該任務執行完畢之後,再執行下一個任務。執行棧則是一個類似於函式呼叫棧的執行容器,當執行棧為空時,JS 引擎便檢查事件佇列,如果不為空的話,事件佇列便將第一個任務壓入執行棧中執行。

回答:

因為 js 是單執行緒執行的,在程式碼執行的時候,通過將不同函式的執行上下文壓入執行棧中來保證程式碼的有序執行。在執行同步程式碼的時候,如果遇到了非同步事件,js 引擎並不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務。當非同步事件執行完畢後,再將非同步事件對應的回撥加入到與當前執行棧中不同的另一個任務佇列中等待執行。任務佇列可以分為巨集任務對列和微任務對列,噹噹前執行棧中的事件執行完畢後,js 引擎首先會判斷微任務對列中是否有任務可以執行,如果有就將微任務隊首的事件壓入棧中執行。當微任務對列中的任務都執行完成後再去判斷巨集任務對列中的任務。

微任務包括了 promise 的回撥、node 中的 process.nextTick 、對 Dom 變化監聽的 MutationObserver。

巨集任務包括了 script 指令碼的執行、setTimeout ,setInterval ,setImmediate 一類的定時事件,還有如 I/O 操作、UI 渲
染等。

詳細資料可以參考:

《瀏覽器事件迴圈機制(event loop)》

《詳解 JavaScript 中的 Event Loop(事件迴圈)機制》

《什麼是 Event Loop?》

《這一次,徹底弄懂 JavaScript 執行機制》

96. js 中的深淺拷貝實現?

相關資料:

// 淺拷貝的實現;

function shallowCopy(object) {
// 只拷貝物件
if (!object || typeof object !== "object") return; // 根據 object 的型別判斷是新建一個數組還是物件
let newObject = Array.isArray(object) ? [] : {}; // 遍歷 object,並且判斷是 object 的屬性才拷貝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
} return newObject;
} // 深拷貝的實現; function deepCopy(object) {
if (!object || typeof object !== "object") return; let newObject = Array.isArray(object) ? [] : {}; for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] =
typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
} return newObject;
}

回答:

淺拷貝指的是將一個物件的屬性值複製到另一個物件,如果有的屬性的值為引用型別的話,那麼會將這個引用的地址複製給物件,因此兩個物件會有同一個引用型別的引用。淺拷貝可以使用  Object.assign 和展開運算子來實現。

深拷貝相對淺拷貝而言,如果遇到屬性值為引用型別的時候,它新建一個引用型別並將對應的值複製給它,因此物件獲得的一個新的引用型別而不是一個原有型別的引用。深拷貝對於一些物件可以使用 JSON 的兩個函式來實現,但是由於 JSON 的物件格式比 js 的物件格式更加嚴格,所以如果屬性值裡邊出現函式或者 Symbol 型別的值時,會轉換失敗。

詳細資料可以參考:

《JavaScript 專題之深淺拷貝》

《前端面試之道》

97. 手寫 call、apply 及 bind 函式

相關資料:

// call函式實現
Function.prototype.myCall = function(context) {
// 判斷呼叫物件
if (typeof this !== "function") {
console.error("type error");
} // 獲取引數
let args = [...arguments].slice(1),
result = null; // 判斷 context 是否傳入,如果未傳入則設定為 window
context = context || window; // 將呼叫函式設為物件的方法
context.fn = this; // 呼叫函式
result = context.fn(...args); // 將屬性刪除
delete context.fn; return result;
}; // apply 函式實現 Function.prototype.myApply = function(context) {
// 判斷呼叫物件是否為函式
if (typeof this !== "function") {
throw new TypeError("Error");
} let result = null; // 判斷 context 是否存在,如果未傳入則為 window
context = context || window; // 將函式設為物件的方法
context.fn = this; // 呼叫方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
} // 將屬性刪除
delete context.fn; return result;
}; // bind 函式實現
Function.prototype.myBind = function(context) {
// 判斷呼叫物件是否為函式
if (typeof this !== "function") {
throw new TypeError("Error");
} // 獲取引數
var args = [...arguments].slice(1),
fn = this; return function Fn() {
// 根據呼叫方式,傳入不同繫結值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};

回答:

call 函式的實現步驟:

  • 1.判斷呼叫物件是否為函式,即使我們是定義在函式的原型上的,但是可能出現使用 call 等方式呼叫的情況。
  • 2.判斷傳入上下文物件是否存在,如果不存在,則設定為 window 。
  • 3.處理傳入的引數,擷取第一個引數後的所有引數。
  • 4.將函式作為上下文物件的一個屬性。
  • 5.使用上下文物件來呼叫這個方法,並儲存返回結果。
  • 6.刪除剛才新增的屬性。
  • 7.返回結果。

apply 函式的實現步驟:

  • 1.判斷呼叫物件是否為函式,即使我們是定義在函式的原型上的,但是可能出現使用 call 等方式呼叫的情況。
  • 2.判斷傳入上下文物件是否存在,如果不存在,則設定為 window 。
  • 3.將函式作為上下文物件的一個屬性。
  • 4.判斷引數值是否傳入
  • 4.使用上下文物件來呼叫這個方法,並儲存返回結果。
  • 5.刪除剛才新增的屬性
  • 6.返回結果

bind 函式的實現步驟:

  • 1.判斷呼叫物件是否為函式,即使我們是定義在函式的原型上的,但是可能出現使用 call 等方式呼叫的情況。
  • 2.儲存當前函式的引用,獲取其餘傳入引數值。
  • 3.建立一個函式返回
  • 4.函式內部使用 apply 來繫結函式呼叫,需要判斷函式作為建構函式的情況,這個時候需要傳入當前函式的 this 給 apply 呼叫,其餘情況都傳入指定的上下文物件。

詳細資料可以參考:

《手寫 call、apply 及 bind 函式》

《JavaScript 深入之 call 和 apply 的模擬實現》

98. 函式柯里化的實現

// 函式柯里化指的是一種將使用多個引數的一個函式轉換成一系列使用一個引數的函式的技術。

function curry(fn, args) {
// 獲取函式需要的引數長度
let length = fn.length; args = args || []; return function() {
let subArgs = args.slice(0); // 拼接得到現有的所有引數
for (let i = 0; i < arguments.length; i++) {
subArgs.push(arguments[i]);
} // 判斷引數的長度是否已經滿足函式所需引數的長度
if (subArgs.length >= length) {
// 如果滿足,執行函式
return fn.apply(this, subArgs);
} else {
// 如果不滿足,遞迴返回科裡化的函式,等待引數的傳入
return curry.call(this, fn, subArgs);
}
};
} // es6 實現
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

詳細資料可以參考:

《JavaScript 專題之函式柯里化》

99. 為什麼 0.1 + 0.2 != 0.3?如何解決這個問題?

當計算機計算 0.1+0.2 的時候,實際上計算的是這兩個數字在計算機裡所儲存的二進位制,0.1 和 0.2 在轉換為二進位制表示的時候會出現位數無限迴圈的情況。js 中是以 64 位雙精度格式來儲存數字的,只有 53 位的有效數字,超過這個長度的位數會被擷取掉這樣就造成了精度丟失的問題。這是第一個會造成精度丟失的地方。在對兩個以 64 位雙精度格式的資料進行計算的時候,首先會進行對階的處理,對階指的是將階碼對齊,也就是將小數點的位置對齊後,再進行計算,一般是小階向大階對齊,因此小階的數在對齊的過程中,有效數字會向右移動,移動後超過有效位數的位會被擷取掉,這是第二個可能會出現精度丟失的地方。當兩個資料階碼對齊後,進行相加運算後,得到的結果可能會超過 53 位有效數字,因此超過的位數也會被擷取掉,這是可能發生精度丟失的第三個地方。

對於這樣的情況,我們可以將其轉換為整數後再進行運算,運算後再轉換為對應的小數,以這種方式來解決這個問題。

我們還可以將兩個數相加的結果和右邊相減,如果相減的結果小於一個極小數,那麼我們就可以認定結果是相等的,這個極小數可以
使用 es6 的 Number.EPSILON

詳細資料可以參考:

《十進位制的 0.1 為什麼不能用二進位制很好的表示?》

《十進位制浮點數轉成二進位制》

《浮點數的二進位制表示》

《js 浮點數儲存精度丟失原理》

《浮點數精度之謎》

《JavaScript 浮點數陷阱及解法》

《0.1+0.2 !== 0.3?》

《JavaScript 中奇特的~運算子》

100. 原碼、反碼和補碼的介紹

原碼是計算機中對數字的二進位制的定點表示方法,最高位表示符號位,其餘位表示數值位。優點是易於分辨,缺點是不能夠直接參與運算。

正數的反碼和其原碼一樣;負數的反碼,符號位為1,數值部分按原碼取反。
如 [+7]原 = 00000111,[+7]反 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000。 正數的補碼和其原碼一樣;負數的補碼為其反碼加1。 例如 [+7]原 = 00000111,[+7]反 = 00000111,[+7]補 = 00000111;
[-7]原 = 10000111,[-7]反 = 11111000,[-7]補 = 11111001 之所以在計算機中使用補碼來表示負數的原因是,這樣可以將加法運算擴充套件到所有的數值計算上,因此在數位電路中我們只需要考慮加法器的設計就行了,而不用再為減法設定新的數位電路。

詳細資料可以參考:

《關於 2 的補碼》

101. toPrecision 和 toFixed 和 Math.round 的區別?

toPrecision 用於處理精度,精度是從左至右第一個不為 0 的數開始數起。
toFixed 是對小數點後指定位數取整,從小數點開始數起。
Math.round 是將一個數字四捨五入到一個整數。

102. 什麼是 XSS 攻擊?如何防範 XSS 攻擊?

XSS 攻擊指的是跨站指令碼攻擊,是一種程式碼注入攻擊。攻擊者通過在網站注入惡意指令碼,使之在使用者的瀏覽器上執行,從而盜取使用者的資訊如 cookie 等。

XSS 的本質是因為網站沒有對惡意程式碼進行過濾,與正常的程式碼混合在一起了,瀏覽器沒有辦法分辨哪些指令碼是可信的,從而導致了惡意程式碼的執行。

XSS 一般分為儲存型、反射型和 DOM 型。

儲存型指的是惡意程式碼提交到了網站的資料庫中,當用戶請求資料的時候,伺服器將其拼接為 HTML 後返回給了使用者,從而導致了惡意程式碼的執行。

反射型指的是攻擊者構建了特殊的 URL,當伺服器接收到請求後,從 URL 中獲取資料,拼接到 HTML 後返回,從而導致了惡意程式碼的執行。

DOM 型指的是攻擊者構建了特殊的 URL,使用者開啟網站後,js 指令碼從 URL 中獲取資料,從而導致了惡意程式碼的執行。

XSS 攻擊的預防可以從兩個方面入手,一個是惡意程式碼提交的時候,一個是瀏覽器執行惡意程式碼的時候。

對於第一個方面,如果我們對存入資料庫的資料都進行的轉義處理,但是一個數據可能在多個地方使用,有的地方可能不需要轉義,由於我們沒有辦法判斷資料最後的使用場景,所以直接在輸入端進行惡意程式碼的處理,其實是不太可靠的。

因此我們可以從瀏覽器的執行來進行預防,一種是使用純前端的方式,不用伺服器端拼接後返回。另一種是對需要插入到 HTML 中的程式碼做好充分的轉義。對於 DOM 型的攻擊,主要是前端指令碼的不可靠而造成的,我們對於資料獲取渲染和字串拼接的時候應該對可能出現的惡意程式碼情況進行判斷。

還有一些方式,比如使用 CSP ,CSP 的本質是建立一個白名單,告訴瀏覽器哪些外部資源可以載入和執行,從而防止惡意程式碼的注入攻擊。

還可以對一些敏感資訊進行保護,比如 cookie 使用 http-only ,使得指令碼無法獲取。也可以使用驗證碼,避免指令碼偽裝成使用者執行一些操作。

詳細資料可以參考:

《前端安全系列(一):如何防止 XSS 攻擊?》

103. 什麼是 CSP?

CSP 指的是內容安全策略,它的本質是建立一個白名單,告訴瀏覽器哪些外部資源可以載入和執行。我們只需要配置規則,如何攔截由瀏覽器自己來實現。

通常有兩種方式來開啟 CSP,一種是設定 HTTP 首部中的 Content-Security-Policy,一種是設定 meta 標籤的方式 <meta
http-equiv="Content-Security-Policy">

詳細資料可以參考:

《內容安全策略(CSP)》

《前端面試之道》

104. 什麼是 CSRF 攻擊?如何防範 CSRF 攻擊?

CSRF 攻擊指的是跨站請求偽造攻擊,攻擊者誘導使用者進入一個第三方網站,然後該網站向被攻擊網站傳送跨站請求。如果使用者在被
攻擊網站中儲存了登入狀態,那麼攻擊者就可以利用這個登入狀態,繞過後臺的使用者驗證,冒充使用者向伺服器執行一些操作。 CSRF 攻擊的本質是利用了 cookie 會在同源請求中攜帶傳送給伺服器的特點,以此來實現使用者的冒充。 一般的 CSRF 攻擊型別有三種: 第一種是 GET 型別的 CSRF 攻擊,比如在網站中的一個 img 標籤裡構建一個請求,當用戶開啟這個網站的時候就會自動發起提
交。 第二種是 POST 型別的 CSRF 攻擊,比如說構建一個表單,然後隱藏它,當用戶進入頁面時,自動提交這個表單。 第三種是連結型別的 CSRF 攻擊,比如說在 a 標籤的 href 屬性裡構建一個請求,然後誘導使用者去點選。 CSRF 可以用下面幾種方法來防護: 第一種是同源檢測的方法,伺服器根據 http 請求頭中 origin 或者 referer 資訊來判斷請求是否為允許訪問的站點,從而對請求進行過濾。當 origin 或者 referer 資訊都不存在的時候,直接阻止。這種方式的缺點是有些情況下 referer 可以被偽造。還有就是我們這種方法同時把搜尋引擎的連結也給遮蔽了,所以一般網站會允許搜尋引擎的頁面請求,但是相應的頁面請求這種請求方式也可能被攻擊者給利用。 第二種方法是使用 CSRF Token 來進行驗證,伺服器向用戶返回一個隨機數 Token ,當網站再次發起請求時,在請求引數中加入伺服器端返回的 token ,然後伺服器對這個 token 進行驗證。這種方法解決了使用 cookie 單一驗證方式時,可能會被冒用的問題,但是這種方法存在一個缺點就是,我們需要給網站中的所有請求都新增上這個 token,操作比較繁瑣。還有一個問題是一般不會只有一臺網站伺服器,如果我們的請求經過負載平衡轉移到了其他的伺服器,但是這個伺服器的 session 中沒有保留這個 token 的話,就沒有辦法驗證了。這種情況我們可以通過改變 token 的構建方式來解決。 第三種方式使用雙重 Cookie 驗證的辦法,伺服器在使用者訪問網站頁面時,向請求域名注入一個Cookie,內容為隨機字串,然後當用戶再次向伺服器傳送請求的時候,從 cookie 中取出這個字串,新增到 URL 引數中,然後伺服器通過對 cookie 中的資料和引數中的資料進行比較,來進行驗證。使用這種方式是利用了攻擊者只能利用 cookie,但是不能訪問獲取 cookie 的特點。並且這種方法比 CSRF Token 的方法更加方便,並且不涉及到分散式訪問的問題。這種方法的缺點是如果網站存在 XSS 漏洞的,那麼這種方式會失效。同時這種方式不能做到子域名的隔離。 第四種方式是使用在設定 cookie 屬性的時候設定 Samesite ,限制 cookie 不能作為被第三方使用,從而可以避免被攻擊者利用。Samesite 一共有兩種模式,一種是嚴格模式,在嚴格模式下 cookie 在任何情況下都不可能作為第三方 Cookie 使用,在寬鬆模式下,cookie 可以被請求是 GET 請求,且會發生頁面跳轉的請求所使用。

詳細資料可以參考:

《前端安全系列之二:如何防止 CSRF 攻擊?》

《[ HTTP 趣談] origin, referer 和 host 區別》

105. 什麼是 Samesite Cookie 屬性?

Samesite Cookie 表示同站 cookie,避免 cookie 被第三方所利用。

將 Samesite 設為 strict ,這種稱為嚴格模式,表示這個 cookie 在任何情況下都不可能作為第三方 cookie。

將 Samesite 設為 Lax ,這種模式稱為寬鬆模式,如果這個請求是個 GET 請求,並且這個請求改變了當前頁面或者打開了新的頁面,那麼這個 cookie 可以作為第三方 cookie,其餘情況下都不能作為第三方 cookie。

使用這種方法的缺點是,因為它不支援子域,所以子域沒有辦法與主域共享登入資訊,每次轉入子域的網站,都回重新登入。還有一個問題就是它的相容性不夠好。

106. 什麼是點選劫持?如何防範點選劫持?

點選劫持是一種視覺欺騙的攻擊手段,攻擊者將需要攻擊的網站通過 iframe 巢狀的方式嵌入自己的網頁中,並將 iframe 設定為透明,在頁面中透出一個按鈕誘導使用者點選。

我們可以在 http 相應頭中設定 X-FRAME-OPTIONS 來防禦用 iframe 巢狀的點選劫持攻擊。通過不同的值,可以規定頁面在特
定的一些情況才能作為 iframe 來使用。

詳細資料可以參考:

《web 安全之--點選劫持攻擊與防禦技術簡介》

107. SQL 注入攻擊?

SQL 注入攻擊指的是攻擊者在 HTTP 請求中注入惡意的 SQL 程式碼,伺服器使用引數構建資料庫 SQL 命令時,惡意 SQL 被一起構
造,破壞原有 SQL 結構,並在資料庫中執行,達到編寫程式時意料之外結果的攻擊行為。

詳細資料可以參考:

《Web 安全漏洞之 SQL 注入》

《如何防範常見的 Web 攻擊》

108. 什麼是 MVVM?比之 MVC 有什麼區別?什麼又是 MVP ?

MVC、MVP 和 MVVM 是三種常見的軟體架構設計模式,主要通過分離關注點的方式來組織程式碼結構,優化我們的開發效率。

比如說我們實驗室在以前專案開發的時候,使用單頁應用時,往往一個路由頁面對應了一個指令碼檔案,所有的頁面邏輯都在一個指令碼檔案裡。頁面的渲染、資料的獲取,對使用者事件的響應所有的應用邏輯都混合在一起,這樣在開發簡單專案時,可能看不出什麼問題,當時一旦專案變得複雜,那麼整個檔案就會變得冗長,混亂,這樣對我們的專案開發和後期的專案維護是非常不利的。

MVC 通過分離 Model、View 和 Controller 的方式來組織程式碼結構。其中 View 負責頁面的顯示邏輯,Model 負責儲存頁面的業務資料,以及對相應資料的操作。並且 View 和 Model 應用了觀察者模式,當 Model 層發生改變的時候它會通知有關 View 層更新頁面。Controller 層是 View 層和 Model 層的紐帶,它主要負責使用者與應用的響應操作,當用戶與頁面產生互動的時候,Co
ntroller 中的事件觸發器就開始工作了,通過呼叫 Model 層,來完成對 Model 的修改,然後 Model 層再去通知 View 層更新。 MVP 模式與 MVC 唯一不同的在於 Presenter 和 Controller。在 MVC 模式中我們使用觀察者模式,來實現當 Model 層資料發生變化的時候,通知 View 層的更新。這樣 View 層和 Model 層耦合在一起,當專案邏輯變得複雜的時候,可能會造成程式碼的混亂,並且可能會對程式碼的複用性造成一些問題。MVP 的模式通過使用 Presenter 來實現對 View 層和 Model 層的解耦。MVC 中的
Controller 只知道 Model 的介面,因此它沒有辦法控制 View 層的更新,MVP 模式中,View 層的介面暴露給了 Presenter 因此我們可以在 Presenter 中將 Model 的變化和 View 的變化繫結在一起,以此來實現 View 和 Model 的同步更新。這樣就實現了對 View 和 Model 的解耦,Presenter 還包含了其他的響應邏輯。 MVVM 模式中的 VM,指的是 ViewModel,它和 MVP 的思想其實是相同的,不過它通過雙向的資料繫結,將 View 和 Model 的同步更新給自動化了。當 Model 發生變化的時候,ViewModel 就會自動更新;ViewModel 變化了,View 也會更新。這樣就將 Presenter 中的工作給自動化了。我瞭解過一點雙向資料繫結的原理,比如 vue 是通過使用資料劫持和釋出訂閱者模式來實現的這一功
能。

詳細資料可以參考:

《淺析前端開發中的 MVC/MVP/MVVM 模式》

《MVC,MVP 和 MVVM 的圖示》

《MVVM》

《一篇文章瞭解架構模式:MVC/MVP/MVVM》

109. vue 雙向資料繫結原理?

vue 通過使用雙向資料繫結,來實現了 View 和 Model 的同步更新。vue 的雙向資料繫結主要是通過使用資料劫持和釋出訂閱者模式來實現的。

首先我們通過 Object.defineProperty() 方法來對 Model 資料各個屬性新增訪問器屬性,以此來實現資料的劫持,因此當 Model 中的資料發生變化的時候,我們可以通過配置的 setter 和 getter 方法來實現對 View 層資料更新的通知。

資料在 html 模板中一共有兩種繫結情況,一種是使用 v-model 來對 value 值進行繫結,一種是作為文字繫結,在對模板引擎進行解析的過程中。

如果遇到元素節點,並且屬性值包含 v-model 的話,我們就從 Model 中去獲取 v-model 所對應的屬性的值,並賦值給元素的 value 值。然後給這個元素設定一個監聽事件,當 View 中元素的資料發生變化的時候觸發該事件,通知 Model 中的對應的屬性的值進行更新。

如果遇到了繫結的文字節點,我們使用 Model 中對應的屬性的值來替換這個文字。對於文字節點的更新,我們使用了釋出訂閱者模式,屬性作為一個主題,我們為這個節點設定一個訂閱者物件,將這個訂閱者物件加入這個屬性主題的訂閱者列表中。當 Model 層資料發生改變的時候,Model 作為釋出者向主題發出通知,主題收到通知再向它的所有訂閱者推送,訂閱者收到通知後更改自己的數
據。

詳細資料可以參考:

《Vue.js 雙向繫結的實現原理》

110. Object.defineProperty 介紹?

Object.defineProperty 函式一共有三個引數,第一個引數是需要定義屬性的物件,第二個引數是需要定義的屬性,第三個是該屬性描述符。

一個屬性的描述符有四個屬性,分別是 value 屬性的值,writable 屬性是否可寫,enumerable 屬性是否可列舉,configurable 屬性是否可配置修改。

詳細資料可以參考:

《Object.defineProperty()》

111. 使用 Object.defineProperty() 來進行資料劫持有什麼缺點?

有一些對屬性的操作,使用這種方法無法攔截,比如說通過下標方式修改陣列資料或者給物件新增屬性,vue 內部通過重寫函式解決了這個問題。在 Vue3.0 中已經不使用這種方式了,而是通過使用 Proxy 對物件進行代理,從而實現資料劫持。使用 Proxy 的好處是它可以完美的監聽到任何方式的資料改變,唯一的缺點是相容性的問題,因為這是 ES6 的語法。

112. 什麼是 Virtual DOM?為什麼 Virtual DOM 比原生 DOM 快?

我對 Virtual DOM 的理解是,

首先對我們將要插入到文件中的 DOM 樹結構進行分析,使用 js 物件將其表示出來,比如一個元素物件,包含 TagName、props 和 Children 這些屬性。然後我們將這個 js 物件樹給儲存下來,最後再將 DOM 片段插入到文件中。

當頁面的狀態發生改變,我們需要對頁面的 DOM 的結構進行調整的時候,我們首先根據變更的狀態,重新構建起一棵物件樹,然後將這棵新的物件樹和舊的物件樹進行比較,記錄下兩棵樹的的差異。

最後將記錄的有差異的地方應用到真正的 DOM 樹中去,這樣檢視就更新了。

我認為 Virtual DOM 這種方法對於我們需要有大量的 DOM 操作的時候,能夠很好的提高我們的操作效率,通過在操作前確定需要做的最小修改,儘可能的減少 DOM 操作帶來的重流和重繪的影響。其實 Virtual DOM 並不一定比我們真實的操作 DOM 要快,這種方法的目的是為了提高我們開發時的可維護性,在任意的情況下,都能保證一個儘量小的效能消耗去進行操作。

詳細資料可以參考:

《Virtual DOM》

《理解 Virtual DOM》

《深度剖析:如何實現一個 Virtual DOM 演算法》

《網上都說操作真實 DOM 慢,但測試結果卻比 React 更快,為什麼?》

113. 如何比較兩個 DOM 樹的差異?

兩個樹的完全 diff 演算法的時間複雜度為 O(n^3) ,但是在前端中,我們很少會跨層級的移動元素,所以我們只需要比較同一層級的元素進行比較,這樣就可以將演算法的時間複雜度降低為 O(n)。

演算法首先會對新舊兩棵樹進行一個深度優先的遍歷,這樣每個節點都會有一個序號。在深度遍歷的時候,每遍歷到一個節點,我們就將這個節點和新的樹中的節點進行比較,如果有差異,則將這個差異記錄到一個物件中。

在對列表元素進行對比的時候,由於 TagName 是重複的,所以我們不能使用這個來對比。我們需要給每一個子節點加上一個 key,列表對比的時候使用 key 來進行比較,這樣我們才能夠複用老的 DOM 樹上的節點。

114. 什麼是 requestAnimationFrame ?

詳細資料可以參考:

《你需要知道的 requestAnimationFrame》

《CSS3 動畫那麼強,requestAnimationFrame 還有毛線用?》

115. 談談你對 webpack 的看法

我當時使用 webpack 的一個最主要原因是為了簡化頁面依賴的管理,並且通過將其打包為一個檔案來降低頁面載入時請求的資源
數。 我認為 webpack 的主要原理是,它將所有的資源都看成是一個模組,並且把頁面邏輯當作一個整體,通過一個給定的入口檔案,webpack 從這個檔案開始,找到所有的依賴檔案,將各個依賴檔案模組通過 loader 和 plugins 處理後,然後打包在一起,最後輸出一個瀏覽器可識別的 JS 檔案。 Webpack 具有四個核心的概念,分別是 Entry(入口)、Output(輸出)、loader 和 Plugins(外掛)。 Entry 是 webpack 的入口起點,它指示 webpack 應該從哪個模組開始著手,來作為其構建內部依賴圖的開始。 Output 屬性告訴 webpack 在哪裡輸出它所建立的打包檔案,也可指定打包檔案的名稱,預設位置為 ./dist。 loader 可以理解為 webpack 的編譯器,它使得 webpack 可以處理一些非 JavaScript 檔案。在對 loader 進行配置的時候,test 屬性,標誌有哪些字尾的檔案應該被處理,是一個正則表示式。use 屬性,指定 test 型別的檔案應該使用哪個 loader 進行預處理。常用的 loader 有 css-loader、style-loader 等。 外掛可以用於執行範圍更廣的任務,包括打包、優化、壓縮、搭建伺服器等等,要使用一個外掛,一般是先使用 npm 包管理器進行安裝,然後在配置檔案中引入,最後將其例項化後傳遞給 plugins 陣列屬性。 使用 webpack 的確能夠提供我們對於專案的管理,但是它的缺點就是除錯和配置起來太麻煩了。但現在 webpack4.0 的免配置一定程度上解決了這個問題。但是我感覺就是對我來說,就是一個黑盒,很多時候出現了問題,沒有辦法很好的定位。

詳細資料可以參考:

《不聊 webpack 配置,來說說它的原理》

《前端工程化——構建工具選型:grunt、gulp、webpack》

《淺入淺出 webpack》

《前端構建工具發展及其比較》

116. offsetWidth/offsetHeight,clientWidth/clientHeight 與 scrollWidth/scrollHeight 的區別?

clientWidth/clientHeight 返回的是元素的內部寬度,它的值只包含 content + padding,如果有滾動條,不包含滾動條。
clientTop 返回的是上邊框的寬度。
clientLeft 返回的左邊框的寬度。 offsetWidth/offsetHeight 返回的是元素的佈局寬度,它的值包含 content + padding + border 包含了滾動條。
offsetTop 返回的是當前元素相對於其 offsetParent 元素的頂部的距離。
offsetLeft 返回的是當前元素相對於其 offsetParent 元素的左部的距離。 scrollWidth/scrollHeight 返回值包含 content + padding + 溢位內容的尺寸。
scrollTop 屬性返回的是一個元素的內容垂直滾動的畫素數。
scrollLeft 屬性返回的是元素滾動條到元素左邊的距離。

詳細資料可以參考:

《最全的獲取元素寬高及位置的方法》

《用 Javascript 獲取頁面元素的位置》

117. 談一談你理解的函數語言程式設計?

簡單說,"函數語言程式設計"是一種"程式設計正規化"(programming paradigm),也就是如何編寫程式的方法論。

它具有以下特性:閉包和高階函式、惰性計算、遞迴、函式是"第一等公民"、只用"表示式"。

詳細資料可以參考:

《函數語言程式設計初探》

118. 非同步程式設計的實現方式?

相關資料:

回撥函式
優點:簡單、容易理解
缺點:不利於維護,程式碼耦合高 事件監聽(採用時間驅動模式,取決於某個事件是否發生):
優點:容易理解,可以繫結多個事件,每個事件可以指定多個回撥函式
缺點:事件驅動型,流程不夠清晰 釋出/訂閱(觀察者模式)
類似於事件監聽,但是可以通過‘訊息中心’,瞭解現在有多少釋出者,多少訂閱者 Promise 物件
優點:可以利用 then 方法,進行鏈式寫法;可以書寫錯誤時的回撥函式;
缺點:編寫和理解,相對比較難 Generator 函式
優點:函式體內外的資料交換、錯誤處理機制
缺點:流程管理不方便 async 函式
優點:內建執行器、更好的語義、更廣的適用性、返回的是 Promise、結構清晰。
缺點:錯誤處理機制

回答:

js 中的非同步機制可以分為以下幾種:

第一種最常見的是使用回撥函式的方式,使用回撥函式的方式有一個缺點是,多個回撥函式巢狀的時候會造成回撥函式地獄,上下兩層的回撥函式間的程式碼耦合度太高,不利於程式碼的可維護。

第二種是 Promise 的方式,使用 Promise 的方式可以將巢狀的回撥函式作為鏈式呼叫。但是使用這種方法,有時會造成多個 then 的鏈式呼叫,可能會造成程式碼的語義不夠明確。

第三種是使用 generator 的方式,它可以在函式的執行過程中,將函式的執行權轉移出去,在函式外部我們還可以將執行權轉移回來。當我們遇到非同步函式執行的時候,將函式執行權轉移出去,當非同步函式執行完畢的時候我們再將執行權給轉移回來。因此我們在 generator 內部對於非同步操作的方式,可以以同步的順序來書寫。使用這種方式我們需要考慮的問題是何時將函式的控制權轉移回來,因此我們需要有一個自動執行 generator 的機制,比如說 co 模組等方式來實現 generator 的自動執行。

第四種是使用 async 函式的形式,async 函式是 generator 和 promise 實現的一個自動執行的語法糖,它內部自帶執行器,當函式內部執行到一個 await 語句的時候,如果語句返回一個 promise 物件,那麼函式將會等待 promise 物件的狀態變為 resolve 後再繼續向下執行。因此我們可以將非同步邏輯,轉化為同步的順序來書寫,並且這個函式可以自動執行。

119. Js 動畫與 CSS 動畫區別及相應實現

CSS3 的動畫的優點

在效能上會稍微好一些,瀏覽器會對 CSS3 的動畫做一些優化
程式碼相對簡單 缺點 在動畫控制上不夠靈活
相容性不好 JavaScript 的動畫正好彌補了這兩個缺點,控制能力很強,可以單幀的控制、變換,同時寫得好完全可以相容 IE6,並且功能強大。對於一些複雜控制的動畫,使用 javascript 會比較靠譜。而在實現一些小的互動動效的時候,就多考慮考慮 CSS 吧

120. get 請求傳參長度的誤區

誤區:我們經常說 get 請求引數的大小存在限制,而 post 請求的引數大小是無限制的。

實際上 HTTP 協議從未規定 GET/POST 的請求長度限制是多少。對 get 請求引數的限制是來源與瀏覽器或web 伺服器,瀏覽器或 web 伺服器限制了 url 的長度。為了明確這個概念,我們必須再次強調下面幾點:
  • 1.HTTP 協議未規定 GET 和 POST 的長度限制
  • 2.GET 的最大長度顯示是因為瀏覽器和 web 伺服器限制了 URI 的長度
  • 3.不同的瀏覽器和 WEB 伺服器,限制的最大長度不一樣
  • 4.要支援 IE,則最大長度為 2083byte,若只支援 Chrome,則最大長度 8182byte

121. URL 和 URI 的區別?

URI: Uniform Resource Identifier      指的是統一資源識別符號
URL: Uniform Resource Location 指的是統一資源定位符
URN: Universal Resource Name 指的是統一資源名稱 URI 指的是統一資源識別符號,用唯一的標識來確定一個資源,它是一種抽象的定義,也就是說,不管使用什麼方法來定義,只要能唯一的標識一個資源,就可以稱為 URI。 URL 指的是統一資源定位符,URN 指的是統一資源名稱。URL 和 URN 是 URI 的子集,URL 可以理解為使用地址來標識資源,URN 可以理解為使用名稱來標識資源。

詳細資料可以參考:

《HTTP 協議中 URI 和 URL 有什麼區別?》

《你知道 URL、URI 和 URN 三者之間的區別嗎?》

《URI、URL 和 URN 的區別》

122. get 和 post 請求在快取方面的區別

相關知識點:

get 請求類似於查詢的過程,使用者獲取資料,可以不用每次都與資料庫連線,所以可以使用快取。

post 不同,post 做的一般是修改和刪除的工作,所以必須與資料庫互動,所以不能使用快取。因此 get 請求適合於請求快取。

回答:

快取一般只適用於那些不會更新服務端資料的請求。一般 get 請求都是查詢請求,不會對伺服器資源資料造成修改,而 post 請求一般都會對伺服器資料造成修改,所以,一般會對 get 請求進行快取,很少會對 post 請求進行快取。

詳細資料可以參考:

《HTML 關於 post 和 get 的區別以及快取問題的理解》

123. 圖片的懶載入和預載入

相關知識點:

預載入:提前載入圖片,當用戶需要檢視時可直接從本地快取中渲染。

懶載入:懶載入的主要目的是作為伺服器前端的優化,減少請求數或延遲請求數。

兩種技術的本質:兩者的行為是相反的,一個是提前載入,一個是遲緩甚至不載入。 懶載入對伺服器前端有一定的緩解壓力作用,預載入則會增加伺服器前端壓力。

回答:

懶載入也叫延遲載入,指的是在長網頁中延遲載入圖片的時機,當用戶需要訪問時,再去載入,這樣可以提高網站的首屏載入速度,提升使用者的體驗,並且可以減少伺服器的壓力。它適用於圖片很多,頁面很長的電商網站的場景。懶載入的實現原理是,將頁面上的圖片的 src 屬性設定為空字串,將圖片的真實路徑儲存在一個自定義屬性中,當頁面滾動的時候,進行判斷,如果圖片進入頁面可視區域內,則從自定義屬性中取出真實路徑賦值給圖片的 src 屬性,以此來實現圖片的延遲載入。

預載入指的是將所需的資源提前請求載入到本地,這樣後面在需要用到時就直接從快取取資源。通過預載入能夠減少使用者的等待時間,提高使用者的體驗。我瞭解的預載入的最常用的方式是使用 js 中的 image 物件,通過為 image 物件來設定 scr 屬性,來實現圖片的預載入。

這兩種方式都是提高網頁效能的方式,兩者主要區別是一個是提前載入,一個是遲緩甚至不載入。懶載入對伺服器前端有一定的緩解壓力作用,預載入則會增加伺服器前端壓力。

詳細資料可以參考:

《懶載入和預載入》

《網頁圖片載入優化方案》

《基於使用者行為的圖片等資源預載入》

124. mouseover 和 mouseenter 的區別?

當滑鼠移動到元素上時就會觸發 mouseenter 事件,類似 mouseover,它們兩者之間的差別是 mouseenter 不會冒泡。

由於 mouseenter 不支援事件冒泡,導致在一個元素的子元素上進入或離開的時候會觸發其 mouseover 和 mouseout 事件,但是卻不會觸發 mouseenter 和 mouseleave 事件。

詳細資料可以參考:

《mouseenter 與 mouseover 為何這般糾纏不清?》

125. js 拖拽功能的實現

相關知識點:

首先是三個事件,分別是 mousedown,mousemove,mouseup
當滑鼠點選按下的時候,需要一個 tag 標識此時已經按下,可以執行 mousemove 裡面的具體方法。
clientX,clientY 標識的是滑鼠的座標,分別標識橫座標和縱座標,並且我們用 offsetX 和 offsetY 來表示
元素的元素的初始座標,移動的舉例應該是:
滑鼠移動時候的座標-滑鼠按下去時候的座標。
也就是說定位資訊為:
滑鼠移動時候的座標-滑鼠按下去時候的座標+元素初始情況下的 offetLeft.

回答:

一個元素的拖拽過程,我們可以分為三個步驟,第一步是滑鼠按下目標元素,第二步是滑鼠保持按下的狀態移動滑鼠,第三步是鼠
標抬起,拖拽過程結束。 這三步分別對應了三個事件,mousedown 事件,mousemove 事件和 mouseup 事件。只有在滑鼠按下的狀態移動滑鼠我們才會
執行拖拽事件,因此我們需要在 mousedown 事件中設定一個狀態來標識滑鼠已經按下,然後在 mouseup 事件中再取消這個狀
態。在 mousedown 事件中我們首先應該判斷,目標元素是否為拖拽元素,如果是拖拽元素,我們就設定狀態並且儲存這個時候鼠
標的位置。然後在 mousemove 事件中,我們通過判斷滑鼠現在的位置和以前位置的相對移動,來確定拖拽元素在移動中的座標。
最後 mouseup 事件觸發後,清除狀態,結束拖拽事件。

詳細資料可以參考:

《原生 js 實現拖拽功能基本思路》

126. 為什麼使用 setTimeout 實現 setInterval?怎麼模擬?

相關知識點:

// 思路是使用遞迴函式,不斷地去執行 setTimeout 從而達到 setInterval 的效果

function mySetInterval(fn, timeout) {
// 控制器,控制定時器是否繼續執行
var timer = {
flag: true
}; // 設定遞迴函式,模擬定時器執行。
function interval() {
if (timer.flag) {
fn();
setTimeout(interval, timeout);
}
} // 啟動定時器
setTimeout(interval, timeout); // 返回控制器
return timer;
}

回答:

setInterval 的作用是每隔一段指定時間執行一個函式,但是這個執行不是真的到了時間立即執行,它真正的作用是每隔一段時間將事件加入事件佇列中去,只有噹噹前的執行棧為空的時候,才能去從事件佇列中取出事件執行。所以可能會出現這樣的情況,就是當前執行棧執行的時間很長,導致事件佇列裡邊積累多個定時器加入的事件,當執行棧結束的時候,這些事件會依次執行,因此就不能到間隔一段時間執行的效果。

針對 setInterval 的這個缺點,我們可以使用 setTimeout 遞迴呼叫來模擬 setInterval,這樣我們就確保了只有一個事件結束了,我們才會觸發下一個定時器事件,這樣解決了 setInterval 的問題。

詳細資料可以參考:

《用 setTimeout 實現 setInterval》

《setInterval 有什麼缺點?》

127. let 和 const 的注意點?

  • 1.宣告的變數只在宣告時的程式碼塊內有效
  • 2.不存在宣告提升
  • 3.存在暫時性死區,如果在變數宣告前使用,會報錯
  • 4.不允許重複宣告,重複宣告會報錯

128. 什麼是 rest 引數?

rest 引數(形式為...變數名),用於獲取函式的多餘引數。

129. 什麼是尾呼叫,使用尾呼叫有什麼好處?

尾呼叫指的是函式的最後一步呼叫另一個函式。我們程式碼執行是基於執行棧的,所以當我們在一個函式裡呼叫另一個函式時,我們會保留當前的執行上下文,然後再新建另外一個執行上下文加入棧中。使用尾呼叫的話,因為已經是函式的最後一步,所以這個時候我們可以不必再保留當前的執行上下文,從而節省了記憶體,這就是尾呼叫優化。但是 ES6 的尾呼叫優化只在嚴格模式下開啟,正常模式是無效的。

130. Symbol 型別的注意點?

  • 1.Symbol 函式前不能使用 new 命令,否則會報錯。
  • 2.Symbol 函式可以接受一個字串作為引數,表示對 Symbol 例項的描述,主要是為了在控制檯顯示,或者轉為字串時,比較容易區分。
  • 3.Symbol 作為屬性名,該屬性不會出現在 for...in、for...of 迴圈中,也不會被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。
  • 4.Object.getOwnPropertySymbols 方法返回一個數組,成員是當前物件的所有用作屬性名的 Symbol 值。
  • 5.Symbol.for 接受一個字串作為引數,然後搜尋有沒有以該引數作為名稱的 Symbol 值。如果有,就返回這個 Symbol 值,否則就新建並返回一個以該字串為名稱的 Symbol 值。
  • 6.Symbol.keyFor 方法返回一個已登記的 Symbol 型別值的 key。

131. Set 和 WeakSet 結構?

  • 1.ES6 提供了新的資料結構 Set。它類似於陣列,但是成員的值都是唯一的,沒有重複的值。
  • 2.WeakSet 結構與 Set 類似,也是不重複的值的集合。但是 WeakSet 的成員只能是物件,而不能是其他型別的值。WeakSet 中的物件都是弱引用,即垃圾回收機制不考慮 WeakSet 對該物件的引用,

132. Map 和 WeakMap 結構?

  • 1.Map 資料結構。它類似於物件,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。
  • 2.WeakMap 結構與 Map 結構類似,也是用於生成鍵值對的集合。但是 WeakMap 只接受物件作為鍵名( null 除外),不接受其他型別的值作為鍵名。而且 WeakMap 的鍵名所指向的物件,不計入垃圾回收機制。

133. 什麼是 Proxy ?

Proxy 用於修改某些操作的預設行為,等同於在語言層面做出修改,所以屬於一種“超程式設計”,即對程式語言進行程式設計。

Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裡表示由它來“代理”某些操作,可以譯為“代理器”。

134. Reflect 物件建立目的?

  • 1.將 Object 物件的一些明顯屬於語言內部的方法(比如 Object.defineProperty,放到 Reflect 物件上。
  • 2.修改某些 Object 方法的返回結果,讓其變得更合理。
  • 3.讓 Object 操作都變成函式行為。
  • 4.Reflect 物件的方法與 Proxy 物件的方法一一對應,只要是 Proxy 物件的方法,就能在 Reflect 物件上找到對應的方法。這就讓 Proxy 物件可以方便地呼叫對應的 Reflect 方法,完成預設行為,作為修改行為的基礎。也就是說,不管 Proxy 怎麼修改預設行為,你總可以在 Reflect 上獲取預設行為。

135. require 模組引入的查詢方式?

當 Node 遇到 require(X) 時,按下面的順序處理。

(1)如果 X 是內建模組(比如 require('http'))
  a. 返回該模組。
  b. 不再繼續執行。 (2)如果 X 以 "./" 或者 "/" 或者 "../" 開頭
  a. 根據 X 所在的父模組,確定 X 的絕對路徑。
  b. 將 X 當成檔案,依次查詢下面檔案,只要其中有一個存在,就返回該檔案,不再繼續執行。
X
X.js
X.json
X.node   c. 將 X 當成目錄,依次查詢下面檔案,只要其中有一個存在,就返回該檔案,不再繼續執行。
X/package.json(main欄位)
X/index.js
X/index.json
X/index.node (3)如果 X 不帶路徑
  a. 根據 X 所在的父模組,確定 X 可能的安裝目錄。
  b. 依次在每個目錄中,將 X 當成檔名或目錄名載入。 (4)丟擲 "not found"

詳細資料可以參考:

《require() 原始碼解讀》

136. 什麼是 Promise 物件,什麼是 Promises/A+ 規範?

Promise 物件是非同步程式設計的一種解決方案,最早由社群提出。Promises/A+ 規範是 JavaScript Promise 的標準,規定了一個 Promise 所必須具有的特性。

Promise 是一個建構函式,接收一個函式作為引數,返回一個 Promise 例項。一個 Promise 例項有三種狀態,分別是 pending、resolved 和 rejected,分別代表了進行中、已成功和已失敗。例項的狀態只能由 pending 轉變 resolved 或者 rejected 狀態,並且狀態一經改變,就凝固了,無法再被改變了。狀態的改變是通過 resolve() 和 reject() 函式來實現的,我們
可以在非同步操作結束後呼叫這兩個函式改變 Promise 例項的狀態,它的原型上定義了一個 then 方法,使用這個 then 方法可以為兩個狀態的改變註冊回撥函式。這個回撥函式屬於微任務,會在本輪事件迴圈的末尾執行。

詳細資料可以參考:

《Promises/A+ 規範》

《Promise》

137. 手寫一個 Promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected"; function MyPromise(fn) {
// 儲存初始化狀態
var self = this; // 初始化狀態
this.state = PENDING; // 用於儲存 resolve 或者 rejected 傳入的值
this.value = null; // 用於儲存 resolve 的回撥函式
this.resolvedCallbacks = []; // 用於儲存 reject 的回撥函式
this.rejectedCallbacks = []; // 狀態轉變為 resolved 方法
function resolve(value) {
// 判斷傳入元素是否為 Promise 值,如果是,則狀態改變必須等待前一個狀態改變後再進行改變
if (value instanceof MyPromise) {
return value.then(resolve, reject);
} // 保證程式碼的執行順序為本輪事件迴圈的末尾
setTimeout(() => {
// 只有狀態為 pending 時才能轉變,
if (self.state === PENDING) {
// 修改狀態
self.state = RESOLVED; // 設定傳入的值
self.value = value; // 執行回撥函式
self.resolvedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
} // 狀態轉變為 rejected 方法
function reject(value) {
// 保證程式碼的執行順序為本輪事件迴圈的末尾
setTimeout(() => {
// 只有狀態為 pending 時才能轉變
if (self.state === PENDING) {
// 修改狀態
self.state = REJECTED; // 設定傳入的值
self.value = value; // 執行回撥函式
self.rejectedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
} // 將兩個方法傳入函式執行
try {
fn(resolve, reject);
} catch (e) {
// 遇到錯誤時,捕獲錯誤,執行 reject 函式
reject(e);
}
} MyPromise.prototype.then = function(onResolved, onRejected) {
// 首先判斷兩個引數是否為函式型別,因為這兩個引數是可選引數
onResolved =
typeof onResolved === "function"
? onResolved
: function(value) {
return value;
}; onRejected =
typeof onRejected === "function"
? onRejected
: function(error) {
throw error;
}; // 如果是等待狀態,則將函式加入對應列表中
if (this.state === PENDING) {
this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
} // 如果狀態已經凝固,則直接執行對應狀態的函式 if (this.state === RESOLVED) {
onResolved(this.value);
} if (this.state === REJECTED) {
onRejected(this.value);
}
};

138. 如何檢測瀏覽器所支援的最小字型大小?

用 JS 設定 DOM 的字型為某一個值,然後再取出來,如果值設定成功,就說明支援。

139. 怎麼做 JS 程式碼 Error 統計?

error 統計使用瀏覽器的 window.error 事件。

140. 單例模式模式是什麼?

單例模式保證了全域性只有一個例項來被訪問。比如說常用的如彈框元件的實現和全域性狀態的實現。

141. 策略模式是什麼?

策略模式主要是用來將方法的實現和方法的呼叫分離開,外部通過不同的引數可以呼叫不同的策略。我主要在 MVP 模式解耦的時候
用來將檢視層的方法定義和方法呼叫分離。

142. 代理模式是什麼?

 代理模式是為一個物件提供一個代用品或佔位符,以便控制對它的訪問。比如說常見的事件代理。

143. 中介者模式是什麼?

中介者模式指的是,多個物件通過一箇中介者進行交流,而不是直接進行交流,這樣能夠將通訊的各個物件解耦。

144. 介面卡模式是什麼?

介面卡用來解決兩個介面不相容的情況,不需要改變已有的介面,通過包裝一層的方式實現兩個介面的正常協作。假如我們需要一種
新的介面返回方式,但是老的介面由於在太多地方已經使用了,不能隨意更改,這個時候就可以使用介面卡模式。比如我們需要一種
自定義的時間返回格式,但是我們又不能對 js 時間格式化的介面進行修改,這個時候就可以使用介面卡模式。

更多關於設計模式的資料可以參考:

《前端面試之道》

《JavaScript 設計模式》

《JavaScript 中常見設計模式整理》

145. 觀察者模式和釋出訂閱模式有什麼不同?

釋出訂閱模式其實屬於廣義上的觀察者模式

在觀察者模式中,觀察者需要直接訂閱目標事件。在目標發出內容改變的事件後,直接接收事件並作出響應。

而在釋出訂閱模式中,釋出者和訂閱者之間多了一個排程中心。排程中心一方面從釋出者接收事件,另一方面向訂閱者釋出事件,訂閱者需要在排程中心中訂閱事件。通過排程中心實現了釋出者和訂閱者關係的解耦。使用釋出訂閱者模式更利於我們程式碼的可維護性。

詳細資料可以參考:

《觀察者模式和釋出訂閱模式有什麼不同?》

146. Vue 的生命週期是什麼?

Vue 的生命週期指的是元件從建立到銷燬的一系列的過程,被稱為 Vue 的生命週期。通過提供的 Vue 在生命週期各個階段的鉤子函式,我們可以很好的在 Vue 的各個生命階段實現一些操作。

147. Vue 的各個生命階段是什麼?

Vue 一共有8個生命階段,分別是建立前、建立後、載入前、載入後、更新前、更新後、銷燬前和銷燬後,每個階段對應了一個生命週期的鉤子函式。

(1)beforeCreate 鉤子函式,在例項初始化之後,在資料監聽和事件配置之前觸發。因此在這個事件中我們是獲取不到 data 資料的。

(2)created 鉤子函式,在例項建立完成後觸發,此時可以訪問 data、methods 等屬性。但這個時候元件還沒有被掛載到頁面中去,所以這個時候訪問不到 $el 屬性。一般我們可以在這個函式中進行一些頁面初始化的工作,比如通過 ajax 請求資料來對頁面進行初始化。

(3)beforeMount 鉤子函式,在元件被掛載到頁面之前觸發。在 beforeMount 之前,會找到對應的 template,並編譯成 render 函式。

(4)mounted 鉤子函式,在元件掛載到頁面之後觸發。此時可以通過 DOM API 獲取到頁面中的 DOM 元素。

(5)beforeUpdate 鉤子函式,在響應式資料更新時觸發,發生在虛擬 DOM 重新渲染和打補丁之前,這個時候我們可以對可能會被移除的元素做一些操作,比如移除事件監聽器。

(6)updated 鉤子函式,虛擬 DOM 重新渲染和打補丁之後呼叫。

(7)beforeDestroy 鉤子函式,在例項銷燬之前呼叫。一般在這一步我們可以銷燬定時器、解綁全域性事件等。

(8)destroyed 鉤子函式,在例項銷燬之後呼叫,呼叫後,Vue 例項中的所有東西都會解除繫結,所有的事件監聽器會被移除,所有的子例項也會被銷燬。

當我們使用 keep-alive 的時候,還有兩個鉤子函式,分別是 activated 和 deactivated 。用 keep-alive 包裹的元件在切換時不會進行銷燬,而是快取到記憶體中並執行 deactivated 鉤子函式,命中快取渲染後會執行 actived 鉤子函式。

詳細資料可以參考:

《vue 生命週期深入》

《Vue 例項》

148. Vue 元件間的引數傳遞方式?

(1)父子元件間通訊

第一種方法是子元件通過 props 屬性來接受父元件的資料,然後父元件在子元件上註冊監聽事件,子元件通過 emit 觸發事
件來向父元件傳送資料。 第二種是通過 ref 屬性給子元件設定一個名字。父元件通過 $refs 元件名來獲得子元件,子元件通過 $parent 獲得父組
件,這樣也可以實現通訊。 第三種是使用 provider/inject,在父元件中通過 provider 提供變數,在子元件中通過 inject 來將變數注入到元件
中。不論子元件有多深,只要呼叫了 inject 那麼就可以注入 provider 中的資料。 (2)兄弟元件間通訊 第一種是使用 eventBus 的方法,它的本質是通過建立一個空的 Vue 例項來作為訊息傳遞的物件,通訊的元件引入這個實
例,通訊的元件通過在這個例項上監聽和觸發事件,來實現訊息的傳遞。 第二種是通過 $parent.$refs 來獲取到兄弟元件,也可以進行通訊。 (3)任意元件之間 使用 eventBus ,其實就是建立一個事件中心,相當於中轉站,可以用它來傳遞事件和接收事件。 如果業務邏輯複雜,很多元件之間需要同時處理一些公共的資料,這個時候採用上面這一些方法可能不利於專案的維護。這個時候
可以使用 vuex ,vuex 的思想就是將這一些公共的資料抽離出來,將它作為一個全域性的變數來管理,然後其他元件就可以對這個
公共資料進行讀寫操作,這樣達到了解耦的目的。

詳細資料可以參考:

《VUE 元件之間資料傳遞全集》

149. computed 和 watch 的差異?

(1)computed 是計算一個新的屬性,並將該屬性掛載到 Vue 例項上,而 watch 是監聽已經存在且已掛載到 Vue 例項上的資料,所以用 watch 同樣可以監聽 computed 計算屬性的變化。

(2)computed 本質是一個惰性求值的觀察者,具有快取性,只有當依賴變化後,第一次訪問 computed 屬性,才會計算新的值。而 watch 則是當資料發生變化便會呼叫執行函式。

(3)從使用場景上說,computed 適用一個數據被多個數據影響,而 watch 適用一個數據影響多個數據。

詳細資料可以參考:

《做面試的不倒翁:淺談 Vue 中 computed 實現原理》

《深入理解 Vue 的 watch 實現原理及其實現方式》

150. vue-router 中的導航鉤子函式

(1)全域性的鉤子函式 beforeEach 和 afterEach

beforeEach 有三個引數,to 代表要進入的路由物件,from 代表離開的路由物件。next 是一個必須要執行的函式,如果不傳引數,那就執行下一個鉤子函式,如果傳入 false,則終止跳轉,如果傳入一個路徑,則導航到對應的路由,如果傳入 error ,則導航終止,error 傳入錯誤的監聽函式。

(2)單個路由獨享的鉤子函式 beforeEnter,它是在路由配置上直接進行定義的。

(3)元件內的導航鉤子主要有這三種:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。它們是直接在路由組
件內部直接進行定義的。

詳細資料可以參考:

《導航守衛》

151. $route 和 $router 的區別?

$route 是“路由資訊物件”,包括 path,params,hash,query,fullPath,matched,name 等路由資訊引數。而 $router 是“路由例項”物件包括了路由的跳轉方法,鉤子函式等。

152. vue 常用的修飾符?

.prevent: 提交事件不再過載頁面;.stop: 阻止單擊事件冒泡;.self: 當事件發生在該元素本身而不是子元素的時候會觸發;

153. vue 中 key 值的作用?

vue 中 key 值的作用可以分為兩種情況來考慮。

第一種情況是 v-if 中使用 key。由於 Vue 會盡可能高效地渲染元素,通常會複用已有元素而不是從頭開始渲染。因此當我們使用 v-if 來實現元素切換的時候,如果切換前後含有相同型別的元素,那麼這個元素就會被複用。如果是相同的 input 元素,那麼切換前後使用者的輸入不會被清除掉,這樣是不符合需求的。因此我們可以通過使用 key 來唯一的標識一個元素,這個情況下,使用 key 的元素不會被複用。這個時候 key 的作用是用來標識一個獨立的元素。

第二種情況是 v-for 中使用 key。用 v-for 更新已渲染過的元素列表時,它預設使用“就地複用”的策略。如果資料項的順序發生了改變,Vue 不會移動 DOM 元素來匹配資料項的順序,而是簡單複用此處的每個元素。因此通過為每個列表項提供一個 key 值,來以便 Vue 跟蹤元素的身份,從而高效的實現複用。這個時候 key 的作用是為了高效的更新渲染虛擬 DOM。

詳細資料可以參考:

《Vue 面試中,經常會被問到的面試題 Vue 知識點整理》

《Vue2.0 v-for 中 :key 到底有什麼用?》

《vue 中 key 的作用》

154. computed 和 watch 區別?

computed 是計算屬性,依賴其他屬性計算值,並且 computed 的值有快取,只有當計算值變化才會返回內容。

watch 監聽到值的變化就會執行回撥,在回撥中可以進行一些邏輯操作。

155. keep-alive 元件有什麼作用?

如果你需要在元件切換的時候,儲存一些元件的狀態防止多次渲染,就可以使用 keep-alive 元件包裹需要儲存的元件。

156. vue 中 mixin 和 mixins 區別?

mixin 用於全域性混入,會影響到每個元件例項。

mixins 應該是我們最常使用的擴充套件元件的方式了。如果多個元件中有相同的業務邏輯,就可以將這些邏輯剝離出來,通過 mixins 混入程式碼,比如上拉下拉載入資料這種邏輯等等。另外需要注意的是 mixins 混入的鉤子函式會先於元件內的鉤子函式執行,並且在遇到同名選項的時候也會有選擇性的進行合併

詳細資料可以參考:

《前端面試之道》

《混入》

157. 開發中常用的幾種 Content-Type ?

(1)application/x-www-form-urlencoded

瀏覽器的原生 form 表單,如果不設定 enctype 屬性,那麼最終就會以 application/x-www-form-urlencoded 方式提交資料。該種方式提交的資料放在 body 裡面,資料按照 key1=val1&key2=val2 的方式進行編碼,key 和 val 都進行了 URL
轉碼。 (2)multipart/form-data 該種方式也是一個常見的 POST 提交方式,通常表單上傳檔案時使用該種方式。 (3)application/json 告訴伺服器訊息主體是序列化後的 JSON 字串。 (4)text/xml 該種方式主要用來提交 XML 格式的資料。

詳細資料可以參考:

《常用的幾種 Content-Type》

158. 如何封裝一個 javascript 的型別判斷函式?

function getType(value) {
// 判斷資料是 null 的情況
if (value === null) {
return value + "";
} // 判斷資料是引用型別的情況
if (typeof value === "object") {
let valueClass = Object.prototype.toString.call(value),
type = valueClass.split(" ")[1].split(""); type.pop(); return type.join("").toLowerCase();
} else {
// 判斷資料是基本資料型別的情況和函式的情況
return typeof value;
}
}

詳細資料可以參考:

《JavaScript 專題之型別判斷(上)》

159. 如何判斷一個物件是否為空物件?

function checkNullObj(obj) {
return Object.keys(obj).length === 0;
}

詳細資料可以參考:

《js 判斷一個 object 物件是否為空》

160. 使用閉包實現每隔一秒列印 1,2,3,4

// 使用閉包實現
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
} // 使用 let 塊級作用域 for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}

161. 手寫一個 jsonp

function jsonp(url, params, callback) {
// 判斷是否含有引數
let queryString = url.indexOf("?") === "-1" ? "?" : "&"; // 新增引數
for (var k in params) {
if (params.hasOwnProperty(k)) {
queryString += k + "=" + params[k] + "&";
}
} // 處理回撥函式名
let random = Math.random()
.toString()
.replace(".", ""),
callbackName = "myJsonp" + random; // 添加回調函式
queryString += "callback=" + callbackName; // 構建請求
let scriptNode = document.createElement("script");
scriptNode.src = url + queryString; window[callbackName] = function() {
// 呼叫回撥函式
callback(...arguments); // 刪除這個引入的指令碼
document.getElementsByTagName("head")[0].removeChild(scriptNode);
}; // 發起請求
document.getElementsByTagName("head")[0].appendChild(scriptNode);
}

詳細資料可以參考:

《原生 jsonp 具體實現》

《jsonp 的原理與實現》

162. 手寫一個觀察者模式?

var events = (function() {
var topics = {}; return {
// 註冊監聽函式
subscribe: function(topic, handler) {
if (!topics.hasOwnProperty(topic)) {
topics[topic] = [];
}
topics[topic].push(handler);
}, // 釋出事件,觸發觀察者回調事件
publish: function(topic, info) {
if (topics.hasOwnProperty(topic)) {
topics[topic].forEach(function(handler) {
handler(info);
});
}
}, // 移除主題的一個觀察者的回撥事件
remove: function(topic, handler) {
if (!topics.hasOwnProperty(topic)) return; var handlerIndex = -1;
topics[topic].forEach(function(item, index) {
if (item === handler) {
handlerIndex = index;
}
}); if (handlerIndex >= 0) {
topics[topic].splice(handlerIndex, 1);
}
}, // 移除主題的所有觀察者的回撥事件
removeAll: function(topic) {
if (topics.hasOwnProperty(topic)) {
topics[topic] = [];
}
}
};
})();

詳細資料可以參考:

《JS 事件模型》

163. EventEmitter 實現

class EventEmitter {
constructor() {
this.events = {};
} on(event, callback) {
let callbacks = this.events[event] || [];
callbacks.push(callback);
this.events[event] = callbacks; return this;
} off(event, callback) {
let callbacks = this.events[event];
this.events[event] = callbacks && callbacks.filter(fn => fn !== callback); return this;
} emit(event, ...args) {
let callbacks = this.events[event];
callbacks.forEach(fn => {
fn(...args);
}); return this;
} once(event, callback) {
let wrapFun = function(...args) {
callback(...args); this.off(event, wrapFun);
};
this.on(event, wrapFun); return this;
}
}

164. 一道常被人輕視的前端 JS 面試題

function Foo() {
getName = function() {
alert(1);
};
return this;
}
Foo.getName = function() {
alert(2);
};
Foo.prototype.getName = function() {
alert(3);
};
var getName = function() {
alert(4);
};
function getName() {
alert(5);
} //請寫出以下輸出結果:
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3

詳細資料可以參考:

《前端程式設計師經常忽視的一個 JavaScript 面試題》

《一道考察運算子優先順序的 JavaScript 面試題》

《一道常被人輕視的前端 JS 面試題》

165. 如何確定頁面的可用性時間,什麼是 Performance API?

Performance API 用於精確度量、控制、增強瀏覽器的效能表現。這個 API 為測量網站效能,提供以前沒有辦法做到的精度。

使用 getTime 來計算指令碼耗時的缺點,首先,getTime方法(以及 Date 物件的其他方法)都只能精確到毫秒級別(一秒的千分之一),想要得到更小的時間差別就無能為力了。其次,這種寫法只能獲取程式碼執行過程中的時間進度,無法知道一些後臺事件的時間進度,比如瀏覽器用了多少時間從伺服器載入網頁。

為了解決這兩個不足之處,ECMAScript 5引入“高精度時間戳”這個 API,部署在 performance 物件上。它的精度可以達到1毫秒
的千分之一(1秒的百萬分之一)。 navigationStart:當前瀏覽器視窗的前一個網頁關閉,發生 unload 事件時的 Unix 毫秒時間戳。如果沒有前一個網頁,則等於 fetchStart 屬性。 loadEventEnd:返回當前網頁 load 事件的回撥函式執行結束時的 Unix 毫秒時間戳。如果該事件還沒有發生,返回 0。

根據上面這些屬性,可以計算出網頁載入各個階段的耗時。比如,網頁載入整個過程的耗時的計算方法如下:

var t = performance.timing;
var pageLoadTime = t.loadEventEnd - t.navigationStart;

詳細資料可以參考:

《Performance API》

166. js 中的命名規則

(1)第一個字元必須是字母、下劃線(_)或美元符號($)
(2)餘下的字元可以是下劃線、美元符號或任何字母或數字字元 一般我們推薦使用駝峰法來對變數名進行命名,因為這樣可以與 ECMAScript 內建的函式和物件命名格式保持一致。

詳細資料可以參考:

《ECMAScript 變數》

167. js 語句末尾分號是否可以省略?

在 ECMAScript 規範中,語句結尾的分號並不是必需的。但是我們一般最好不要省略分號,因為加上分號一方面有
利於我們程式碼的可維護性,另一方面也可以避免我們在對程式碼進行壓縮時出現錯誤。

168. Object.assign()

Object.assign() 方法用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件。它將返回目標物件。

169. Math.ceil 和 Math.floor

Math.ceil() === 向上取整,函式返回一個大於或等於給定數字的最小整數。

Math.floor() === 向下取整,函式返回一個小於或等於給定數字的最大整數。

170. js for 迴圈注意點

for (var i = 0, j = 0; i < 5, j < 9; i++, j++) {
console.log(i, j);
} // 當判斷語句含有多個語句時,以最後一個判斷語句的值為準,因此上面的程式碼會執行 10 次。
// 當判斷語句為空時,迴圈會一直進行。

171. 一個列表,假設有 100000 個數據,這個該怎麼辦?

我們需要思考的問題:該處理是否必須同步完成?資料是否必須按順序完成?

解決辦法:

(1)將資料分頁,利用分頁的原理,每次伺服器端只返回一定數目的資料,瀏覽器每次只對一部分進行載入。

(2)使用懶載入的方法,每次載入一部分資料,其餘資料當需要使用時再去載入。

(3)使用陣列分塊技術,基本思路是為要處理的專案建立一個佇列,然後設定定時器每過一段時間取出一部分資料,然後再使用定時器取出下一個要處理的專案進行處理,接著再設定另一個定時器。

172. js 中倒計時的糾偏實現?

在前端實現中我們一般通過 setTimeout 和 setInterval 方法來實現一個倒計時效果。但是使用這些方法會存在時間偏差的問題,這是由於 js 的程式執行機制造成的,setTimeout 和 setInterval 的作用是隔一段時間將回調事件加入到事件佇列中,因此事件並不是立即執行的,它會等到當前執行棧為空的時候再取出事件執行,因此事件等待執行的時間就是造成誤差的原因。

一般解決倒計時中的誤差的有這樣兩種辦法:

(1)第一種是通過前端定時向伺服器傳送請求獲取最新的時間差,以此來校準倒計時時間。

(2)第二種方法是前端根據偏差時間來自動調整間隔時間的方式來實現的。這一種方式首先是以 setTimeout 遞迴的方式來實現倒計時,然後通過一個變數來記錄已經倒計時的秒數。每一次函式呼叫的時候,首先將變數加一,然後根據這個變數和每次的間隔時間,我們就可以計算出此時無偏差時應該顯示的時間。然後將當前的真實時間與這個時間相減,這樣我們就可以得到時間的偏差大小,因此我們在設定下一個定時器的間隔大小的時候,我們就從間隔時間中減去這個偏差大小,以此來實現由於程式執行所造成的時間誤差的糾正。

詳細資料可以參考:

《JavaScript 前端倒計時糾偏實現》

173. 程序間通訊的方式?

  • 1.管道通訊
  • 2.訊息佇列通訊
  • 3.訊號量通訊
  • 4.訊號通訊
  • 5.共享記憶體通訊
  • 6.套接字通訊

詳細資料可以參考:

《程序間 8 種通訊方式詳解》

《程序與執行緒的一個簡單解釋》

174. 如何查詢一篇英文文章中出現頻率最高的單詞?

function findMostWord(article) {
// 合法性判斷
if (!article) return; // 引數處理
article = article.trim().toLowerCase(); let wordList = article.match(/[a-z]+/g),
visited = [],
maxNum = 0,
maxWord = ""; article = " " + wordList.join(" ") + " "; // 遍歷判斷單詞出現次數
wordList.forEach(function(item) {
if (visited.indexOf(item) < 0) {
let word = new RegExp(" " + item + " ", "g"),
num = article.match(word).length; if (num > maxNum) {
maxNum = num;
maxWord = item;
}
}
}); return maxWord + " " + maxNum;
}

最後

最後如果文章和筆記能帶您一絲幫助或者啟發,請不要吝嗇你的贊和收藏,你的肯定是我前進的最大動力