由淺入深詳細整理JavaScript面試知識點

2022-02-14 19:00:38
本篇文章給大家帶來了關於面試知識點的詳細總結,文章總結了66條JavaScript知識點由淺入深,希望對大家有幫助。

前言

我只想面個CV工程師,面試官偏偏讓我挑戰造火箭工程師,加上今年這個情況更是前後兩男,但再難苟且的生活還要繼續,飯碗還是要繼續找的。在最近的面試中我一直在總結,每次面試回來也都會覆盤,下面是我這幾天遇到的面試知識點。但今天主題是標題所寫的66條JavaScript知識點,由淺入深,整理了一週,每(zhěng)天(lǐ)整(bù)理( yì)10條( qiú)左(diǎn)右(zàn), 希望對正在找工作的小夥伴有點幫助,文中如有表述不對,還請指出。

HTML&CSS:

  • 瀏覽器核心
  • 盒模型、flex佈局、兩/三欄佈局、水平/垂直居中;
  • BFC、清除浮動;
  • css3動畫、H5新特性。

JavaScript:

  • 繼承、原型鏈、this指向、設計模式、call, apply, bind,;
  • new實現、防抖節流、let, var, const 區別、暫時性死區、event、loop;
  • promise使用及實現、promise並行執行和順序執行;
  • async/await的優缺點;
  • 閉包、垃圾回收和記憶體漏失、陣列方法、陣列亂序, 陣列扁平化、事件委託、事件監聽、事件模型

Vue:

  • vue資料雙向繫結原理;
  • vue computed原理、computed和watch的區別;
  • vue編譯器結構圖、生命週期、vue元件通訊;
  • mvvm模式、mvc模式理解;
  • vue dom diff、vuex、vue-router

網路:

  • HTTP1, HTTP2, HTTPS、常見的http狀態碼;
  • 瀏覽從輸入網址到回車發生了什麼;
  • 前端安全(CSRF、XSS)
  • 前端跨域、瀏覽器快取、cookie, session, token, localstorage, sessionstorage;
  • TCP連線(三次握手, 四次揮手)

效能相關

  • 圖片優化的方式
  • 500 張圖片,如何實現預載入優化
  • 懶載入具體實現
  • 減少http請求的方式
  • webpack如何設定大型專案

另外更全面的面試題集我也在整理中,先給個預告圖:

img下面進入正題:

1.介紹一下js的資料型別有哪些,值是如何儲存的

具體可看我之前的文章:「前端料包」可能是最透徹的JavaScript資料型別詳解

JavaScript一共有8種資料型別,其中有7種基本資料型別:Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示獨一無二的值)和BigInt(es10新增);

1種參照資料型別——Object(Object本質上是由一組無序的名值對組成的)。裡面包含 function、Array、Date等。JavaScript不支援任何建立自定義型別的機制,而所有值最終都將是上述 8 種資料型別之一。

原始資料型別:直接儲存在(stack)中,佔據空間小、大小固定,屬於被頻繁使用資料,所以放入棧中儲存。

參照資料型別:同時儲存在(stack)和(heap)中,佔據空間大、大小不固定。參照資料型別在棧中儲存了指標,該指標指向堆中該實體的起始地址。當直譯器尋找參照值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實體。

2. && 、 ||和!! 運運算元分別能做什麼

  • && 叫邏輯與,在其運算元中找到第一個虛值表示式並返回它,如果沒有找到任何虛值表示式,則返回最後一個真值表示式。它採用短路來防止不必要的工作。
  • || 叫邏輯或,在其運算元中找到第一個真值表示式並返回它。這也使用了短路來防止不必要的工作。在支援 ES6 預設函數引數之前,它用於初始化函數中的預設引數值。
  • !! 運運算元可以將右側的值強制轉換為布林值,這也是將值轉換為布林值的一種簡單方法。

3. js的資料型別的轉換

在 JS 中型別轉換只有三種情況,分別是:

  • 轉換為布林值(呼叫Boolean()方法)
  • 轉換為數位(呼叫Number()、parseInt()和parseFloat()方法)
  • 轉換為字串(呼叫.toString()或者String()方法)
null和underfined沒有.toString方法

img 此外還有一些操作符會存在隱式轉換,此處不做展開,可自行百度00

4. JS中資料型別的判斷( typeof,instanceof,constructor,Object.prototype.toString.call()

(1)typeof

typeof 對於原始型別來說,除了 null 都可以顯示正確的型別

console.log(typeof 2);              
 // numberconsole.log(typeof true);            
 // booleanconsole.log(typeof 'str');           
 // stringconsole.log(typeof []);              
 // object     
 []陣列的資料型別在 typeof 中被解釋為 objectconsole.log(typeof function(){});    
 // functionconsole.log(typeof {});              
 // objectconsole.log(typeof undefined);      
  // undefinedconsole.log(typeof null);            
  // object     null 的資料型別被 typeof 解釋為 object

typeof 對於物件來說,除了函數都會顯示 object,所以說 typeof 並不能準確判斷變數到底是什麼型別,所以想判斷一個物件的正確型別,這時候可以考慮使用 instanceof

(2)instanceof

instanceof 可以正確的判斷物件的型別,因為內部機制是通過判斷物件的原型鏈中是不是能找到型別的 prototype。

console.log(2 instanceof Number);           
         // falseconsole.log(true instanceof Boolean);     
                    // false console.log('str' instanceof String);                // false  console.log([] instanceof Array);                    // trueconsole.log(function(){} instanceof Function);       // trueconsole.log({} instanceof Object);                  
                     // true   
                      // console.log(undefined instanceof Undefined);
                     // console.log(null instanceof Null);

可以看出直接的字面量值判斷資料型別,instanceof可以精準判斷參照資料型別(Array,Function,Object),而基本資料型別不能被instanceof精準判斷。

我們來看一下 instanceof 在MDN中的解釋:instanceof 運運算元用來測試一個物件在其原型鏈中是否存在一個建構函式的 prototype 屬性。其意思就是判斷物件是否是某一資料型別(如Array)的範例,請重點關注一下是判斷一個物件是否是資料型別的範例。在這裡字面量值,2, true ,'str’不是範例,所以判斷值為false。

(3)constructor

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

(4)Object.prototype.toString.call() 使用 Object 物件的原型方法 toString ,使用 call 進行狸貓換太子,借用Object的 toString 方法

var a = Object.prototype.toString;
 console.log(a.call(2));console.log(a.call(true));
 console.log(a.call('str'));console.log(a.call([]));
 console.log(a.call(function(){}));console.log(a.call({}));
 console.log(a.call(undefined));console.log(a.call(null));

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

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

涉及知識點:

全域性的物件( 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 所有內建物件屬性和方法彙總》

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 主要用於賦值給一些可能會返回物件的變數,作為初始化。

其實 null 不是物件,雖然 typeof null 會輸出 object,但是這只是 JS 存在的一個悠久 Bug。在 JS 的最初版本中使用的是 32 位系統,為了效能考慮使用低位儲存變數的型別資訊,000 開頭代表是物件,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部型別判斷程式碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

undefined 在 js 中不是一個保留字,這意味著我們可以使用 undefined 來作為一個變數名,這樣的做法是非常危險的,它 會影響我們對 undefined 值的判斷。但是我們可以通過一些方法獲得安全的 undefined 值,比如說 void 0。

當我們對兩種型別使用 typeof 進行判斷的時候,Null 型別化會返回 「object」,這是一個歷史遺留的問題。當我們使用雙等 號對兩種型別的值進行比較時會返回 true,使用三個等號時會返回 false。

詳細資料可以參考:

《JavaScript 深入理解之 undefined 與 null》

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

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

9. Javascript 的作用域和作用域鏈

作用域: 作用域是定義變數的區域,它有一套存取變數的規則,這套規則來管理瀏覽器引擎如何在當前作用域以及巢狀的作用域中根據變數(識別符號)進行變數查詢。

作用域鏈: 作用域鏈的作用是保證對執行環境有權存取的所有變數和函數的有序存取,通過作用域鏈,我們可以存取到外層環境的變數和 函數。

作用域鏈的本質上是一個指向變數物件的指標列表。變數物件是一個包含了執行環境中所有變數和函數的物件。作用域鏈的前 端始終都是當前執行上下文的變數物件。全域性執行上下文的變數物件(也就是全域性物件)始終是作用域鏈的最後一個物件。

當我們查詢一個變數時,如果當前執行環境中沒有找到,我們可以沿著作用域鏈向後查詢。

作用域鏈的建立過程跟執行上下文的建立有關…

詳細資料可以參考: 《JavaScript 深入理解之作用域鏈》

也可以看看我的文章:「前端料包」深究JavaScript作用域(鏈)知識點和閉包

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

我們一般使用字面量的形式直接建立物件,但是這種建立方式對於建立大量相似物件的時候,會產生大量的重複程式碼。但 js和一般的物件導向的語言不同,在 ES6 之前它沒有類的概念。但是我們可以使用函數來進行模擬,從而產生出可複用的物件建立方式,我瞭解到的方式有這麼幾種:

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

嗯我目前瞭解到的就是這麼幾種方式。

詳細資料可以參考: 《JavaScript 深入理解之物件建立》

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

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

  • 第一種是以原型鏈的方式來實現繼承,但是這種實現方式存在的缺點是,在包含有參照型別的資料時,會被所有的範例物件所共用,容易造成修改的混亂。還有就是在建立子型別的時候不能向超型別傳遞引數。
  • 第二種方式是使用借用建構函式的方式,這種方式是通過在子型別的函數中呼叫超型別的建構函式來實現的,這一種方法解決了不能向超型別傳遞引數的缺點,但是它存在的一個問題就是無法實現函數方法的複用,並且超型別原型定義的方法子型別也沒有辦法存取到。
  • 第三種方式是組合繼承,組合繼承是將原型鏈和借用建構函式組合起來使用的一種方式。通過借用建構函式的方式來實現型別的屬性的繼承,通過將子型別的原型設定為超型別的範例來實現方法的繼承。這種方式解決了上面的兩種模式單獨使用時的問題,但是由於我們是以超型別的範例來作為子型別的原型,所以呼叫了兩次超類別建構函式,造成了子型別的原型中多了很多不必要的屬性。
  • 第四種方式是原型式繼承,原型式繼承的主要思路就是基於已有的物件來建立新的物件,實現的原理是,向函數中傳入一個物件,然後返回一個以這個物件為原型的物件。這種繼承的思路主要不是為了實現創造一種新的型別,只是對某個物件實現一種簡單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實現。缺點與原型鏈方式相同。
  • 第五種方式是寄生式繼承,寄生式繼承的思路是建立一個用於封裝繼承過程的函數,通過傳入一個物件,然後複製一個物件的副本,然後物件進行擴充套件,最後返回這個物件。這個擴充套件的過程就可以理解是一種繼承。這種繼承的優點就是對一個簡單物件實現繼承,如果這個物件不是我們的自定義型別時。缺點是沒有辦法實現函數的複用
  • 第六種方式是寄生式組合繼承,組合繼承的缺點就是使用超型別的範例做為子型別的原型,導致新增了不必要的原型屬性。寄生式組合繼承的方式是使用超型別的原型的副本來作為子型別的原型,這樣就避免了建立不必要的屬性。(詳細解釋見紅寶書247-248頁)
    複製程式碼

詳細資料可以參考: 《JavaScript 深入理解之繼承》

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

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 + ".");
  };

