由淺入深,66條JavaScript面試知識點

前言

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

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

網絡:css

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

性能相關html

  • 圖片優化的方式
  • 500 張圖片,如何實現預加載優化
  • 懶加載具體實現
  • 減小http請求的方式
  • webpack如何配置大型項目

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

下面進入正題:

———— 高能預警分割線⚡—————

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方法

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

4. JS中數據類型的判斷( typeof,instanceof,constructor,Object.prototype.toString.call()

(1)typeof

typeof 對於原始類型來講,除了 null 均可以顯示正確的類型

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object     []數組的數據類型在 typeof 中被解釋爲 object
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object     null 的數據類型被 typeof 解釋爲 object
複製代碼

typeof 對於對象來講,除了函數都會顯示 object,因此說 typeof 並不能準確判斷變量究竟是什麼類型,因此想判斷一個對象的正確類型,這時候能夠考慮使用 instanceof

(2)instanceof

instanceof 能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈中是否是能找到類型的 prototype。

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

能夠看出直接的字面量值判斷數據類型,instanceof能夠精準判斷引用數據類型(Array,Function,Object),而基本數據類型不能被instanceof精準判斷。

咱們來看一下 instanceof 在MDN中的解釋:instanceof 運算符用來測試一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性。其意思就是判斷對象是不是某一數據類型(如Array)的實例,請重點關注一下是判斷一個對象是不是數據類型的實例。在這裏字面量值,2, true ,'str'不是實例,因此判斷值爲false。

(3)constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
複製代碼

這裏有一個坑,若是我建立一個對象,更改它的原型,constructor就會變得不可靠了

function Fn(){};
 
Fn.prototype=new Array();
 
var f=new Fn();
 
console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true 
複製代碼

(4)Object.prototype.toString.call() 使用 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 中實現繼承的幾種方式有:

(1)第一種是以原型鏈的方式來實現繼承,可是這種實現方式存在的缺點是,在包含有引用類型的數據時,會被全部的實例對象所共享,容易形成修改的混亂。還有就是在建立子類型的時候不能向超類型傳遞參數。

(2)第二種方式是使用借用構造函數的方式,這種方式是經過在子類型的函數中調用超類型的構造函數來實現的,這一種方法解決了不能向超類型傳遞參數的缺點,可是它存在的一個問題就是沒法實現函數方法的複用,而且超類型原型定義的方法子類型也沒有辦法訪問到。

(3)第三種方式是組合繼承,組合繼承是將原型鏈和借用構造函數組合起來使用的一種方式。經過借用構造函數的方式來實現類型的屬性的繼承,經過將子類型的原型設置爲超類型的實例來實現方法的繼承。這種方式解決了上面的兩種模式單獨使用時的問題,可是因爲咱們是以超類型的實例來做爲子類型的原型,因此調用了兩次超類的構造函數,形成了子類型的原型中多了不少沒必要要的屬性。

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

(5)第五種方式是寄生式繼承,寄生式繼承的思路是建立一個用於封裝繼承過程的函數,經過傳入一個對象,而後複製一個對象的副本,而後對象進行擴展,最後返回這個對象。這個擴展的過程就能夠理解是一種繼承。這種繼承的優勢就是對一個簡單對象實現繼承,若是這個對象不是咱們的自定義類型時。缺點是沒有辦法實現函數的複用。

(6)第六種方式是寄生式組合繼承,組合繼承的缺點就是使用超類型的實例作爲子類型的原型,致使添加了沒必要要的原型屬性。寄生式組合繼承的方式是使用超類型的原型的副原本做爲子類型的原型,這樣就避免了建立沒必要要的屬性。
複製代碼

詳細資料能夠參考: 《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的執行,同時也能夠傳參。

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(p)

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

閉包是指有權訪問另外一個函數做用域內變量的函數,建立閉包的最多見的方式就是在一個函數內建立另外一個函數,建立的函數能夠 訪問到當前函數的局部變量。

閉包有兩個經常使用的用途。

  • 閉包的第一個用途是使咱們在函數外部可以訪問到函數內部的變量。經過使用閉包,咱們能夠經過在外部調用閉包函數,從而在外部訪問到函數內部的變量,能夠使用這種方法來建立私有變量。
  • 函數的另外一個用途是使已經運行結束的函數上下文中的變量對象繼續留在內存中,由於閉包函數保留了這個變量對象的引用,因此這個變量對象不會被回收。
function a(){
    var n = 0;
    function add(){
       n++;
       console.log(n);
    }
    return add;
}
var a1 = a(); //注意,函數名只是一個標識(指向函數的指針),而()纔是執行函數;
a1();    //1
a1();    //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 結構:

<div class="grandparent">
  <div class="parent">
    <div class="child">1</div>
  </div>
</div>
複製代碼

對應的 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 結構:

<div class="grandparent">
  <div class="parent">
    <div class="child">1</div>
  </div>
</div>
複製代碼

對應的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數組和對象有哪些原生方法,列舉一下

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 通訊,而後根據服務器返回的數據,更新網頁的相應部分,而不用刷新整個頁面的一種方法。

建立步驟:

面試手寫(原生):

//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.readySates==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 <script> 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 語句 的時候才執行對應的模塊,這樣模塊的執行順序就和咱們書寫的順序保持一致了。
// 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 的區別》

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代碼執行過程當中會有不少任務,這些任務總的分紅兩類:

  • 同步任務
  • 異步任務

當咱們打開網站時,網頁的渲染過程就是一大堆同步任務,好比頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,就是異步任務。,咱們用導圖來講明:

咱們解釋一下這張圖:

  • 同步和異步任務分別進入不一樣的執行"場所",同步的進入主線程,異步的進入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 引入了增量標記的方法,將一次停頓進行的過程分爲了多步,每次執行完一小步就讓運行邏輯執行一會,就這樣交替運行。
複製代碼

相關資料:

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

《JavaScript 中的垃圾回收》

37. 哪些操做會形成內存泄漏?

  • 1.意外的全局變量
  • 2.被遺忘的計時器或回調函數
  • 3.脫離 DOM 的引用
  • 4.閉包
  • 第一種狀況是咱們因爲使用未聲明的變量,而意外的建立了一個全局變量,而使這個變量一直留在內存中沒法被回收。
  • 第二種狀況是咱們設置了setInterval定時器,而忘記取消它,若是循環函數有對外部變量的引用的話,那麼這個變量會被一直留在內存中,而沒法被回收。
  • 第三種狀況是咱們獲取一個DOM元素的引用,然後面這個元素被刪除,因爲咱們一直保留了對這個元素的引用,因此它也沒法被回收。
  • 第四種狀況是不合理的使用閉包,從而致使某些變量一直被留在內存當中。

相關資料:

《JavaScript 內存泄漏教程》

《4 類 JavaScript 內存泄漏及如何避免》

《杜絕 js 中四種內存泄漏類型的發生》

《javascript 典型內存泄漏及 chrome 的排查方法》

如下38~46條是ECMAScript 2015(ES6)中常考的基礎知識點

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 100

let b = 10;
console.log(b,window.b);    // 10 undefined

const 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); // 100
console.log(b)  // 報錯:b is not defined ===> 找不到b這個變量

