您好,登錄后才能下訂單哦!
這篇文章主要介紹了JavaScript面試知識點有哪些的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇JavaScript面試知識點有哪些文章都會有所收獲,下面我們一起來看看吧。
瀏覽器內核
盒模型、flex布局、兩/三欄布局、水平/垂直居中;
BFC、清除浮動;
css3動畫、H5新特性。
繼承、原型鏈、this指向、設計模式、call, apply, bind,;
new實現、防抖節流、let, var, const 區別、暫時性死區、event、loop;
promise使用及實現、promise并行執行和順序執行;
async/await的優缺點;
閉包、垃圾回收和內存泄漏、數組方法、數組亂序, 數組扁平化、事件委托、事件監聽、事件模型
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如何配置大型項目
下面進入正題:
具體可看我之前的文章:「前端料包」可能是最透徹的JavaScript數據類型詳解
JavaScript一共有8種數據類型,其中有7種基本數據類型:Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示獨一無二的值)和BigInt(es10新增);
1種引用數據類型——Object(Object本質上是由一組無序的名值對組成的)。里面包含 function、Array、Date等。JavaScript不支持任何創建自定義類型的機制,而所有值最終都將是上述 8 種數據類型之一。
原始數據類型:直接存儲在棧(stack)中,占據空間小、大小固定,屬于被頻繁使用數據,所以放入棧中存儲。
引用數據類型:同時存儲在棧(stack)和堆(heap)中,占據空間大、大小不固定。引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址后從堆中獲得實體。
&&
叫邏輯與,在其操作數中找到第一個虛值表達式并返回它,如果沒有找到任何虛值表達式,則返回最后一個真值表達式。它采用短路來防止不必要的工作。
||
叫邏輯或,在其操作數中找到第一個真值表達式并返回它。這也使用了短路來防止不必要的工作。在支持 ES6 默認函數參數之前,它用于初始化函數中的默認參數值。
!!
運算符可以將右側的值強制轉換為布爾值,這也是將值轉換為布爾值的一種簡單方法。
在 JS 中類型轉換只有三種情況,分別是:
轉換為布爾值(調用Boolean()方法)
轉換為數字(調用Number()、parseInt()和parseFloat()方法)
轉換為字符串(調用.toString()或者String()方法)
null和underfined沒有.toString方法
此外還有一些操作符會存在隱式轉換,此處不做展開,可自行百度00
(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));
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 所有內置對象屬性和方法匯總》
已在作用域中聲明但還沒有賦值的變量,是 undefined。相反,還沒有在作用域中聲明過的變量,是 undeclared 的。
對于 undeclared 變量的引用,瀏覽器會報引用錯誤,如 ReferenceError: b is not defined 。但是我們可以使用 typ eof 的安全防范機制來避免報錯,因為對于 undeclared(或者 not defined )變量,typeof 會返回 “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》
{} 的 valueOf 結果為 {} ,toString 的結果為 "[object Object]"[] 的 valueOf 結果為 [] ,toString 的結果為 ""
作用域: 作用域是定義變量的區域,它有一套訪問變量的規則,這套規則來管理瀏覽器引擎如何在當前作用域以及嵌套的作用域中根據變量(標識符)進行變量查找。
作用域鏈: 作用域鏈的作用是保證對執行環境有權訪問的所有變量和函數的有序訪問,通過作用域鏈,我們可以訪問到外層環境的變量和 函數。
作用域鏈的本質上是一個指向變量對象的指針列表。變量對象是一個包含了執行環境中所有變量和函數的對象。作用域鏈的前 端始終都是當前執行上下文的變量對象。全局執行上下文的變量對象(也就是全局對象)始終是作用域鏈的最后一個對象。
當我們查找一個變量時,如果當前執行環境中沒有找到,我們可以沿著作用域鏈向后查找。
作用域鏈的創建過程跟執行上下文的建立有關…
詳細資料可以參考: 《JavaScript 深入理解之作用域鏈》
也可以看看我的文章:「前端料包」深究JavaScript作用域(鏈)知識點和閉包
我們一般使用字面量的形式直接創建對象,但是這種創建方式對于創建大量相似對象的時候,會產生大量的重復代碼。但 js和一般的面向對象的語言不同,在 ES6 之前它沒有類的概念。但是我們可以使用函數來進行模擬,從而產生出可復用的對象創建方式,我了解到的方式有這么幾種:
第一種是工廠模式,工廠模式的主要工作原理是用函數來封裝創建對象的細節,從而通過調用函數來達到復用的目的。但是它有一個很大的問題就是創建出來的對象無法和某個類型聯系起來,它只是簡單的封裝了復用代碼,而沒有建立起對象和類型間的關系。
第二種是構造函數模式。js 中每一個函數都可以作為構造函數,只要一個函數是通過 new 來調用的,那么我們就可以把它稱為構造函數。執行構造函數首先會創建一個對象,然后將對象的原型指向構造函數的 prototype 屬性,然后將執行上下文中的 this 指向這個對象,最后再執行整個函數,如果返回值不是對象,則返回新建的對象。因為 this 的值指向了新建的對象,因此我們可以使用 this 給對象賦值。構造函數模式相對于工廠模式的優點是,所創建的對象和構造函數建立起了聯系,因此我們可以通過原型來識別對象的類型。但是構造函數存在一個缺點就是,造成了不必要的函數對象的創建,因為在 js 中函數也是一個對象,因此如果對象屬性中如果包含函數的話,那么每次我們都會新建一個函數對象,浪費了不必要的內存空間,因為函數是所有的實例都可以通用的。
第三種模式是原型模式,因為每一個函數都有一個 prototype 屬性,這個屬性是一個對象,它包含了通過構造函數創建的所有實例都能共享的屬性和方法。因此我們可以使用原型對象來添加公用屬性和方法,從而實現代碼的復用。這種方式相對于構造函數模式來說,解決了函數對象的復用問題。但是這種模式也存在一些問題,一個是沒有辦法通過傳入參數來初始化值,另一個是如果存在一個引用類型如 Array 這樣的值,那么所有的實例將共享一個對象,一個實例對引用類型值的改變會影響所有的實例。
第四種模式是組合使用構造函數模式和原型模式,這是創建自定義類型的最常見方式。因為構造函數模式和原型模式分開使用都存在一些問題,因此我們可以組合使用這兩種模式,通過構造函數來初始化對象的屬性,通過原型對象來實現函數方法的復用。這種方法很好的解決了兩種模式單獨使用時的缺點,但是有一點不足的就是,因為使用了兩種不同的模式,所以對于代碼的封裝性不夠好。
第五種模式是動態原型模式,這一種模式將原型方法賦值的創建過程移動到了構造函數的內部,通過對屬性是否存在的判斷,可以實現僅在第一次調用函數時對原型對象賦值一次的效果。這一種方式很好地對上面的混合模式進行了封裝。
第六種模式是寄生構造函數模式,這一種模式和工廠模式的實現基本相同,我對這個模式的理解是,它主要是基于一個已有的類型,在實例化時對實例化的對象進行擴展。這樣既不用修改原來的構造函數,也達到了擴展對象的目的。它的一個缺點和工廠模式一樣,無法實現對象的識別。
嗯我目前了解到的就是這么幾種方式。
詳細資料可以參考: 《JavaScript 深入理解之對象創建》
我了解的 js 中實現繼承的幾種方式有:
第一種是以原型鏈的方式來實現繼承,但是這種實現方式存在的缺點是,在包含有引用類型的數據時,會被所有的實例對象所共享,容易造成修改的混亂。還有就是在創建子類型的時候不能向超類型傳遞參數。
第二種方式是使用借用構造函數的方式,這種方式是通過在子類型的函數中調用超類型的構造函數來實現的,這一種方法解決了不能向超類型傳遞參數的缺點,但是它存在的一個問題就是無法實現函數方法的復用,并且超類型原型定義的方法子類型也沒有辦法訪問到。
第三種方式是組合繼承,組合繼承是將原型鏈和借用構造函數組合起來使用的一種方式。通過借用構造函數的方式來實現類型的屬性的繼承,通過將子類型的原型設置為超類型的實例來實現方法的繼承。這種方式解決了上面的兩種模式單獨使用時的問題,但是由于我們是以超類型的實例來作為子類型的原型,所以調用了兩次超類的構造函數,造成了子類型的原型中多了很多不必要的屬性。
第四種方式是原型式繼承,原型式繼承的主要思路就是基于已有的對象來創建新的對象,實現的原理是,向函數中傳入一個對象,然后返回一個以這個對象為原型的對象。這種繼承的思路主要不是為了實現創造一種新的類型,只是對某個對象實現一種簡單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實現。缺點與原型鏈方式相同。
第五種方式是寄生式繼承,寄生式繼承的思路是創建一個用于封裝繼承過程的函數,通過傳入一個對象,然后復制一個對象的副本,然后對象進行擴展,最后返回這個對象。這個擴展的過程就可以理解是一種繼承。這種繼承的優點就是對一個簡單對象實現繼承,如果這個對象不是我們的自定義類型時。缺點是沒有辦法實現函數的復用
第六種方式是寄生式組合繼承,組合繼承的缺點就是使用超類型的實例做為子類型的原型,導致添加了不必要的原型屬性。寄生式組合繼承的方式是使用超類型的原型的副本來作為子類型的原型,這樣就避免了創建不必要的屬性。(詳細解釋見紅寶書247-248頁)
復制代碼
詳細資料可以參考: 《JavaScript 深入理解之繼承》
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 + "."); };
詳情可看我之前的文章:「前端料包」一文徹底搞懂JavaScript中的this、call、apply和bind
在瀏覽器里,在全局范圍內this 指向window對象;
在函數中,this永遠指向最后調用他的那個對象;
構造函數中,this指向new出來的那個新的對象;
call、apply、bind中的this被強綁定在指定的那個對象上;
箭頭函數中this比較特殊,箭頭函數this為父作用域的this,不是調用時的this.要知道前四種方式,都是調用時確定,也就是動態的,而箭頭函數的this指向是靜態的,聲明的時候就確定了下來;
apply、call、bind都是js給函數內置的一些API,調用他們可以為函數指定this的執行,同時也可以傳參。
在 js 中我們是使用構造函數來新建一個對象的,每一個構造函數的內部都有一個 prototype 屬性值,這個屬性值是一個對 象,這個對象包含了可以由該構造函數的所有實例共享的屬性和方法。當我們使用構造函數新建一個對象后,在這個對象的內部 將包含一個指針,這個指針指向構造函數的 prototype 屬性對應的值,在 ES5 中這個指針被稱為對象的原型。一般來說我們 是不應該能夠獲取到這個值的,但是現在瀏覽器中都實現了 proto 屬性來讓我們訪問這個屬性,但是我們最好不要使用這 個屬性,因為它不是規范中規定的。ES5 中新增了一個 Object.getPrototypeOf() 方法,我們可以通過這個方法來獲取對 象的原型。
當我們訪問一個對象的屬性時,如果這個對象內部不存在這個屬性,那么它就會去它的原型對象里找這個屬性,這個原型對象又 會有自己的原型,于是就這樣一直找下去,也就是原型鏈的概念。原型鏈的盡頭一般來說都是 Object.prototype 所以這就 是我們新建的對象為什么能夠使用 toString() 等方法的原因。
特點:
JavaScript 對象是通過引用來傳遞的,我們創建的每個新對象實體中并沒有一份屬于自己的原型副本。當我們修改原型時,與 之相關的對象也會繼承這一改變。
參考文章:
《JavaScript 深入理解之原型與原型鏈》
也可以看看我寫的:「前端料包」深入理解JavaScript原型和原型鏈
p.proto
p.constructor.prototype
Object.getPrototypeOf§
閉包是指有權訪問另一個函數作用域內變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數,創建的函數可以 訪問到當前函數的局部變量。
閉包有兩個常用的用途。
閉包的第一個用途是使我們在函數外部能夠訪問到函數內部的變量。通過使用閉包,我們可以通過在外部調用閉包函數,從而在外部訪問到函數內部的變量,可以使用這種方法來創建私有變量。
函數的另一個用途是使已經運行結束的函數上下文中的變量對象繼續留在內存中,因為閉包函數保留了這個變量對象的引用,所以這個變量對象不會被回收。
function a(){ var n = 0; function add(){ n++; console.log(n); } return add;}var a1 = a(); //注意,函數名只是一個標識(指向函數的指針),而()才是執行函數;a1(); //1a1(); //2 第二次調用n變量還在內存中
其實閉包的本質就是作用域鏈的一個特殊的應用,只要了解了作用域鏈的創建過程,就能夠理解閉包的實現原理。
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 詳解》
事件 是用戶操作網頁時發生的交互動作或者網頁本身的一些操作,現代瀏覽器一共有三種事件模型。
DOM0級模型: ,這種模型不會傳播,所以沒有事件流的概念,但是現在有的瀏覽器支持以冒泡的方式實現,它可以在網頁中直接定義監聽函數,也可以通過 js屬性來指定監聽函數。這種方式是所有瀏覽器都兼容的。
IE 事件模型: 在該事件模型中,一次事件共有兩個過程,事件處理階段,和事件冒泡階段。事件處理階段會首先執行目標元素綁定的監聽事件。然后是事件冒泡階段,冒泡指的是事件從目標元素冒泡到 document,依次檢查經過的節點是否綁定了事件監聽函數,如果有則執行。這種模型通過 attachEvent 來添加監聽函數,可以添加多個監聽函數,會按順序依次執行。
DOM2 級事件模型: 在該事件模型中,一次事件共有三個過程,第一個過程是事件捕獲階段。捕獲指的是事件從 document 一直向下傳播到目標元素,依次檢查經過的節點是否綁定了事件監聽函數,如果有則執行。后面兩個階段和 IE 事件模型的兩個階段相同。這種事件模型,事件綁定的函數是 addEventListener,其中第三個參數可以指定事件是否在捕獲階段執行。
相關資料:
《一個 DOM 元素綁定多個事件時,先執行冒泡還是捕獲》
事件委托 本質上是利用了瀏覽器事件冒泡的機制。因為事件在冒泡過程中會上傳到父節點,并且父節點可以通過事件對象獲取到 目標節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件,這種方式稱為事件代理。
使用事件代理我們可以不必要為每一個子元素都綁定一個監聽事件,這樣減少了內存上的消耗。并且使用事件代理我們還可以實現事件的動態綁定,比如說新增了一個子節點,我們并不需要單獨地為它添加一個監聽事件,它所發生的事件會交給父元素中的監聽函數來處理。
相關資料:
《JavaScript 事件委托詳解》
當事件發生在DOM元素上時,該事件并不完全發生在那個元素上。在“當事件發生在DOM元素上時,該事件并不完全發生在那個元素上。
事件傳播有三個階段:
捕獲階段–事件從 window 開始,然后向下到每個元素,直到到達目標元素事件或event.target。
目標階段–事件已達到目標元素。
冒泡階段–事件從目標元素冒泡,然后上升到每個元素,直到到達 window。
當事件發生在 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
元素,它將分別在控制臺上打印window
,document
,html
,grandparent
和parent
,這就是事件捕獲。
事件冒泡剛好與事件捕獲相反,當前元素---->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
元素,它將分別在控制臺上打印child
,parent
,grandparent
,html
,document
和window
,這就是事件冒泡。
(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 合集》
//(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}$/
我對 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.readySate==4&&xhr.status==200 || xhr.status==304 ) console.log(xhr.responsetXML)
$.ajax({ type:'post', url:'', async:ture,//async 異步 sync 同步 data:data,//針對post請求 dataType:'jsonp', success:function (msg) { }, error:function (error) { } })
// 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;}
js 的加載、解析和執行會阻塞頁面的渲染過程,因此我們希望 js 腳本能夠盡可能的延遲加載,提高頁面的渲染速度。
我了解到的幾種方式是:
將 js 腳本放在文檔的底部,來使 js 腳本盡可能的在最后來加載執行。
給 js 腳本添加 defer屬性,這個屬性會讓腳本的加載與文檔的解析同步解析,然后在文檔解析完成后再執行這個腳本文件,這樣的話就能使頁面的渲染不被阻塞。多個設置了 defer 屬性的腳本按規范來說最后是順序執行的,但是在一些瀏覽器中可能不是這樣。
給 js 腳本添加 async屬性,這個屬性會使腳本異步加載,不會阻塞頁面的解析過程,但是當腳本加載完成后立即執行 js腳本,這個時候如果文檔沒有解析完成的話同樣會阻塞。多個 async 屬性的腳本的執行順序是不可預測的,一般不會按照代碼的順序依次執行。
動態創建 DOM 標簽的方式,我們可以對文檔的加載事件進行監聽,當文檔加載完成后再動態的創建 script 標簽來引入 js 腳本。
相關資料:
《JS 延遲加載的幾種方式》
《HTML 5 `` async
屬性》
我對模塊的理解是,一個模塊是實現一個特定功能的一組方法。在最開始的時候,js 只實現一些簡單的功能,所以并沒有模塊的概念 ,但隨著程序越來越復雜,代碼的模塊化開發變得越來越重要。
由于函數具有獨立作用域的特點,最原始的寫法是使用函數來作為模塊,幾個函數作為一個模塊,但是這種方式容易造成全局變量的污 染,并且模塊間沒有聯系。
后面提出了對象寫法,通過將函數作為一個對象的方法來實現,這樣解決了直接使用函數作為模塊的一些缺點,但是這種辦法會暴露所 有的所有的模塊成員,外部代碼可以修改內部屬性的值。
現在最常用的是立即執行函數的寫法,通過利用閉包來實現模塊私有作用域的建立,同時不會對全局作用域造成污染。
相關資料: 《淺談模塊化開發》
《Javascript 模塊化編程(一):模塊的寫法》
《前端模塊化:CommonJS,AMD,CMD,ES6》
《Module 的語法》
js 中現在比較成熟的有四種模塊加載方案:
第一種是 CommonJS 方案,它通過 require 來引入模塊,通過 module.exports 定義模塊的輸出接口。這種模塊加載方案是服務器端的解決方案,它是以同步的方式來引入模塊的,因為在服務端文件都存儲在本地磁盤,所以讀取非常快,所以以同步的方式加載沒有問題。但如果是在瀏覽器端,由于模塊的加載是使用網絡請求,因此使用異步加載的方式更加合適。
第二種是 AMD 方案,這種方案采用異步加載的方式來加載模塊,模塊的加載不影響后面語句的執行,所有依賴這個模塊的語句都定義在一個回調函數里,等到加載完成后再執行回調函數。require.js 實現了 AMD 規范。
第三種是 CMD 方案,這種方案和 AMD 方案都是為了解決異步模塊加載的問題,sea.js 實現了 CMD 規范。它和require.js的區別在于模塊定義時對依賴的處理不同和對依賴模塊的執行時機的處理不同。
第四種方案是 ES6 提出的方案,使用 import 和 export 的形式來導入導出模塊。
它們之間的主要區別有兩個方面。
第一個方面是在模塊定義時對依賴的處理不同。AMD推崇依賴前置,在定義模塊的時候就要聲明其依賴的模塊。而 CMD 推崇就近依賴,只有在用到某個模塊的時候再去 require。
第二個方面是對依賴模塊的執行時機處理不同。首先 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 的區別》
1.CommonJS
模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。CommonJS
模塊輸出的是值的
,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令 import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。
2.CommonJS
模塊是運行時加載,ES6 模塊是編譯時輸出接口。CommonJS
模塊就是對象,即在輸入時是先加載整個模塊,生成一個對象,然后再從這個對象上面讀取方法,這種加載稱為“運行時加載”。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
require.js 的核心原理是通過動態創建 script 腳本來異步引入模塊,然后對每個腳本的 load 事件進行監聽,如果每個腳本都加載完成了,再調用回調函數。"
詳細資料可以參考: 《requireJS 的用法和原理分析》
《requireJS 的核心原理是什么?》
《requireJS 原理分析》
JavaScript語言的一大特點就是單線程,即同一時間只能做一件事情。
JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?
所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。
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 渲 染等。
面試中該如何回答呢? 下面是我個人推薦的回答:
首先js 是單線程運行的,在代碼執行的時候,通過將不同函數的執行上下文壓入執行棧中來保證代碼的有序執行。
在執行同步代碼的時候,如果遇到了異步事件,js 引擎并不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務
當同步事件執行完畢后,再將異步事件對應的回調加入到與當前執行棧中不同的另一個任務隊列中等待執行。
任務隊列可以分為宏任務對列和微任務對列,當當前執行棧中的事件執行完畢后,js 引擎首先會判斷微任務對列中是否有任務可以執行,如果有就將微任務隊首的事件壓入棧中執行。
當微任務對列中的任務都執行完成后再去判斷宏任務對列中的任務。
最后可以用下面一道題檢測一下收獲:
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 執行機制》
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;
這會自動將所有參數值放入數組中。
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();
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 引入了增量標記的方法,將一次停頓進行的過程分為了多步,每次執行完一小步就讓運行邏輯執行一會,就這樣交替運行。
1.意外的全局變量
2.被遺忘的計時器或回調函數
3.脫離 DOM 的引用
4.閉包
第一種情況是我們由于使用未聲明的變量,而意外的創建了一個全局變量,而使這個變量一直留在內存中無法被回收。
第二種情況是我們設置了setInterval
定時器,而忘記取消它,如果循環函數有對外部變量的引用的話,那么這個變量會被一直留在內存中,而無法被回收。
第三種情況是我們獲取一個DOM元素的引用,而后面這個元素被刪除,由于我們一直保留了對這個元素的引用,所以它也無法被回收。
第四種情況是不合理的使用閉包,從而導致某些變量一直被留在內存當中。
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。
塊作用域
類
箭頭函數
模板字符串
加強的對象字面
對象解構
Promise
模塊
Symbol
代理(proxy)Set
函數默認參數
rest 和展開
var
,let
和const
的區別是什么?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'}
箭頭函數表達式的語法比函數表達式更簡潔,并且沒有自己的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 對象。
類(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
模板字符串是在 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 版本更整潔。
對象析構是從對象或數組中獲取或提取值的一種新的、更簡潔的方法。假設有如下的對象:
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;
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 的鍵名所指向的對象,不計入垃圾回收機制。
Proxy 用于修改某些操作的默認行為,等同于在語言層面做出修改,所以屬于一種“元編程”,即對編程語言進行編程。
Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
以下47~64條是JavaScript中比較難的高級知識及相關手寫實現,各位看官需慢慢細品
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; } }};
函數式編程(通常縮寫為FP)是通過編寫純函數,避免共享狀態、可變數據、副作用 來構建軟件的過程。數式編程是聲明式 的而不是命令式 的,應用程序的狀態是通過純函數流動的。與面向對象編程形成對比,面向對象中應用程序的狀態通常與對象中的方法共享和共處。
函數式編程是一種編程范式 ,這意味著它是一種基于一些基本的定義原則(如上所列)思考軟件構建的方式。當然,編程范式的其他示例也包括面向對象編程和過程編程。
函數式的代碼往往比命令式或面向對象的代碼更簡潔,更可預測,更容易測試 - 但如果不熟悉它以及與之相關的常見模式,函數式的代碼也可能看起來更密集雜亂,并且 相關文獻對新人來說是不好理解的。
高階函數只是將函數作為參數或返回值的函數。
function higherOrderFunction(param,callback){ return callback(param);}
在JavaScript中,函數不僅擁有一切傳統函數的使用方式(聲明和調用),而且可以做到像簡單值一樣:
賦值(var func = function(){}
)、
傳參(function func(x,callback){callback();}
)、
返回(function(){return function(){}}
),
這樣的函數也稱之為第一級函數(First-class Function
)。不僅如此,JavaScript中的函數還充當了類的構造函數的作用,同時又是一個Function類的實例(instance)。這樣的多重身份讓JavaScript的函數變得非常重要。
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; }}
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 }}
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; }}
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};
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 函數的實現步驟:
判斷調用對象是否為函數,即使我們是定義在函數的原型上的,但是可能出現使用 call 等方式調用的情況。
判斷傳入上下文對象是否存在,如果不存在,則設置為 window 。
將函數作為上下文對象的一個屬性。
判斷參數值是否傳入
使用上下文對象來調用這個方法,并保存返回結果。
刪除剛才新增的屬性
返回結果
// 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) ); };};
// 函數柯里化指的是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。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 專題之函數柯里化》
這個問題如果你在掘金上搜,你可能會搜索到類似下面的回答:
說實話,看第一遍,我是不理解的,我需要去理一遍原型及原型鏈的知識才能理解。所以我覺得MDN對new的解釋更容易理解:
new
運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象的實例。new
關鍵字會進行如下的操作:
創建一個空的簡單JavaScript對象(即{});
鏈接該對象(即設置該對象的構造函數)到另一個對象 ;
將步驟1新創建的對象作為this的上下文 ;
如果該函數沒有返回對象,則返回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;}
回調函數是一段可執行的代碼段,它作為一個參數傳遞給其他的代碼,其作用是在需要的時候方便調用這段(回調函數)代碼。
在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。接下來的兩條就是來解決這些問題的,咱們往下看。
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()等的運用,由于篇幅原因就不再做展開,想要深入了解的可看下面的文章。
Iterator
是什么,有什么作用?Iterator
是理解第61條的先決知識,也許是我IQ不夠Iterator和Generator
看了很多遍還是一知半解,即使當時理解了,過一陣又忘得一干二凈。。。
Iterator(迭代器)是一種接口,也可以說是一種規范。為各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。
Iterator語法:
const obj = { [Symbol.iterator]:function(){}}
[Symbol.iterator]
屬性名是固定的寫法,只要擁有了該屬性的對象,就能夠用迭代器的方式進行遍歷。
迭代器的遍歷方法是首先獲得一個迭代器的指針,初始時該指針指向第一條數據之前,接著通過調用 next 方法,改變指針的指向,讓其指向下一條數據 每一次的 next 都會返回一個對象,該對象有兩個屬性
value 代表想要獲取的數據
done 布爾值,false表示當前指針指向的數據有值,true表示遍歷已經結束
Iterator 的作用有三個:
為各種數據結構,提供一個統一的、簡便的訪問接口;
使得數據結構的成員能夠按某種次序排列;
ES6 創造了一種新的遍歷命令for…of循環,Iterator 接口主要供for…of消費。
遍歷過程:
創建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員。
第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
不斷調用指針對象的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 }
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
函數解決回調地獄的問題。
async/await
及其如何工作,有什么優缺點?async/await
是一種建立在Promise之上的編寫異步或非阻塞代碼的新方法,被普遍認為是 JS異步操作的最終且最優雅的解決方案。相對于 Promise 和回調,它的可讀性和簡潔度都更高。畢竟一直then()也很煩。
async
是異步的意思,而 await
是 async 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對象返回。
相比于 Promise
,async/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);}
現在分別用 Promise
和async/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中主要的異步解決方案了,難度是有的,需要好好揣摩并加以練習。
instanceof 可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype。
實現 instanceof:
首先獲取類型的原型
然后獲得對象的原型
然后一直循環判斷對象的原型是否等于類型的原型,直到對象原型為 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__ }}
函數防抖 是指在事件被觸發 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 的防抖與節流》
設計模式是一套被反復使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是為了重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式于己于他人于系統都是多贏的,設計模式使代碼編制真正工程化,設計模式是軟件工程的基石,如同大廈的一塊塊磚石一樣。
S – Single Responsibility Principle 單一職責原則
一個程序只做好一件事
如果功能過于復雜就拆分開,每個部分保持獨立
O – OpenClosed Principle 開放/封閉原則
對擴展開放,對修改封閉
增加需求時,擴展新代碼,而非修改已有代碼
L – Liskov Substitution Principle 里氏替換原則
子類能覆蓋父類
父類能出現的地方子類就能出現
I – Interface Segregation Principle 接口隔離原則
保持接口的單一獨立
類似單一職責原則,這里更關注接口
D – Dependency Inversion Principle 依賴倒轉原則
面向接口編程,依賴于抽象而不依賴于具
使用方只關注接口而不關注具體類的實現
結構型模式(Structural Patterns): 通過識別系統中組件間的簡單關系來簡化系統的設計。
創建型模式(Creational Patterns): 處理對象的創建,根據實際情況使用合適的方式創建對象。常規的對象創建方式可能會導致設計上的問題,或增加設計的復雜度。創建型模式通過以某種方式控制對象的創建來解決問題。
行為型模式(Behavioral Patterns): 用于識別對象之間常見的交互模式并加以實現,如此,增加了這些交互的靈活性。
外觀模式是最常見的設計模式之一,它為子系統中的一組接口提供一個統一的高層接口,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中復雜邏輯進行抽象,從而提供一個更統一、更簡潔、更易用的API。很多我們常用的框架和庫基本都遵循了外觀設計模式,比如JQuery就把復雜的原生DOM操作進行了抽象和封裝,并消除了瀏覽器之間的兼容問題,從而提供了一個更高級更易用的版本。其實在平時工作中我們也會經常用到外觀模式進行開發,只是我們不自知而已。
兼容瀏覽器事件綁定
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 }};
封裝接口
let myEvent = { // ... stop: e => { e.stopPropagation(); e.preventDefault(); }};
場景
設計初期,應該要有意識地將不同的兩個層分離,比如經典的三層結構,在數據訪問層和業務邏輯層、業務邏輯層和表示層之間建立外觀Facade
在開發階段,子系統往往因為不斷的重構演化而變得越來越復雜,增加外觀Facade可以提供一個簡單的接口,減少他們之間的依賴。
在維護一個遺留的大型系統時,可能這個系統已經很難維護了,這時候使用外觀Facade也是非常合適的,為系系統開發一個外觀Facade類,為設計粗糙和高度復雜的遺留代碼提供比較清晰的接口,讓新系統和Facade對象交互,Facade與遺留代碼交互所有的復雜工作。
優點
減少系統相互依賴。
提高靈活性。
提高了安全性
缺點
不符合開閉原則,如果要改東西很麻煩,繼承重寫都不合適。
是為一個對象提供一個代用品或占位符,以便控制對它的訪問
假設當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()方法
優點
代理模式能將代理對象與被調用對象分離,降低了系統的耦合度。代理模式在客戶端和目標對象之間起到一個中介作用,這樣可以起到保護目標對象的作用
代理對象可以擴展目標對象的功能;通過修改代理對象就可以了,符合開閉原則;
缺點
處理請求速度可能有差別,非直接訪問存在開銷
工廠模式定義一個用于創建對象的接口,這個接口由子類決定實例化哪一個類。該模式使一個類的實例化延遲到了子類。而子類可以重寫接口方法以便創建的時候指定自己的對象類型。
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的時候就應該考慮是否用工廠模式;
需要依賴具體環境創建不同實例,這些實例都有相同的行為,這時候我們可以使用工廠模式,簡化實現的過程,同時也可以減少每種對象所需的代碼量,有利于消除對象間的耦合,提供更大的靈活性
優點
創建對象的過程可能很復雜,但我們只需要關心創建結果。
構造函數和創建者分離, 符合“開閉原則”
一個調用者想創建一個對象,只要知道其名稱就可以了。
擴展性高,如果想增加一個產品,只要擴展一個工廠類就可以。
缺點
添加新產品時,需要編寫新的具體產品類,一定程度上增加了系統的復雜度
考慮到系統的可擴展性,需要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度
什么時候不用
當被應用到錯誤的問題類型上時,這一模式會給應用程序引入大量不必要的復雜性.除非為創建對象提供一個接口是我們編寫的庫或者框架的一個設計上目標,否則我會建議使用明確的構造器,以避免不必要的開銷。
由于對象的創建過程被高效的抽象在一個接口后面的事實,這也會給依賴于這個過程可能會有多復雜的單元測試帶來問題。
顧名思義,單例模式中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; } }})();
實現的關鍵點有:
使用 IIFE創建局部作用域并即時執行;
getInstance()
為一個 閉包 ,使用閉包保存局部作用域中的單例對象并返回。
我們可以驗證下單例對象是否創建成功:
const fooService1 = FooServiceSingleton.getInstance(); const fooService2 = FooServiceSingleton.getInstance(); console.log(fooService1 === fooService2); // true
場景例子
定義命名空間和實現分支型方法
登錄框
vuex 和 redux中的store
優點
劃分命名空間,減少全局變量
增強模塊性,把自己的代碼組織在一個全局變量名下,放在單一位置,便于維護
且只會實例化一次。簡化了代碼的調試和維護
缺點
由于單例模式提供的是一種單點訪問,所以它有可能導致模塊間的強耦合
從而不利于單元測試。無法單獨測試一個調用了來自單例的方法的類,而只能把它與那個單例作為一
個單元一起測試。
策略模式簡單描述就是:對象有某個行為,但是在不同的場景中,該行為有不同的實現算法。把它們一個個封裝起來,并且使它們可以互相替換
<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
如果你看到這,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
觀察者模式又稱發布-訂閱模式(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 響應式
優點
支持簡單的廣播通信,自動通知所有已經訂閱過的對象
目標對象與觀察者之間的抽象耦合關系能單獨擴展以及重用
增加了靈活性
觀察者模式所做的工作就是在解耦,讓耦合的雙方都依賴于抽象,而不是依賴于具體。從而使得各自的變化都不會影響到另一邊的變化。
缺點
過度使用會導致對象與對象之間的聯系弱化,會導致程序難以跟蹤維護和理解
在中介者模式中,中介者(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
優點
使各對象之間耦合松散,而且可以獨立地改變它們之間的交互
中介者和對象一對多的關系取代了對象之間的網狀多對多的關系
如果對象之間的復雜耦合度導致維護很困難,而且耦合度隨項目變化增速很快,就需要中介者重構代碼
缺點
系統中會新增一個中介者對象,因為對象之間交互的復雜性,轉移成了中介者對象的復雜性,使得中介者對象經常是巨大的。中介 者對象自身往往就是一個難以維護的對象。
訪問者模式 是一種將算法與對象結構分離的設計模式,通俗點講就是:訪問者模式讓我們能夠在不改變一個對象結構的前提下能夠給該對象增加新的邏輯,新增的邏輯保存在一個獨立的訪問者對象中。訪問者模式常用于拓展一些第三方的庫和工具。
// 訪問者 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,并通過調用Visitor
的 visit()
為其提供獲取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面試知識點有哪些”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“JavaScript面試知識點有哪些”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。