13. 談談你對this、call、apply和bind的理解

詳情可看我之前的文章:「前端料包」一文徹底搞懂JavaScript中的this、call、apply和bind

  1. 在瀏覽器裡,在全域性範圍內this 指向window物件;
  2. 在函數中,this永遠指向最後呼叫他的那個物件;
  3. 建構函式中,this指向new出來的那個新的物件;
  4. call、apply、bind中的this被強繫結在指定的那個物件上;
  5. 箭頭函數中this比較特殊,箭頭函數this為父作用域的this,不是呼叫時的this.要知道前四種方式,都是呼叫時確定,也就是動態的,而箭頭函數的this指向是靜態的,宣告的時候就確定了下來;
  6. apply、call、bind都是js給函數內建的一些API,呼叫他們可以為函數指定this的執行,同時也可以傳參。

img

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

在 js 中我們是使用建構函式來新建一個物件的,每一個建構函式的內部都有一個 prototype 屬性值,這個屬性值是一個對 象,這個物件包含了可以由該建構函式的所有範例共用的屬性和方法。當我們使用建構函式新建一個物件後,在這個物件的內部 將包含一個指標,這個指標指向建構函式的 prototype 屬性對應的值,在 ES5 中這個指標被稱為物件的原型。一般來說我們 是不應該能夠獲取到這個值的,但是現在瀏覽器中都實現了 proto 屬性來讓我們存取這個屬性,但是我們最好不要使用這 個屬性,因為它不是規範中規定的。ES5 中新增了一個 Object.getPrototypeOf() 方法,我們可以通過這個方法來獲取對 象的原型。

當我們存取一個物件的屬性時,如果這個物件內部不存在這個屬性,那麼它就會去它的原型物件裡找這個屬性,這個原型物件又 會有自己的原型,於是就這樣一直找下去,也就是原型鏈的概念。原型鏈的盡頭一般來說都是 Object.prototype 所以這就 是我們新建的物件為什麼能夠使用 toString() 等方法的原因。

特點:

JavaScript 物件是通過參照來傳遞的,我們建立的每個新物件實體中並沒有一份屬於自己的原型副本。當我們修改原型時,與 之相關的物件也會繼承這一改變。

參考文章:

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

也可以看看我寫的:「前端料包」深入理解JavaScript原型和原型鏈

15. js 獲取原型的方法?

  • p.proto
  • p.constructor.prototype
  • Object.getPrototypeOf§

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

閉包是指有權存取另一個函數作用域內變數的函數,建立閉包的最常見的方式就是在一個函數內建立另一個函數,建立的函數可以 存取到當前函數的區域性變數。

閉包有兩個常用的用途。

  • 閉包的第一個用途是使我們在函數外部能夠存取到函數內部的變數。通過使用閉包,我們可以通過在外部呼叫閉包函數,從而在外部存取到函數內部的變數,可以使用這種方法來建立私有變數。
  • 函數的另一個用途是使已經執行結束的函數上下文中的變數物件繼續留在記憶體中,因為閉包函數保留了這個變數物件的參照,所以這個變數物件不會被回收。
function a(){
    var n = 0;
    function add(){
       n++;
       console.log(n);
    }
    return add;}var a1 = a(); //注意,函數名只是一個標識(指向函數的指標),而()才是執行函數;a1();
        //1a1();   
         //2  第二次呼叫n變數還在記憶體中

其實閉包的本質就是作用域鏈的一個特殊的應用,只要瞭解了作用域鏈的建立過程,就能夠理解閉包的實現原理。

17. 什麼是 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 詳解》

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

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

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

相關資料:

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

19. 事件委託是什麼?

事件委託 本質上是利用了瀏覽器事件冒泡的機制。因為事件在冒泡過程中會上傳到父節點,並且父節點可以通過事件物件獲取到 目標節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件,這種方式稱為事件代理。

使用事件代理我們可以不必要為每一個子元素都繫結一個監聽事件,這樣減少了記憶體上的消耗。並且使用事件代理我們還可以實現事件的動態繫結,比如說新增了一個子節點,我們並不需要單獨地為它新增一個監聽事件,它所發生的事件會交給父元素中的監聽函數來處理。

相關資料:

《JavaScript 事件委託詳解》

20. 什麼是事件傳播?

事件發生在DOM元素上時,該事件並不完全發生在那個元素上。在「當事件發生在DOM元素上時,該事件並不完全發生在那個元素上。

事件傳播有三個階段:

  1. 捕獲階段–事件從 window 開始,然後向下到每個元素,直到到達目標元素事件或event.target。
  2. 目標階段–事件已達到目標元素。
  3. 冒泡階段–事件從目標元素冒泡,然後上升到每個元素,直到到達 window。

21. 什麼是事件捕獲?

當事件發生在 DOM 元素上時,該事件並不完全發生在那個元素上。在捕獲階段,事件從window開始,一直到觸發事件的元素。window----> document----> html----> body ---->目標元素

假設有如下的 HTML 結構:

<p class="grandparent">
  <p class="parent">
    <p class="child">1</p>
  </p>
</p>

對應的 JS 程式碼:

function addEvent(el, event, callback, isCapture = false) {
  if (!el || !event || !callback || typeof callback !== 'function') return;
  if (typeof el === 'string') {
    el = document.querySelector(el);
  };
  el.addEventListener(event, callback, isCapture);}addEvent(document, 'DOMContentLoaded', () => {
  const child = document.querySelector('.child');
  const parent = document.querySelector('.parent');
  const grandparent = document.querySelector('.grandparent');

  addEvent(child, 'click', function (e) {
    console.log('child');
  });

  addEvent(parent, 'click', function (e) {
    console.log('parent');
  });

  addEvent(grandparent, 'click', function (e) {
    console.log('grandparent');
  });

  addEvent(document, 'click', function (e) {
    console.log('document');
  });

  addEvent('html', 'click', function (e) {
    console.log('html');
  })

  addEvent(window, 'click', function (e) {
    console.log('window');
  })});

addEventListener方法具有第三個可選引數useCapture,其預設值為false,事件將在冒泡階段中發生,如果為true,則事件將在捕獲階段中發生。如果單擊child元素,它將分別在控制檯上列印windowdocumenthtmlgrandparentparent,這就是事件捕獲

22. 什麼是事件冒泡?

事件冒泡剛好與事件捕獲相反,當前元素---->body ----> html---->document ---->window。當事件發生在DOM元素上時,該事件並不完全發生在那個元素上。在冒泡階段,事件冒泡,或者事件發生在它的父代,祖父母,祖父母的父代,直到到達window為止。

假設有如下的 HTML 結構:

<p class="grandparent">
  <p class="parent">
    <p class="child">1</p>
  </p></p>

對應的JS程式碼:

function addEvent(el, event, callback, isCapture = false) {
  if (!el || !event || !callback || typeof callback !== 'function') return;
  if (typeof el === 'string') {
    el = document.querySelector(el);
  };
  el.addEventListener(event, callback, isCapture);}addEvent(document, 'DOMContentLoaded', () => {
  const child = document.querySelector('.child');
  const parent = document.querySelector('.parent');
  const grandparent = document.querySelector('.grandparent');

  addEvent(child, 'click', function (e) {
    console.log('child');
  });

  addEvent(parent, 'click', function (e) {
    console.log('parent');
  });

  addEvent(grandparent, 'click', function (e) {
    console.log('grandparent');
  });

  addEvent(document, 'click', function (e) {
    console.log('document');
  });

  addEvent('html', 'click', function (e) {
    console.log('html');
  })

  addEvent(window, 'click', function (e) {
    console.log('window');
  })});

addEventListener方法具有第三個可選引數useCapture,其預設值為false,事件將在冒泡階段中發生,如果為true,則事件將在捕獲階段中發生。如果單擊child元素,它將分別在控制檯上列印childparentgrandparenthtmldocumentwindow,這就是事件冒泡

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

(1)建立新節點

  createDocumentFragment()    //建立一個DOM片段
  createElement()   //建立一個具體的元素
  createTextNode()   //建立一個文位元組點

(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 合集》

24. js陣列和字串有哪些原生方法,列舉一下

img

img

25. 常用的正規表示式(僅做收集,涉及不深)

//(1)匹配 16 進位制顏色值
var color = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

//(2)匹配日期,如 yyyy-mm-dd 格式
var date = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

//(3)匹配 qq 號
var qq = /^[1-9][0-9]{4,10}$/g;

//(4)手機號碼正則
var phone = /^1[34578]\d{9}$/g;

//(5)使用者名稱正則
var username = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;

//(6)Email正則
var email = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;

//(7)身份證號(18位元)正則
var cP = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;

//(8)URL正則
var urlP= /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;

// (9)ipv4地址正則
var ipP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

// (10)//車牌號正則
var cPattern = /^[京津滬渝冀豫雲遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陝吉閩貴粵青藏川寧瓊使領A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9掛學警港澳]{1}$/;

// (11)強密碼(必須包含大小寫字母和數位的組合,不能使用特殊字元,長度在8-10之間):var pwd = /^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$/

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

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

建立步驟:

img

面試手寫(原生):

//1:建立Ajax物件var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 相容IE6及以下版本//2:設定 Ajax請求地址xhr.open('get','index.xml',true);//3:傳送請求xhr.send(null); // 嚴謹寫法//4:監聽請求,接受響應xhr.onreadysatechange=function(){
     if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )
          console.log(xhr.responsetXML)