-------------------------------------------------------------

if(1){
  var a = 100;
  const c = 1;
}
console.log(a); // 100
console.log(c)  // 報錯:c is not defined ===> 找不到c這個變量
複製代碼

同一做用域下let和const不能聲明同名變量,而var能夠

var a = 100;
console.log(a); // 100

var 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

/* * &emsp;&emsp;一、一旦聲明必須賦值,不能使用null佔位。 * * &emsp;&emsp;二、聲明後不能再修改 * * &emsp;&emsp;三、若是聲明的是複合類型數據,能夠修改其屬性 * * */

const a = 100; 

const list = [];
list[0] = 10;
console.log(list);&emsp;&emsp;// [10]

const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj);&emsp;&emsp;// {a:10000,name:'apple'}
複製代碼

41. 什麼是箭頭函數?

箭頭函數表達式的語法比函數表達式更簡潔,而且沒有本身的this,arguments,super或new.target。箭頭函數表達式更適用於那些原本須要匿名函數的地方,而且它不能用做構造函數。

//ES5 Version
var getCurrentDate = function (){
  return new Date();
}

//ES6 Version
const getCurrentDate = () => new Date();
複製代碼

在本例中,ES5 版本中有function(){}聲明和return關鍵字,這兩個關鍵字分別是建立函數和返回值所須要的。在箭頭函數版本中,咱們只須要()括號,不須要 return 語句,由於若是咱們只有一個表達式或值須要返回,箭頭函數就會有一個隱式的返回。