jQuery寫法

  $.ajax({
          type:'post',
          url:'',
          async:ture,//async 非同步  sync  同步
          data:data,//針對post請求
          dataType:'jsonp',
          success:function (msg) {

          },
          error:function (error) {

          }
        })

promise 封裝實現:

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

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

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

我瞭解到的幾種方式是:

  1. 將 js 指令碼放在檔案的底部,來使 js 指令碼儘可能的在最後來載入執行。
  2. 給 js 指令碼新增 defer屬性,這個屬性會讓指令碼的載入與檔案的解析同步解析,然後在檔案解析完成後再執行這個指令碼檔案,這樣的話就能使頁面的渲染不被阻塞。多個設定了 defer 屬性的指令碼按規範來說最後是順序執行的,但是在一些瀏覽器中可能不是這樣。
  3. 給 js 指令碼新增 async屬性,這個屬性會使指令碼非同步載入,不會阻塞頁面的解析過程,但是當指令碼載入完成後立即執行 js指令碼,這個時候如果檔案沒有解析完成的話同樣會阻塞。多個 async 屬性的指令碼的執行順序是不可預測的,一般不會按照程式碼的順序依次執行。
  4. 動態建立 DOM 標籤的方式,我們可以對檔案的載入事件進行監聽,當檔案載入完成後再動態的建立 script 標籤來引入 js 指令碼。

相關資料:

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

《HTML 5 `` async 屬性》

28. 談談你對模組化開發的理解?

我對模組的理解是,一個模組是實現一個特定功能的一組方法。在最開始的時候,js 只實現一些簡單的功能,所以並沒有模組的概念 ,但隨著程式越來越複雜,程式碼的模組化開發變得越來越重要。

由於函數具有獨立作用域的特點,最原始的寫法是使用函數來作為模組,幾個函數作為一個模組,但是這種方式容易造成全域性變數的汙 染,並且模組間沒有聯絡。

後面提出了物件寫法,通過將函數作為一個物件的方法來實現,這樣解決了直接使用函數作為模組的一些缺點,但是這種辦法會暴露所 有的所有的模組成員,外部程式碼可以修改內部屬性的值。

現在最常用的是立即執行函數的寫法,通過利用閉包來實現模組私有作用域的建立,同時不會對全域性作用域造成汙染。

相關資料: 《淺談模組化開發》

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

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

《Module 的語法》

29. js 的幾種模組規範?

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

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

30. AMD 和 CMD 規範的區別?

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

  1. 第一個方面是在模組定義時對依賴的處理不同。AMD推崇依賴前置,在定義模組的時候就要宣告其依賴的模組。而 CMD 推崇就近依賴,只有在用到某個模組的時候再去 require。
  2. 第二個方面是對依賴模組的執行時機處理不同。首先 AMD 和 CMD 對於模組的載入方式都是非同步載入,不過它們的區別在於

模組的執行時機,AMD 在依賴模組載入完成後就直接執行依賴模組,依賴模組的執行順序和我們書寫的順序不一定一致。而 CMD 在依賴模組載入完成後並不執行,只是下載而已,等到所有的依賴模組都載入好後,進入回撥函數邏輯,遇到 require 語句 的時候才執行對應的模組,這樣模組的執行順序就和我們書寫的順序保持一致了。

// CMDdefine(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 的區別》

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

  • 1.CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的參照。CommonJS 模組輸出的是值的

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

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

32. requireJS的核心原理是什麼?

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

詳細資料可以參考: 《requireJS 的用法和原理分析》

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

《requireJS 原理分析》

33. 談談JS的執行機制

1. js單執行緒

JavaScript語言的一大特點就是單執行緒,即同一時間只能做一件事情。

JavaScript的單執行緒,與它的用途有關。作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?

所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。

2. js事件迴圈

js程式碼執行過程中會有很多工,這些任務總的分成兩類:

  • 同步任務
  • 非同步任務

當我們開啟網站時,網頁的渲染過程就是一大堆同步任務,比如頁面骨架和頁面元素的渲染。而像載入圖片音樂之類佔用資源大耗時久的任務,就是非同步任務。,我們用導圖來說明: img 我們解釋一下這張圖:

  • 同步和非同步任務分別進入不同的執行"場所",同步的進入主執行緒,非同步的進入Event Table並註冊函數。
  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue
  • 主執行緒內的任務執行完畢為空,會去Event Queue讀取對應的函數,進入主執行緒執行。
  • 上述過程會不斷重複,也就是常說的Event Loop(事件迴圈)。

那主執行緒執行棧何時為空呢?js引擎存在monitoring process程序,會持續不斷的檢查主執行緒執行棧是否為空,一旦為空,就會去Event Queue那裡檢查是否有等待被呼叫的函數。

以上就是js執行的整體流程

需要注意的是除了同步任務和非同步任務,任務還可以更加細分為macrotask(宏任務)和microtask(微任務),js引擎會優先執行微任務

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

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

面試中該如何回答呢? 下面是我個人推薦的回答:

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

最後可以用下面一道題檢測一下收穫:

setTimeout(function() {
  console.log(1)}, 0);new Promise(function(resolve, reject) {
  console.log(2);
  resolve()}).then(function() {
  console.log(3)});process.nextTick(function () {
  console.log(4)})console.log(5)

第一輪:主程開始執行,遇到setTimeout,將setTimeout的回撥函數丟到宏任務佇列中,在往下執行new Promise立即執行,輸出2,then的回撥函數丟到微任務佇列中,再繼續執行,遇到process.nextTick,同樣將回撥函數扔到為任務佇列,再繼續執行,輸出5,當所有同步任務執行完成後看有沒有可以執行的微任務,發現有then函數和nextTick兩個微任務,先執行哪個呢?process.nextTick指定的非同步任務總是發生在所有非同步任務之前,因此先執行process.nextTick輸出4然後執行then函數輸出3,第一輪執行結束。 第二輪:從宏任務佇列開始,發現setTimeout回撥,輸出1執行完畢,因此結果是25431

相關資料:

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

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

《什麼是 Event Loop?》

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

34. arguments 的物件是什麼?

arguments物件是函數中傳遞的引數值的集合。它是一個類似陣列的物件,因為它有一個length屬性,我們可以使用陣列索引表示法arguments[1]來存取單個值,但它沒有陣列中的內建方法,如:forEach、reduce、filter和map。

我們可以使用Array.prototype.slice將arguments物件轉換成一個陣列。

function one() {
  return Array.prototype.slice.call(arguments);}

注意:箭頭函數中沒有arguments物件。

function one() {
  return arguments;}const two = function () {
  return arguments;}const three = function three() {
  return arguments;}const four = () => arguments;four(); 
  // Throws an error  - arguments is not defined

當我們呼叫函數four時,它會丟擲一個ReferenceError: arguments is not defined error。使用rest語法,可以解決這個問題。

const four = (...args) => args;

這會自動將所有引數值放入陣列中。

35. 為什麼在呼叫這個函數時,程式碼中的b會變成一個全域性變數?

function myFunc() {
  let a = b = 0;}myFunc();

原因是賦值運運算元是從右到左的求值的。這意味著當多個賦值運運算元出現在一個表示式中時,它們是從右向左求值的。所以上面程式碼變成了這樣:

function myFunc() {
  let a = (b = 0);}myFunc();

首先,表示式b = 0求值,在本例中b沒有宣告。因此,JS引擎在這個函數外建立了一個全域性變數b,之後表示式b = 0的返回值為0,並賦給新的區域性變數a。

我們可以通過在賦值之前先宣告變數來解決這個問題。

function myFunc() {
  let a,b;
  a = b = 0;}myFunc();

36. 簡單介紹一下 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 引入了增量標記的方法,將一次停頓進行的過程分為了多步,每次執行完一小步就讓執行邏輯執行一會,就這樣交替執行。

37. 哪些操作會造成記憶體漏失?

  • 1.意外的全域性變數

  • 2.被遺忘的計時器或回撥函數

  • 3.脫離 DOM 的參照

  • 4.閉包

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

  • 第二種情況是我們設定了setInterval定時器,而忘記取消它,如果迴圈函數有對外部變數的參照的話,那麼這個變數會被一直留在記憶體中,而無法被回收。

  • 第三種情況是我們獲取一個DOM元素的參照,而後面這個元素被刪除,由於我們一直保留了對這個元素的參照,所以它也無法被回收。

  • 第四種情況是不合理的使用閉包,從而導致某些變數一直被留在記憶體當中。

38. ECMAScript 是什麼?

ECMAScript 是編寫指令碼語言的標準,這意味著JavaScript遵循ECMAScript標準中的規範變化,因為它是JavaScript的藍圖。

ECMAScript 和 Javascript,本質上都跟一門語言有關,一個是語言本身的名字,一個是語言的約束條件 只不過發明JavaScript的那個人(Netscape公司),把東西交給了ECMA(European Computer Manufacturers Association),這個人規定一下他的標準,因為當時有java語言了,又想強調這個東西是讓ECMA這個人定的規則,所以就這樣一個神奇的東西誕生了,這個東西的名稱就叫做ECMAScript。

javaScript = ECMAScript + DOM + BOM(自認為是一種廣義的JavaScript)

ECMAScript說什麼JavaScript就得做什麼!

JavaScript(狹義的JavaScript)做什麼都要問問ECMAScript我能不能這樣幹!如果不能我就錯了!能我就是對的!

——突然感覺JavaScript好沒有尊嚴,為啥要搞個人出來約束自己,

那個人被創造出來也好委屈,自己被創造出來完全是因為要約束JavaScript。

39. ECMAScript 2015(ES6)有哪些新特性?

  • 塊作用域
  • 箭頭函數
  • 模板字串
  • 加強的物件字面
  • 物件解構
  • Promise
  • 模組
  • Symbol
  • 代理(proxy)Set
  • 函數預設引數
  • rest 和展開

40. var,letconst的區別是什麼?

var宣告的變數會掛載在window上,而let和const宣告的變數不會:

var a = 100;console.log(a,window.a);    
// 100 100let b = 10;console.log(b,window.b);  
  // 10 undefinedconst c = 1;console.log(c,window.c);  
    // 1 undefined

var宣告變數存在變數提升,let和const不存在變數提升:

console.log(a); 
// undefined  ===>  a已宣告還沒賦值,預設得到undefined值var a = 100;console.log(b);
 // 報錯:b is not defined  ===> 找不到b這個變數let b = 10;console.log(c); 
 // 報錯:c is not defined  ===> 找不到c這個變數const c = 10;

let和const宣告形成塊作用域

if(1){
  var a = 100;
  let b = 10;}console.log(a);
   // 100console.log(b) 
    // 報錯:b is not defined  ===> 找不到b這個變數-------------------------------------------------------------if(1){
  var a = 100;
  const c = 1;}console.log(a); 
  // 100console.log(c) 
   // 報錯:c is not defined  ===> 找不到c這個變數

同一作用域下let和const不能宣告同名變數,而var可以

var a = 100;console.log(a); 
// 100var a = 10;console.log(a); 
// 10-------------------------------------let a = 100;
let a = 10;
//  控制檯報錯:Identifier 'a' has already been declared  ===> 識別符號a已經被宣告了。

暫存死區

var a = 100;if(1){
    a = 10;
    //在當前塊作用域中存在a使用let/const宣告的情況下,給a賦值10時,只會在當前作用域找變數a,
    // 而這時,還未到宣告時候,所以控制檯Error:a is not defined
    let a = 1;}

const

/*
*   1、一旦宣告必須賦值,不能使用null佔位。
*
*   2、宣告後不能再修改
*
*   3、如果宣告的是複合型別資料,可以修改其屬性
*
* */const a = 100; const list = [];list[0] = 10;console.log(list);  
// [10]const obj = {a:100};
obj.name = 'apple';obj.a = 10000;
console.log(obj);  
// {a:10000,name:'apple'}

41. 什麼是箭頭函數?

箭頭函數表示式的語法比函數表示式更簡潔,並且沒有自己的this,arguments,super或new.target。箭頭函數表示式更適用於那些本來需要匿名函數的地方,並且它不能用作建構函式。

//ES5 Versionvar getCurrentDate = function (){
  return new Date();}
  //ES6 Versionconst getCurrentDate = () => new Date();

在本例中,ES5 版本中有function(){}宣告和return關鍵字,這兩個關鍵字分別是建立函數和返回值所需要的。在箭頭函數版本中,我們只需要()括號,不需要 return 語句,因為如果我們只有一個表示式或值需要返回,箭頭函數就會有一個隱式的返回。

//ES5 Versionfunction greet(name) {
  return 'Hello ' + name + '!';}
  //ES6 Versionconst greet = (name) => `Hello ${name}`;
  const greet2 = name => `Hello ${name}`;

我們還可以在箭頭函數中使用與函數表示式和函數宣告相同的引數。如果我們在一個箭頭函數中有一個引數,則可以省略括號。

const getArgs = () => argumentsconst getArgs2 = (...rest) => rest

箭頭函數不能存取arguments物件。所以呼叫第一個getArgs函數會丟擲一個錯誤。相反,我們可以使用rest引數來獲得在箭頭函數中傳遞的所有引數。

const data = {
  result: 0,
  nums: [1, 2, 3, 4, 5],
  computeResult() {
    // 這裡的「this」指的是「data」物件
    const addAll = () => {
      return this.nums.reduce((total, cur) => total + cur, 0)
    };
    this.result = addAll();
  }};

箭頭函數沒有自己的this值。它捕獲詞法作用域函數的this值,在此範例中,addAll函數將複製computeResult 方法中的this值,如果我們在全域性作用域宣告箭頭函數,則this值為 window 物件。

42. 什麼是類?

類(class)是在 JS 中編寫建構函式的新方法。它是使用建構函式的語法糖,在底層中使用仍然是原型和基於原型的繼承。

 //ES5 Version
   function Person(firstName, lastName, age, address){
      this.firstName = firstName;
      this.lastName = lastName;
      this.age = age;
      this.address = address;
   }

   Person.self = function(){
     return this;
   }

   Person.prototype.toString = function(){
     return "[object Person]";
   }

   Person.prototype.getFullName = function (){
     return this.firstName + " " + this.lastName;
   }  

   //ES6 Version
   class Person {
        constructor(firstName, lastName, age, address){
            this.lastName = lastName;
            this.firstName = firstName;
            this.age = age;
            this.address = address;
        }

        static self() {
           return this;
        }

        toString(){
           return "[object Person]";
        }

        getFullName(){
           return `${this.firstName} ${this.lastName}`;
        }
   }

重寫方法並從另一個類繼承。

//ES5 VersionEmployee.prototype = Object.create(Person.prototype);function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
  Person.call(this, firstName, lastName, age, address);
  this.jobTitle = jobTitle;
  this.yearStarted = yearStarted;}Employee.prototype.describe = function () {
  return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;}Employee.prototype.toString = function () {
  return "[object Employee]";}//ES6 Versionclass Employee extends Person { //Inherits from "Person" class
  constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
    super(firstName, lastName, age, address);
    this.jobTitle = jobTitle;
    this.yearStarted = yearStarted;
  }

  describe() {
    return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
  }

  toString() { // Overriding the "toString" method of "Person"
    return "[object Employee]";
  }}

所以我們要怎麼知道它在內部使用原型?

class Something {}function AnotherSomething(){}const as = new AnotherSomething();
const s = new Something();
console.log(typeof Something); 
// "function"console.log(typeof AnotherSomething); 
// "function"console.log(as.toString()); 
// "[object Object]"console.log(as.toString()); 
// "[object Object]"console.log(as.toString === Object.prototype.toString); 
// trueconsole.log(s.toString === Object.prototype.toString); // true

43. 什麼是模板字串?

模板字串是在 JS 中建立字串的一種新方法。我們可以通過使用反引號使模板字串化。

//ES5 Versionvar greet = 'Hi I\'m Mark';
//ES6 Versionlet greet = `Hi I'm Mark`;

在 ES5 中我們需要使用一些跳脫字元來達到多行的效果,在模板字串不需要這麼麻煩:

//ES5 Versionvar lastWords = '\n'
  + '   I  \n'
  + '   Am  \n'
  + 'Iron Man \n';//ES6 Versionlet lastWords = `
    I
    Am
  Iron Man   