//ES5 Version
function greet(name) {
  return 'Hello ' + name + '!';
}

//ES6 Version
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;
複製代碼

咱們還能夠在箭頭函數中使用與函數表達式和函數聲明相同的參數。若是咱們在一個箭頭函數中有一個參數,則能夠省略括號。

const getArgs = () => arguments

const 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 Version
Employee.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 Version
class 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); // true
console.log(s.toString === Object.prototype.toString); // true
複製代碼

相關資料:

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

《Class 的基本語法》

43. 什麼是模板字符串?

模板字符串是在 JS 中建立字符串的一種新方法。咱們能夠經過使用反引號使模板字符串化。

//ES5 Version
var greet = 'Hi I\'m Mark';

//ES6 Version
let greet = `Hi I'm Mark`;
複製代碼

在 ES5 中咱們須要使用一些轉義字符來達到多行的效果,在模板字符串不須要這麼麻煩:

//ES5 Version
var lastWords = '\n'
  + ' I \n'
  + ' Am \n'
  + 'Iron Man \n';


//ES6 Version
let lastWords = ` I Am Iron Man `;
複製代碼

在ES5版本中,咱們須要添加\n以在字符串中添加新行。在模板字符串中,咱們不須要這樣作。

//ES5 Version
function greet(name) {
  return 'Hello ' + name + '!';
}


//ES6 Version
function 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") // true
set2.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 ? 0 : 1, 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
};
複製代碼

參考文章:

如何寫出一個驚豔面試官的深拷貝

深拷貝的終極探索(99%的人都不知道)

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)
    );
  };
};
複製代碼

參考文章: 《手寫 call、apply 及 bind 函數》

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

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操做符的實現

這個問題若是你在掘金上搜,你可能會搜索到相似下面的回答:

說實話,看第一遍,我是不理解的,我須要去理一遍原型及原型鏈的知識才能理解。因此我以爲 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() 沒有返回值,因此返回obj
var 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()等的運用,因爲篇幅緣由就再也不作展開,想要深刻了解的可看下面的文章。

相關資料:

「硬核JS」深刻了解異步解決方案

【翻譯】Promises/A+規範

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

Iterator是理解第24條的先決知識,也許是我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)

外觀模式是最多見的設計模式之一,它爲子系統中的一組接口提供一個統一的高層接口,使子系統更容易使用。簡而言以外觀設計模式就是把多個子系統中複雜邏輯進行抽象,從而提供一個更統1、更簡潔、更易用的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>
複製代碼

優勢

  • 代理模式能將代理對象與被調用對象分離,下降了系統的耦合度。代理模式在客戶端和目標對象之間起到一箇中介做用,這樣能夠起到保護目標對象的做用

  • 代理對象能夠擴展目標對象的功能;經過修改代理對象就能夠了,符合開閉原則;

缺點

  • 處理請求速度可能有差異,非直接訪問存在開銷

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

// use
let 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)
    }
}
// client
let 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 Objectfunction 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 Objectfunction 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設計模式es6(23種)

《前端面試之道》

《JavaScript 設計模式》

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

💕看完三件事:

  1. 點贊 | 你能夠點擊——>收藏——>退出一鼓作氣,但別忘了點贊🤭
  2. 關注 | 點個關注,下次不迷路😘
  3. 也能夠到GitHub拿我全部文章源文件🤗

後話

以上66條即是這幾天覆盤的總結,總體上是按照由淺入深的順序來的,小部份內容並不是原創,相關的參考我都有在每條的末尾貼了連接,在這裏要特別感謝各路大佬的博客,給了我不少幫助~

前端是個大雜燴,各類框架層出不窮,但萬變不離JS,務實基礎纔是根本,若是你以爲本文對你有所幫助,點個贊支持一下吧~

相關文章
相關標籤/搜索