`;

在ES5版本中,我們需要新增\n以在字串中新增新行。在模板字串中,我們不需要這樣做。

//ES5 Versionfunction greet(name) {
  return 'Hello ' + name + '!';}//ES6 Versionfunction greet(name) {
  return `Hello ${name} !`;}

在 ES5 版本中,如果需要在字串中新增表示式或值,則需要使用+運運算元。在模板字串s中,我們可以使用${expr}嵌入一個表示式,這使其比 ES5 版本更整潔。

44. 什麼是物件解構?

物件解構是從物件或陣列中獲取或提取值的一種新的、更簡潔的方法。假設有如下的物件:

const employee = {
  firstName: "Marko",
  lastName: "Polo",
  position: "Software Developer",
  yearHired: 2017};

從物件獲取屬性,早期方法是建立一個與物件屬性同名的變數。這種方法很麻煩,因為我們要為每個屬性建立一個新變數。假設我們有一個大物件,它有很多屬性和方法,用這種方法提取屬性會很麻煩。

var firstName = employee.firstName;
var lastName = employee.lastName;
var position = employee.position;
var yearHired = employee.yearHired;

使用解構方式語法就變得簡潔多了:

{ firstName, lastName, position, yearHired } = employee;

我們還可以為屬性取別名:

let { firstName: fName, lastName: lName, position, yearHired } = employee;

當然如果屬性值為 undefined 時,我們還可以指定預設值:

let { firstName = "Mark", lastName: lName, position, yearHired } = employee;

45. 什麼是Set物件,它是如何工作的?

Set 物件允許你儲存任何型別的唯一值,無論是原始值或者是物件參照。

我們可以使用Set建構函式建立Set範例。

const set1 = new Set();
const set2 = new Set(["a","b","c","d","d","e"]);

我們可以使用add方法向Set範例中新增一個新值,因為add方法返回Set物件,所以我們可以以鏈式的方式再次使用add。如果一個值已經存在於Set物件中,那麼它將不再被新增。

set2.add("f");
set2.add("g").add("h").add("i").add("j").add("k").add("k");
// 後一個「k」不會被新增到set物件中,因為它已經存在了

我們可以使用has方法檢查Set範例中是否存在特定的值。

set2.has("a") 
// trueset2.has("z") 
// true

我們可以使用size屬性獲得Set範例的長度。

set2.size 
// returns 10

可以使用clear方法刪除 Set 中的資料。

set2.clear();

我們可以使用Set物件來刪除陣列中重複的元素。

const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
const uniqueNums = [...new Set(numbers)]; 
// [1,2,3,4,5,6,7,8]

另外還有WeakSet, 與 Set 類似,也是不重複的值的集合。但是 WeakSet 的成員只能是物件,而不能是其他型別的值。WeakSet 中的物件都是弱參照,即垃圾回收機制不考慮 WeakSet對該物件的參照。

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

46. 什麼是Proxy?

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

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

以下47~64條是JavaScript中比較難的高階知識及相關手寫實現,各位看官需慢慢細品

47. 寫一個通用的事件偵聽器函數

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

48. 什麼是函數語言程式設計? JavaScript的哪些特性使其成為函數式語言的候選語言?

函數語言程式設計(通常縮寫為FP)是通過編寫純函數,避免共用狀態、可變資料、副作用 來構建軟體的過程。數式程式設計是宣告式 的而不是命令式 的,應用程式的狀態是通過純函數流動的。與物件導向程式設計形成對比,物件導向中應用程式的狀態通常與物件中的方法共用和共處。

函數語言程式設計是一種程式設計正規化 ,這意味著它是一種基於一些基本的定義原則(如上所列)思考軟體構建的方式。當然,程式設計正規化的其他範例也包括物件導向程式設計和過程程式設計。

函數式的程式碼往往比命令式或物件導向的程式碼更簡潔,更可預測,更容易測試 - 但如果不熟悉它以及與之相關的常見模式,函數式的程式碼也可能看起來更密集雜亂,並且 相關文獻對新人來說是不好理解的。

49. 什麼是高階函數?

高階函數只是將函數作為引數或返回值的函數。

function higherOrderFunction(param,callback){
    return callback(param);}

50. 為什麼函數被稱為一等公民?

在JavaScript中,函數不僅擁有一切傳統函數的使用方式(宣告和呼叫),而且可以做到像簡單值一樣:

  • 賦值(var func = function(){})、
  • 傳參(function func(x,callback){callback();})、
  • 返回(function(){return function(){}}),

這樣的函數也稱之為第一級函數(First-class Function)。不僅如此,JavaScript中的函數還充當了類別建構函式的作用,同時又是一個Function類的範例(instance)。這樣的多重身份讓JavaScript的函數變得非常重要。

51. 手動實現 Array.prototype.map 方法

map() 方法建立一個新陣列,其結果是該陣列中的每個元素都呼叫一個提供的函數後返回的結果。

function map(arr, mapCallback) {
  // 首先,檢查傳遞的引數是否正確。
  if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') { 
    return [];
  } else {
    let result = [];
    // 每次呼叫此函數時,我們都會建立一個 result 陣列
    // 因為我們不想改變原始陣列。
    for (let i = 0, len = arr.length; i < len; i++) {
      result.push(mapCallback(arr[i], i, arr)); 
      // 將 mapCallback 返回的結果 push 到 result 陣列中
    }
    return result;
  }}

52. 手動實現Array.prototype.filter方法

filter()方法建立一個新陣列, 其包含通過所提供函數實現的測試的所有元素。

function filter(arr, filterCallback) {
  // 首先,檢查傳遞的引數是否正確。
  if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function') 
  {
    return [];
  } else {
    let result = [];
     // 每次呼叫此函數時,我們都會建立一個 result 陣列
     // 因為我們不想改變原始陣列。
    for (let i = 0, len = arr.length; i < len; i++) {
      // 檢查 filterCallback 的返回值是否是真值
      if (filterCallback(arr[i], i, arr)) { 
      // 如果條件為真,則將陣列元素 push 到 result 中
        result.push(arr[i]);
      }
    }
    return result; // return the result array
  }}

53. 手動實現Array.prototype.reduce方法

reduce() 方法對陣列中的每個元素執行一個由您提供的reducer函數(升序執行),將其結果彙總為單個返回值。

function reduce(arr, reduceCallback, initialValue) {
  // 首先,檢查傳遞的引數是否正確。
  if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function') 
  {
    return [];
  } else {
    // 如果沒有將initialValue傳遞給該函數,我們將使用第一個陣列項作為initialValue
    let hasInitialValue = initialValue !== undefined;
    let value = hasInitialValue ? initialValue : arr[0];
   、    // 如果有傳遞 initialValue,則索引從 1 開始,否則從 0 開始
    for (let i = hasInitialValue ? 1 : 0, len = arr.length; i < len; i++) {
      value = reduceCallback(value, arr[i], i, arr); 
    }
    return value;
  }}

54. js的深淺拷貝

JavaScript的深淺拷貝一直是個難點,如果現在面試官讓我寫一個深拷貝,我可能也只是能寫出個基礎版的。所以在寫這條之前我拜讀了收藏夾裡各路大佬寫的博文。具體可以看下面我貼的連結,這裡只做簡單的總結。

  • 淺拷貝: 建立一個新物件,這個物件有著原始物件屬性值的一份精確拷貝。如果屬性是基本型別,拷貝的就是基本型別的值,如果屬性是參照型別,拷貝的就是記憶體地址 ,所以如果其中一個物件改變了這個地址,就會影響到另一個物件。
  • 深拷貝: 將一個物件從記憶體中完整的拷貝一份出來,從堆記憶體中開闢一個新的區域存放新物件,且修改新物件不會影響原物件。

淺拷貝的實現方式:

  • Object.assign() 方法: 用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件。它將返回目標物件。
  • **Array.prototype.slice():**slice() 方法返回一個新的陣列物件,這一物件是一個由 begin和end(不包括end)決定的原陣列的淺拷貝。原始陣列不會被改變。
  • 拓展運運算元...
let a = {
    name: "Jake",
    flag: {
        title: "better day by day",
        time: "2020-05-31"
    }}let b = {...a};

深拷貝的實現方式:

  • 乞丐版: JSON.parse(JSON.stringify(object)),缺點諸多(會忽略undefined、symbol、函數;不能解決迴圈參照;不能處理正則、new Date())
  • 基礎版(面試夠用): 淺拷貝+遞迴 (只考慮了普通的 object和 array兩種資料型別)
function cloneDeep(target,map = new WeakMap()) {
  if(typeOf taret ==='object'){
     let cloneTarget = Array.isArray(target) ? [] : {};
      
     if(map.get(target)) {
        return target;
    }
     map.set(target, cloneTarget);
     for(const key in target){
        cloneTarget[key] = cloneDeep(target[key], map);
     }
     return cloneTarget  }else{
       return target  }
 }
  • 終極版:
const mapTag = '[object Map]';const setTag = '[object Set]';const arrayTag = '[object Array]';const objectTag = '[object Object]';const argsTag = '[object Arguments]';const boolTag = '[object Boolean]';const dateTag = '[object Date]';const numberTag = '[object Number]';const stringTag = '[object String]';const symbolTag = '[object Symbol]';const errorTag = '[object Error]';const regexpTag = '[object RegExp]';const funcTag = '[object Function]';const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index);
    }
    return array;}function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');}function getType(target) {
    return Object.prototype.toString.call(target);}function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();}function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));}function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;}function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }}function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        case funcTag:
            return cloneFunction(targe);
        default:
            return null;
    }}function clone(target, map = new WeakMap()) {

    // 克隆原始型別
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    } else {
        return cloneOtherType(target, type);
    }

    // 防止迴圈參照
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆物件和陣列
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;}module.exports = {
    clone};

55. 手寫call、apply及bind函數

call 函數的實現步驟:

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

    1. 判斷呼叫物件是否為函數,即使我們是定義在函數的原型上的,但是可能出現使用 call 等方式呼叫的情況。
    1. 判斷傳入上下文物件是否存在,如果不存在,則設定為 window 。
    1. 將函數作為上下文物件的一個屬性。
    1. 判斷引數值是否傳入
    1. 使用上下文物件來呼叫這個方法,並儲存返回結果。
    1. 刪除剛才新增的屬性
    1. 返回結果
// 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 函數的實現步驟:

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

56. 函數柯里化的實現

// 函數柯里化指的是一種將使用多個引數的一個函數轉換成一系列使用一個引數的函數的技術。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 專題之函數柯里化》

57. js模擬new操作符的實現

這個問題如果你在掘金上搜,你可能會搜尋到類似下面的回答:

img 說實話,看第一遍,我是不理解的,我需要去理一遍原型及原型鏈的知識才能理解。所以我覺得MDN對new的解釋更容易理解:

new 運運算元建立一個使用者定義的物件型別的範例或具有建構函式的內建物件的範例。new 關鍵字會進行如下的操作:

  1. 建立一個空的簡單JavaScript物件(即{});
  2. 連結該物件(即設定該物件的建構函式)到另一個物件 ;
  3. 將步驟1新建立的物件作為this的上下文 ;
  4. 如果該函數沒有返回物件,則返回this。

接下來我們看實現:

function Dog(name, color, age) {
  this.name = name;
  this.color = color;
  this.age = age;}Dog.prototype={
  getName: function() {
    return this.name  }}var dog = new Dog('大黃', 'yellow', 3)

上面的程式碼相信不用解釋,大家都懂。我們來看最後一行帶new關鍵字的程式碼,按照上述的1,2,3,4步來解析new背後的操作。

第一步:建立一個簡單空物件

var obj = {}

第二步:連結該物件到另一個物件(原型鏈)

// 設定原型鏈obj.__proto__ = Dog.prototype

第三步:將步驟1新建立的物件作為 this 的上下文

// this指向obj物件Dog.apply(obj, ['大黃', 'yellow', 3])

第四步:如果該函數沒有返回物件,則返回this

// 因為 Dog() 沒有返回值,所以返回objvar dog = obj
dog.getName() // '大黃'

需要注意的是如果 Dog() 有 return 則返回 return的值

var rtnObj = {}function Dog(name, color, age) {
  // ...
  //返回一個物件
  return rtnObj}var dog = new Dog('大黃', 'yellow', 3)console.log(dog === rtnObj) // true

接下來我們將以上步驟封裝成一個物件範例化方法,即模擬new的操作:

function objectFactory(){
    var obj = {};
    //取得該方法的第一個引數(並刪除第一個引數),該引數是建構函式
    var Constructor = [].shift.apply(arguments);
    //將新物件的內部屬性__proto__指向建構函式的原型,這樣新物件就可以存取原型中的屬性和方法
    obj.__proto__ = Constructor.prototype;
    //取得建構函式的返回值
    var ret = Constructor.apply(obj, arguments);
    //如果返回值是一個物件就返回該物件,否則返回建構函式的一個範例物件
    return typeof ret === "object" ? ret : obj;}

58. 什麼是回撥函數?回撥函數有什麼缺點

回撥函數是一段可執行的程式碼段,它作為一個引數傳遞給其他的程式碼,其作用是在需要的時候方便呼叫這段(回撥函數)程式碼。

在JavaScript中函數也是物件的一種,同樣物件可以作為引數傳遞給函數,因此函數也可以作為引數傳遞給另外一個函數,這個作為引數的函數就是回撥函數。

const btnAdd = document.getElementById('btnAdd');btnAdd.addEventListener('click', function clickCallback(e) {
    // do something useless});

在本例中,我們等待id為btnAdd的元素中的click事件,如果它被單擊,則執行clickCallback函數。回撥函數向某些資料或事件新增一些功能。

回撥函數有一個致命的弱點,就是容易寫出回撥地獄(Callback hell)。假設多個事件存在依賴性:

setTimeout(() => {
    console.log(1)
    setTimeout(() => {
        console.log(2)
        setTimeout(() => {
            console.log(3)
    
        },3000)
    
    },2000)},1000)

這就是典型的回撥地獄,以上程式碼看起來不利於閱讀和維護,事件一旦多起來就更是亂糟糟,所以在es6中提出了Promise和async/await來解決回撥地獄的問題。當然,回撥函數還存在著別的幾個缺點,比如不能使用 try catch 捕獲錯誤,不能直接 return。接下來的兩條就是來解決這些問題的,咱們往下看。

59. Promise是什麼,可以手寫實現一下嗎?

Promise,翻譯過來是承諾,承諾它過一段時間會給你一個結果。從程式設計講Promise 是非同步程式設計的一種解決方案。下面是Promise在MDN的相關說明:

Promise 物件是一個代理物件(代理一個值),被代理的值在Promise物件建立時可能是未知的。它允許你為非同步操作的成功和失敗分別繫結相應的處理方法(handlers)。 這讓非同步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的promise物件。

一個 Promise有以下幾種狀態:

  • pending: 初始狀態,既不是成功,也不是失敗狀態。
  • fulfilled: 意味著操作成功完成。
  • rejected: 意味著操作失敗。

這個承諾一旦從等待狀態變成為其他狀態就永遠不能更改狀態了,也就是說一旦狀態變為 fulfilled/rejected 後,就不能再次改變。 可能光看概念大家不理解Promise,我們舉個簡單的栗子;

假如我有個女朋友,下週一是她生日,我答應她生日給她一個驚喜,那麼從現在開始這個承諾就進入等待狀態,等待下週一的到來,然後狀態改變。如果下週一我如約給了女朋友驚喜,那麼這個承諾的狀態就會由pending切換為fulfilled,表示承諾成功兌現,一旦是這個結果了,就不會再有其他結果,即狀態不會在發生改變;反之如果當天我因為工作太忙加班,把這事給忘了,說好的驚喜沒有兌現,狀態就會由pending切換為rejected,時間不可倒流,所以狀態也不能再發生變化。

上一條我們說過Promise可以解決回撥地獄的問題,沒錯,pending 狀態的 Promise 物件會觸發 fulfilled/rejected 狀態,一旦狀態改變,Promise 物件的 then 方法就會被呼叫;否則就會觸發 catch。我們將上一條回撥地獄的程式碼改寫一下:

new Promise((resolve,reject) => {
     setTimeout(() => {
            console.log(1)
            resolve()
        },1000)
        }).then((res) => {
    setTimeout(() => {
            console.log(2)
        },2000)}).then((res) => {
    setTimeout(() => {
            console.log(3)
        },3000)}).catch((err) => {console.log(err)})

其實Promise也是存在一些缺點的,比如無法取消 Promise,錯誤需要通過回撥函數捕獲。

promise手寫實現,面試夠用版:

function myPromise(constructor){
    let self=this;
    self.status="pending" //定義狀態改變前的初始狀態
    self.value=undefined;//定義狀態為resolved的時候的狀態
    self.reason=undefined;//定義狀態為rejected的時候的狀態
    function resolve(value){
        //兩個==="pending",保證了狀態的改變是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //兩個==="pending",保證了狀態的改變是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕獲構造異常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }}// 定義鏈式呼叫的then方法myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }}

關於Promise還有其他的知識,比如Promise.all()、Promise.race()等的運用,由於篇幅原因就不再做展開,想要深入瞭解的可看下面的文章。

60. Iterator是什麼,有什麼作用?

Iterator是理解第61條的先決知識,也許是我IQ不夠Iterator和Generator看了很多遍還是一知半解,即使當時理解了,過一陣又忘得一乾二淨。。。

Iterator(迭代器)是一種介面,也可以說是一種規範。為各種不同的資料結構提供統一的存取機制。任何資料結構只要部署Iterator介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。

Iterator語法:

const obj = {
    [Symbol.iterator]:function(){}}

[Symbol.iterator]屬性名是固定的寫法,只要擁有了該屬性的物件,就能夠用迭代器的方式進行遍歷。

迭代器的遍歷方法是首先獲得一個迭代器的指標,初始時該指標指向第一條資料之前,接著通過呼叫 next 方法,改變指標的指向,讓其指向下一條資料 每一次的 next 都會返回一個物件,該物件有兩個屬性

  • value 代表想要獲取的資料
  • done 布林值,false表示當前指標指向的資料有值,true表示遍歷已經結束

Iterator 的作用有三個:

  1. 為各種資料結構,提供一個統一的、簡便的存取介面;
  2. 使得資料結構的成員能夠按某種次序排列;
  3. ES6 創造了一種新的遍歷命令for…of迴圈,Iterator 介面主要供for…of消費。

遍歷過程:

  1. 建立一個指標物件,指向當前資料結構的起始位置。也就是說,遍歷器物件本質上,就是一個指標物件。
  2. 第一次呼叫指標物件的next方法,可以將指標指向資料結構的第一個成員。
  3. 第二次呼叫指標物件的next方法,指標就指向資料結構的第二個成員。
  4. 不斷呼叫指標物件的next方法,直到它指向資料結構的結束位置。

每一次呼叫next方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含value和done兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。

let arr = [{num:1},2,3]let it = arr[Symbol.iterator]() 
// 獲取陣列中的迭代器console.log(it.next()) 	
// { value: Object { num: 1 }, done: false }console.log(it.next()) 
	// { value: 2, done: false }console.log(it.next()) 
		// { value: 3, done: false }console.log(it.next())
		 	// { value: undefined, done: true }

61. Generator函數是什麼,有什麼作用?

Generator函數可以說是Iterator介面的具體實現方式。Generator 最大的特點就是可以控制函數的執行。

function *foo(x) {
  let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)}let it = foo(5)console.log(it.next())  
   // => {value: 6, done: false}console.log(it.next(12)) 
   // => {value: 8, done: false}console.log(it.next(13))
    // => {value: 42, done: true}

上面這個範例就是一個Generator函數,我們來分析其執行過程:

  • 首先 Generator 函數呼叫時它會返回一個迭代器
  • 當執行第一次 next 時,傳參會被忽略,並且函數暫停在 yield (x + 1) 處,所以返回 5 + 1 = 6
  • 當執行第二次 next 時,傳入的引數等於上一個 yield 的返回值,如果你不傳參,yield 永遠返回 undefined。此時 let y = 2 * 12,所以第二個 yield 等於 2 * 12 / 3 = 8
  • 當執行第三次 next 時,傳入的引數會傳遞給 z,所以 z = 13, x = 5, y = 24,相加等於 42

Generator 函數一般見到的不多,其實也於他有點繞有關係,並且一般會配合 co 庫去使用。當然,我們可以通過 Generator 函數解決回撥地獄的問題。

62. 什麼是 async/await 及其如何工作,有什麼優缺點?

async/await是一種建立在Promise之上的編寫非同步或非阻塞程式碼的新方法,被普遍認為是 JS非同步操作的最終且最優雅的解決方案。相對於 Promise 和回撥,它的可讀性和簡潔度都更高。畢竟一直then()也很煩。

async 是非同步的意思,而 awaitasync wait的簡寫,即非同步等待。

所以從語意上就很好理解 async 用於宣告一個 function 是非同步的,而await 用於等待一個非同步方法執行完成。

一個函數如果加上 async ,那麼該函數就會返回一個 Promise

async function test() {
  return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}

可以看到輸出的是一個Promise物件。所以,async 函數返回的是一個 Promise 物件,如果在 async 函數中直接 return 一個直接量,async 會把這個直接量通過 PromIse.resolve()封裝成Promise物件返回。

相比於 Promiseasync/await能更好地處理 then 鏈

function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });}function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);}function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);}function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);}

現在分別用 Promiseasync/await來實現這三個步驟的處理。

使用Promise

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
        });}doIt();// step1 with 300// step2 with 500// step3 with 700// result is 900

使用async/await

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);}doIt();

結果和之前的 Promise 實現是一樣的,但是這個程式碼看起來是不是清晰得多,優雅整潔,幾乎跟同步程式碼一樣。

await關鍵字只能在async function中使用。在任何非async function的函數中使用await關鍵字都會丟擲錯誤。await關鍵字在執行下一行程式碼之前等待右側表示式(可能是一個Promise)返回。

優缺點:

async/await的優勢在於處理 then 的呼叫鏈,能夠更清晰準確的寫出程式碼,並且也能優雅地解決回撥地獄問題。當然也存在一些缺點,因為 await 將非同步程式碼改造成了同步程式碼,如果多個非同步程式碼沒有依賴性卻使用了 await 會導致效能上的降低。

參考文章:

「硬核JS」深入瞭解非同步解決方案

以上21~25條就是JavaScript中主要的非同步解決方案了,難度是有的,需要好好揣摩並加以練習。

63. instanceof的原理是什麼,如何實現

instanceof 可以正確的判斷物件的型別,因為內部機制是通過判斷物件的原型鏈中是不是能找到型別的 prototype。

實現 instanceof:

  1. 首先獲取型別的原型
  2. 然後獲得物件的原型
  3. 然後一直迴圈判斷物件的原型是否等於型別的原型,直到物件原型為 null,因為原型鏈最終為 null
function myInstanceof(left, right) {
  let prototype = right.prototype
  left = left.__proto__  while (true) {
    if (left === null || left === undefined)
      return false
    if (prototype === left)
      return true
    left = left.__proto__  }}

64. js 的節流與防抖

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

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

// 函數防抖的實現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);
    }
  };}

詳細資料可以參考:

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

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

《JS 的防抖與節流》

65. 什麼是設計模式?

1. 概念

設計模式是一套被反覆使用的、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。使用設計模式是為了重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的,設計模式使程式碼編制真正工程化,設計模式是軟體工程的基石,如同大廈的一塊塊磚石一樣。

2. 設計原則

  1. S – Single Responsibility Principle 單一職責原則
    • 一個程式只做好一件事
    • 如果功能過於複雜就拆分開,每個部分保持獨立
  2. O – OpenClosed Principle 開放/封閉原則
    • 對擴充套件開放,對修改封閉
    • 增加需求時,擴充套件新程式碼,而非修改已有程式碼
  3. L – Liskov Substitution Principle 里氏替換原則
    • 子類能覆蓋父類別
    • 父類別能出現的地方子類就能出現
  4. I – Interface Segregation Principle 介面隔離原則
    • 保持介面的單一獨立
    • 類似單一職責原則,這裡更關注介面
  5. D – Dependency Inversion Principle 依賴倒轉原則
    • 面向介面程式設計,依賴於抽象而不依賴於具
    • 使用方只關注介面而不關注具體類的實現

3. 設計模式的型別

  1. 結構型模式(Structural Patterns): 通過識別系統中元件間的簡單關係來簡化系統的設計。
  2. 建立型模式(Creational Patterns): 處理物件的建立,根據實際情況使用合適的方式建立物件。常規的物件建立方式可能會導致設計上的問題,或增加設計的複雜度。建立型模式通過以某種方式控制物件的建立來解決問題。
  3. 行為型模式(Behavioral Patterns): 用於識別物件之間常見的互動模式並加以實現,如此,增加了這些互動的靈活性。

66. 9種前端常見的設計模式

1. 外觀模式(Facade Pattern)

外觀模式是最常見的設計模式之一,它為子系統中的一組介面提供一個統一的高層介面,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中複雜邏輯進行抽象,從而提供一個更統一、更簡潔、更易用的API。很多我們常用的框架和庫基本都遵循了外觀設計模式,比如JQuery就把複雜的原生DOM操作進行了抽象和封裝,並消除了瀏覽器之間的相容問題,從而提供了一個更高階更易用的版本。其實在平時工作中我們也會經常用到外觀模式進行開發,只是我們不自知而已。

  1. 相容瀏覽器事件繫結
let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn    }};
  1. 封裝介面
let myEvent = {
    // ...
    stop: e => {
        e.stopPropagation();
        e.preventDefault();
    }};

場景

  • 設計初期,應該要有意識地將不同的兩個層分離,比如經典的三層結構,在資料存取層和業務邏輯層、業務邏輯層和表示層之間建立外觀Facade
  • 在開發階段,子系統往往因為不斷的重構演化而變得越來越複雜,增加外觀Facade可以提供一個簡單的介面,減少他們之間的依賴。
  • 在維護一個遺留的大型系統時,可能這個系統已經很難維護了,這時候使用外觀Facade也是非常合適的,為繫系統開發一個外觀Facade類,為設計粗糙和高度複雜的遺留程式碼提供比較清晰的介面,讓新系統和Facade物件互動,Facade與遺留程式碼互動所有的複雜工作。

優點

  • 減少系統相互依賴。
  • 提高靈活性。
  • 提高了安全性

缺點

  • 不符合開閉原則,如果要改東西很麻煩,繼承重寫都不合適。

2. 代理模式(Proxy Pattern)

是為一個物件提供一個代用品或預留位置,以便控制對它的存取

假設當A 在心情好的時候收到花,小明表白成功的機率有

60%,而當A 在心情差的時候收到花,小明表白的成功率無限趨近於0。 小明跟A 剛剛認識兩天,還無法辨別A 什麼時候心情好。如果不合時宜地把花送給A,花 被直接扔掉的可能性很大,這束花可是小明吃了7 天泡麵換來的。 但是A 的朋友B 卻很瞭解A,所以小明只管把花交給B,B 會監聽A 的心情變化,然後選 擇A 心情好的時候把花轉交給A,程式碼如下:

let Flower = function() {}let xiaoming = {
  sendFlower: function(target) {
    let flower = new Flower()
    target.receiveFlower(flower)
  }}let B = {
  receiveFlower: function(flower) {
    A.listenGoodMood(function() {
      A.receiveFlower(flower)
    })
  }}let A = {
  receiveFlower: function(flower) {
    console.log('收到花'+ flower)
  },
  listenGoodMood: function(fn) {
    setTimeout(function() {
      fn()
    }, 1000)
  }}xiaoming.sendFlower(B)

場景

  • HTML元 素事件代理
<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li></ul><script>
  let ul = document.querySelector('#ul');
  ul.addEventListener('click', event => {
    console.log(event.target);
  });</script>
  • ES6 的 proxy 阮一峰Proxy
  • jQuery.proxy()方法

優點

  • 代理模式能將代理物件與被呼叫物件分離,降低了系統的耦合度。代理模式在使用者端和目標物件之間起到一箇中介作用,這樣可以起到保護目標物件的作用
  • 代理物件可以擴充套件目標物件的功能;通過修改代理物件就可以了,符合開閉原則;

缺點

  • 處理請求速度可能有差別,非直接存取存在開銷

3. 工廠模式(Factory Pattern)

工廠模式定義一個用於建立物件的介面,這個介面由子類決定範例化哪一個類。該模式使一個類的範例化延遲到了子類。而子類可以重寫介面方法以便建立的時候指定自己的物件型別。

class Product {
    constructor(name) {
        this.name = name    }
    init() {
        console.log('init')
    }
    fun() {
        console.log('fun')
    }}class Factory {
    create(name) {
        return new Product(name)
    }}// uselet factory = new Factory()let p = factory.create('p1')p.init()p.fun()

場景

  • 如果你不想讓某個子系統與較大的那個物件之間形成強耦合,而是想執行時從許多子系統中進行挑選的話,那麼工廠模式是一個理想的選擇
  • 將new操作簡單封裝,遇到new的時候就應該考慮是否用工廠模式;
  • 需要依賴具體環境建立不同範例,這些範例都有相同的行為,這時候我們可以使用工廠模式,簡化實現的過程,同時也可以減少每種物件所需的程式碼量,有利於消除物件間的耦合,提供更大的靈活性

優點

  • 建立物件的過程可能很複雜,但我們只需要關心建立結果。
  • 建構函式和建立者分離, 符合「開閉原則」
  • 一個呼叫者想建立一個物件,只要知道其名稱就可以了。
  • 擴充套件性高,如果想增加一個產品,只要擴充套件一個工廠類就可以。

缺點

  • 新增新產品時,需要編寫新的具體產品類,一定程度上增加了系統的複雜度
  • 考慮到系統的可延伸性,需要引入抽象層,在使用者端程式碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度

什麼時候不用

  • 當被應用到錯誤的問題型別上時,這一模式會給應用程式引入大量不必要的複雜性.除非為建立物件提供一個介面是我們編寫的庫或者框架的一個設計上目標,否則我會建議使用明確的構造器,以避免不必要的開銷。
  • 由於物件的建立過程被高效的抽象在一個介面後面的事實,這也會給依賴於這個過程可能會有多複雜的單元測試帶來問題。

4. 單例模式(Singleton Pattern)

顧名思義,單例模式中Class的範例個數最多為1。當需要一個物件去貫穿整個系統執行某些任務時,單例模式就派上了用場。而除此之外的場景儘量避免單例模式的使用,因為單例模式會引入全域性狀態,而一個健康的系統應該避免引入過多的全域性狀態。

實現單例模式需要解決以下幾個問題:

  • 如何確定Class只有一個範例?
  • 如何簡便的存取Class的唯一範例?
  • Class如何控制範例化的過程?
  • 如何將Class的範例個數限制為1?

我們一般通過實現以下兩點來解決上述問題:

  • 隱藏Class的建構函式,避免多次範例化
  • 通過暴露一個 getInstance() 方法來建立/獲取唯一範例

Javascript中單例模式可以通過以下方式實現:

// 單例構造器const FooServiceSingleton = (function () {
  // 隱藏的Class的建構函式
  function FooService() {}

  // 未初始化的單例物件
  let fooService;

  return {
    // 建立/獲取單例物件的函數
    getInstance: function () {
      if (!fooService) {
        fooService = new FooService();
      }
      return fooService;
    }
  }})();

實現的關鍵點有:

  1. 使用 IIFE建立區域性作用域並即時執行;
  2. getInstance()為一個 閉包 ,使用閉包儲存區域性作用域中的單例物件並返回。

我們可以驗證下單例物件是否建立成功:

const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); 
// true

場景例子

  • 定義名稱空間和實現分支型方法
  • 登入框
  • vuex 和 redux中的store

優點

  • 劃分名稱空間,減少全域性變數
  • 增強模組性,把自己的程式碼組織在一個全域性變數名下,放在單一位置,便於維護
  • 且只會範例化一次。簡化了程式碼的偵錯和維護

缺點

  • 由於單例模式提供的是一種單點存取,所以它有可能導致模組間的強耦合
  • 從而不利於單元測試。無法單獨測試一個呼叫了來自單例的方法的類,而只能把它與那個單例作為一
  • 個單元一起測試。

5. 策略模式(Strategy Pattern)

策略模式簡單描述就是:物件有某個行為,但是在不同的場景中,該行為有不同的實現演演算法。把它們一個個封裝起來,並且使它們可以互相替換

<html><head>
    <title>策略模式-校驗表單</title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head><body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        使用者名稱:<input type="text" name="userName">
        密碼:<input type="text" name="password">
        手機號碼:<input type="text" name="phoneNumber">
        <button type="submit">提交</button>
    </form>
    <script type="text/javascript">
        // 策略物件
        const strategies = {
          isNoEmpty: function (value, errorMsg) {
            if (value === '') {
              return errorMsg;
            }
          },
          isNoSpace: function (value, errorMsg) {
            if (value.trim() === '') {
              return errorMsg;
            }
          },
          minLength: function (value, length, errorMsg) {
            if (value.trim().length < length) {
              return errorMsg;
            }
          },
          maxLength: function (value, length, errorMsg) {
            if (value.length > length) {
              return errorMsg;
            }
          },
          isMobile: function (value, errorMsg) {
            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
              return errorMsg;
            }                
          }
        }
        
        // 驗證類
        class Validator {
          constructor() {
            this.cache = []
          }
          add(dom, rules) {
            for(let i = 0, rule; rule = rules[i++];) {
              let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg              this.cache.push(() => {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {
            for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
              let errorMsg = validatorFunc()
              if (errorMsg) {
                return errorMsg              }
            }
          }
        }

        // 呼叫程式碼
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {
          let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '使用者名稱不可為空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不允許以空白字元命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '使用者名稱長度不能小於2位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '密碼長度不能小於6位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '請輸入正確的手機號碼格式'
          }])
          return validator.start()
        }

        registerForm.onsubmit = function() {
          let errorMsg = validataFunc()
          if (errorMsg) {
            alert(errorMsg)
            return false
          }
        }
    </script></body></html>

場景例子

  • 如果在一個系統裡面有許多類,它們之間的區別僅在於它們的’行為’,那麼使用策略模式可以動態地讓一個物件在許多行為中選擇一種行為。
  • 一個系統需要動態地在幾種演演算法中選擇一種。
  • 表單驗證

優點

  • 利用組合、委託、多型等技術和思想,可以有效的避免多重條件選擇語句
  • 提供了對開放-封閉原則的完美支援,將演演算法封裝在獨立的strategy中,使得它們易於切換,理解,易於擴充套件
  • 利用組合和委託來讓Context擁有執行演演算法的能力,這也是繼承的一種更輕便的代替方案

缺點

  • 會在程式中增加許多策略類或者策略物件
  • 要使用策略模式,必須瞭解所有的strategy,必須瞭解各個strategy之間的不同點,這樣才能選擇一個合適的strategy

6. 迭代器模式(Iterator Pattern)

如果你看到這,ES6中的迭代器 Iterator 相信你還是有點印象的,上面第60條已經做過簡單的介紹。迭代器模式簡單的說就是提供一種方法順序一個聚合物件中各個元素,而又不暴露該物件的內部表示。

迭代器模式解決了以下問題:

  • 提供一致的遍歷各種資料結構的方式,而不用瞭解資料的內部結構
  • 提供遍歷容器(集合)的能力而無需改變容器的介面

一個迭代器通常需要實現以下介面:

  • hasNext():判斷迭代是否結束,返回Boolean
  • next():查詢並返回下一個元素

為Javascript的陣列實現一個迭代器可以這麼寫:

const item = [1, 'red', false, 3.14];function Iterator(items) {
  this.items = items;
  this.index = 0;}Iterator.prototype = {
  hasNext: function () {
    return this.index < this.items.length;
  },
  next: function () {
    return this.items[this.index++];
  }}

驗證一下迭代器是否工作:

const iterator = new Iterator(item);while(iterator.hasNext()){
  console.log(iterator.next());}
  /輸出:1, red, false, 3.14

ES6提供了更簡單的迭代迴圈語法 for…of,使用該語法的前提是操作物件需要實現 可迭代協定(The iterable protocol),簡單說就是該物件有個Key為 Symbol.iterator 的方法,該方法返回一個iterator物件。

比如我們實現一個 Range 類用於在某個數位區間進行迭代:

function Range(start, end) {
  return {
    [Symbol.iterator]: function () {
      return {
        next() {
          if (start < end) {
            return { value: start++, done: false };
          }
          return { done: true, value: end };
        }
      }
    }
  }}

驗證一下:

for (num of Range(1, 5)) {
  console.log(num);}// 輸出:1, 2, 3, 4

7. 觀察者模式(Observer Pattern)

觀察者模式又稱釋出-訂閱模式(Publish/Subscribe Pattern),是我們經常接觸到的設計模式,日常生活中的應用也比比皆是,比如你訂閱了某個博主的頻道,當有內容更新時會收到推播;又比如JavaScript中的事件訂閱響應機制。觀察者模式的思想用一句話描述就是:被觀察物件(subject)維護一組觀察者(observer),當被觀察物件狀態改變時,通過呼叫觀察者的某個方法將這些變化通知到觀察者

觀察者模式中Subject物件一般需要實現以下API:

  • subscribe(): 接收一個觀察者observer物件,使其訂閱自己
  • unsubscribe(): 接收一個觀察者observer物件,使其取消訂閱自己
  • fire(): 觸發事件,通知到所有觀察者

用JavaScript手動實現觀察者模式:

// 被觀察者function Subject() {
  this.observers = [];}Subject.prototype = {
  // 訂閱
  subscribe: function (observer) {
    this.observers.push(observer);
  },
  // 取消訂閱
  unsubscribe: function (observerToRemove) {
    this.observers = this.observers.filter(observer => {
      return observer !== observerToRemove;
    })
  },
  // 事件觸發
  fire: function () {
    this.observers.forEach(observer => {
      observer.call();
    });
  }}

驗證一下訂閱是否成功:

const subject = new Subject();function observer1() {
  console.log('Observer 1 Firing!');}function observer2() {
  console.log('Observer 2 Firing!');}subject.subscribe(observer1);subject.subscribe(observer2);subject.fire();
  //輸出:Observer 1 Firing! Observer 2 Firing!

驗證一下取消訂閱是否成功:

subject.unsubscribe(observer2);subject.fire();//輸出:Observer 1 Firing!

場景

  • DOM事件
document.body.addEventListener('click', function() {
    console.log('hello world!');});document.body.click()
  • vue 響應式

優點

  • 支援簡單的廣播通訊,自動通知所有已經訂閱過的物件
  • 目標物件與觀察者之間的抽象耦合關係能單獨擴充套件以及重用
  • 增加了靈活性
  • 觀察者模式所做的工作就是在解耦,讓耦合的雙方都依賴於抽象,而不是依賴於具體。從而使得各自的變化都不會影響到另一邊的變化。

缺點

  • 過度使用會導致物件與物件之間的聯絡弱化,會導致程式難以跟蹤維護和理解

8. 中介者模式(Mediator Pattern)

在中介者模式中,中介者(Mediator)包裝了一系列物件相互作用的方式,使得這些物件不必直接相互作用,而是由中介者協調它們之間的互動,從而使它們可以鬆散偶合。當某些物件之間的作用發生改變時,不會立即影響其他的一些物件之間的作用,保證這些作用可以彼此獨立的變化。

中介者模式和觀察者模式有一定的相似性,都是一對多的關係,也都是集中式通訊,不同的是中介者模式是處理同級物件之間的互動,而觀察者模式是處理Observer和Subject之間的互動。中介者模式有些像婚戀中介,相親物件剛開始並不能直接交流,而是要通過中介去篩選匹配再決定誰和誰見面。

場景

  • 例如購物車需求,存在商品選擇表單、顏色選擇表單、購買數量表單等等,都會觸發change事件,那麼可以通過中介者來轉發處理這些事件,實現各個事件間的解耦,僅僅維護中介者物件即可。
var goods = {   //手機庫存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,};//中介者var mediator = (function() {
    var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {
        changed: function(obj) {
            switch(obj){
                case colorSelect:
                    //TODO
                    break;
                case memorySelect:
                    //TODO
                    break;
                case numSelect:
                    //TODO
                    break;
            }
        }
    }})();colorSelect.onchange = function() {
    mediator.changed(this);};memorySelect.onchange = function() {
    mediator.changed(this);};numSelect.onchange = function() {
    mediator.changed(this);};
  • 聊天室裡

聊天室成員類:

function Member(name) {
  this.name = name;
  this.chatroom = null;}Member.prototype = {
  // 傳送訊息
  send: function (message, toMember) {
    this.chatroom.send(message, this, toMember);
  },
  // 接收訊息
  receive: function (message, fromMember) {
    console.log(`${fromMember.name} to ${this.name}: ${message}`);
  }}

聊天室類:

function Chatroom() {
  this.members = {};}Chatroom.prototype = {
  // 增加成員
  addMember: function (member) {
    this.members[member.name] = member;
    member.chatroom = this;
  },
  // 傳送訊息
  send: function (message, fromMember, toMember) {
    toMember.receive(message, fromMember);
  }}

測試一下:

const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');
chatroom.addMember(bruce);chatroom.addMember(frank);
bruce.send('Hey frank', frank);
//輸出:bruce to frank: hello frank

優點

  • 使各物件之間耦合鬆散,而且可以獨立地改變它們之間的互動
  • 中介者和物件一對多的關係取代了物件之間的網狀多對多的關係
  • 如果物件之間的複雜耦合度導致維護很困難,而且耦合度隨專案變化增速很快,就需要中介者重構程式碼

缺點

  • 系統中會新增一箇中介者物件,因為物件之間互動的複雜性,轉移成了中介者物件的複雜性,使得中介者物件經常是巨大的。中介 者物件自身往往就是一個難以維護的物件。

9. 存取者模式(Visitor Pattern)

存取者模式 是一種將演演算法與物件結構分離的設計模式,通俗點講就是:存取者模式讓我們能夠在不改變一個物件結構的前提下能夠給該物件增加新的邏輯,新增的邏輯儲存在一個獨立的存取者物件中。存取者模式常用於拓展一些第三方的庫和工具。

// 存取者  class Visitor {
    constructor() {}
    visitConcreteElement(ConcreteElement) {
        ConcreteElement.operation()
    }}// 元素類  class ConcreteElement{
    constructor() {
    }
    operation() {
       console.log("ConcreteElement.operation invoked");  
    }
    accept(visitor) {
        visitor.visitConcreteElement(this)
    }}// clientlet visitor = new Visitor()let element = new ConcreteElement()elementA.accept(visitor)

存取者模式的實現有以下幾個要素:

  • Visitor Object:存取者物件,擁有一個visit()方法
  • Receiving Object:接收物件,擁有一個accept() 方法
  • visit(receivingObj):用於Visitor接收一個Receiving Object
  • accept(visitor):用於Receving Object接收一個Visitor,並通過呼叫Visitorvisit() 為其提供獲取Receiving Object資料的能力

簡單的程式碼實現如下:

Receiving Object:function Employee(name, salary) {
  this.name = name;
  this.salary = salary;}Employee.prototype = {
  getSalary: function () {
    return this.salary;
  },
  setSalary: function (salary) {
    this.salary = salary;
  },
  accept: function (visitor) {
    visitor.visit(this);
  }}Visitor Object:function Visitor() { }Visitor.prototype = {
  visit: function (employee) {
    employee.setSalary(employee.getSalary() * 2);
  }}

驗證一下:

const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);console.log(employee.getSalary());
//輸出:2000

場景

  • 物件結構中物件對應的類很少改變,但經常需要在此物件結構上定義新的操作
  • 需要對一個物件結構中的物件進行很多不同的並且不相關的操作,而需要避免讓這些操作"汙染"這些物件的類,也不希望在增加新操作時修改這些類。

優點

  • 符合單一職責原則
  • 優秀的擴充套件性
  • 靈活性

缺點

  • 具體元素對存取者公佈細節,違反了迪米特原則
  • 違反了依賴倒置原則,依賴了具體類,沒有依賴抽象。
  • 具體元素變更比較困難

相關推薦:

以上就是由淺入深詳細整理JavaScript面試知識點的詳細內容,更多請關注TW511.COM其它相關文章!