您好,登錄后才能下訂單哦!
這篇文章給大家介紹66條JavaScript面試知識點(diǎn)和答案解析,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
今天主題是標(biāo)題所寫的66條JavaScript知識點(diǎn)
HTML&CSS:
瀏覽器內(nèi)核
盒模型、flex布局、兩/三欄布局、水平/垂直居中;
BFC、清除浮動;
css3動畫、H5新特性。
JavaScript:
繼承、原型鏈、this指向、設(shè)計(jì)模式、call, apply, bind,;
new實(shí)現(xiàn)、防抖節(jié)流、let, var, const 區(qū)別、暫時(shí)性死區(qū)、event、loop;
promise使用及實(shí)現(xiàn)、promise并行執(zhí)行和順序執(zhí)行;
async/await的優(yōu)缺點(diǎn);
閉包、垃圾回收和內(nèi)存泄漏、數(shù)組方法、數(shù)組亂序, 數(shù)組扁平化、事件委托、事件監(jiān)聽、事件模型
Vue:
vue數(shù)據(jù)雙向綁定原理;
vue computed原理、computed和watch的區(qū)別;
vue編譯器結(jié)構(gòu)圖、生命周期、vue組件通信;
mvvm模式、mvc模式理解;
vue dom diff、vuex、vue-router
網(wǎng)絡(luò):
HTTP1, HTTP2, HTTPS、常見的http狀態(tài)碼;
瀏覽從輸入網(wǎng)址到回車發(fā)生了什么;
前端安全(CSRF、XSS)
前端跨域、瀏覽器緩存、cookie, session, token, localstorage, sessionstorage;
TCP連接(三次握手, 四次揮手)
性能相關(guān)
圖片優(yōu)化的方式
500 張圖片,如何實(shí)現(xiàn)預(yù)加載優(yōu)化
懶加載具體實(shí)現(xiàn)
減少http請求的方式
另外更全面的面試題集我也在整理中,先給個(gè)預(yù)告圖:
下面進(jìn)入正題:
1. 介紹一下 js 的數(shù)據(jù)類型有哪些,值是如何存儲的
2. && 、 ||和!! 運(yùn)算符分別能做什么
3. js的數(shù)據(jù)類型的轉(zhuǎn)換
4. JS中數(shù)據(jù)類型的判斷( typeof,instanceof,constructor,Object.prototype.toString.call()
5. 介紹 js 有哪些內(nèi)置對象?
6. undefined 與 undeclared 的區(qū)別?
7. null 和 undefined 的區(qū)別?
8. {} 和 [] 的 valueOf 和 toString 的結(jié)果是什么?
9. Javascript 的作用域和作用域鏈?
10. javascript 創(chuàng)建對象的幾種方式?
11. JavaScript 繼承的幾種實(shí)現(xiàn)方式?
12. 寄生式組合繼承的實(shí)現(xiàn)?
13. 談?wù)勀銓his、call、apply和bind的理解
14. JavaScript 原型,原型鏈?有什么特點(diǎn)?
15. js 獲取原型的方法?
16. 什么是閉包,為什么要用它?
17. 什么是 DOM 和 BOM?
18. 三種事件模型是什么?
19. 事件委托是什么?
20. 什么是事件傳播?
21. 什么是事件捕獲?
22. 什么是事件冒泡?
23. DOM 操作——怎樣添加、移除、移動、復(fù)制、創(chuàng)建和查找節(jié)點(diǎn)?
24. js數(shù)組和對象有哪些原生方法,列舉一下
25. 常用的正則表達(dá)式
26. Ajax 是什么? 如何創(chuàng)建一個(gè) Ajax?
27. js 延遲加載的方式有哪些?
28. 談?wù)勀銓δK化開發(fā)的理解?
29. js 的幾種模塊規(guī)范?
30. AMD和CMD 規(guī)范的區(qū)別?
31. ES6 模塊與 CommonJS 模塊、AMD、CMD 的差異。
32. requireJS的核心原理是什么?
33. 談?wù)凧S的運(yùn)行機(jī)制
34. arguments 的對象是什么?
35. 為什么在調(diào)用這個(gè)函數(shù)時(shí),代碼中的`b`會變成一個(gè)全局變量?
36.簡單介紹一下V8引擎的垃圾回收機(jī)制
37. 哪些操作會造成內(nèi)存泄漏?
38. ECMAScript 是什么?
39. ECMAScript 2015(ES6)有哪些新特性?
40. `var`,`let`和`const`的區(qū)別是什么?
41. 什么是箭頭函數(shù)?
42. 什么是類?
43. 什么是模板字符串?
44. 什么是對象解構(gòu)?
45 什么是`Set`對象,它是如何工作的?
46. 什么是Proxy?
————
高能預(yù)警分割線?—————
47. 寫一個(gè)通用的事件偵聽器函數(shù),為什么要用它?
48. 什么是函數(shù)式編程? JavaScript的哪些特性使其成為函數(shù)式語言的候選語言?
49. 什么是高階函數(shù)?
50. 為什么函數(shù)被稱為一等公民?
51. 手動實(shí)現(xiàn)`Array.prototype.map 方法`
52. 手動實(shí)現(xiàn)`Array.prototype.filter`方法
53. 手動實(shí)現(xiàn)`Array.prototype.reduce`方法
54. js的深淺拷貝
55. 手寫call、apply及bind函數(shù)
56. 函數(shù)柯里化的實(shí)現(xiàn)
57. js模擬new操作符的實(shí)現(xiàn)
58. 什么是回調(diào)函數(shù)?回調(diào)函數(shù)有什么缺點(diǎn)
59. Promise是什么,可以手寫實(shí)現(xiàn)一下嗎?
60. `Iterator`是什么,有什么作用?
61. `Generator`函數(shù)是什么,有什么作用?
62. 什么是 `async/await`及其如何工作,有什么優(yōu)缺點(diǎn)?
63. instanceof的原理是什么,如何實(shí)現(xiàn)
64. js的節(jié)流與防抖
65. 什么是設(shè)計(jì)模式?
66. 9種前端常見的設(shè)計(jì)模式
1. 介紹一下 js 的數(shù)據(jù)類型有哪些,值是如何存儲的
具體可看我之前的文章:「前端料包」可能是最透徹的JavaScript數(shù)據(jù)類型詳解
JavaScript一共有8種數(shù)據(jù)類型,其中有7種基本數(shù)據(jù)類型:Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示獨(dú)一無二的值)和BigInt(es10新增);
1種引用數(shù)據(jù)類型——Object(Object本質(zhì)上是由一組無序的名值對組成的)。里面包含 function、Array、Date等。JavaScript不支持任何創(chuàng)建自定義類型的機(jī)制,而所有值最終都將是上述 8 種數(shù)據(jù)類型之一。
原始數(shù)據(jù)類型:直接存儲在棧(stack)中,占據(jù)空間小、大小固定,屬于被頻繁使用數(shù)據(jù),所以放入棧中存儲。
引用數(shù)據(jù)類型:同時(shí)存儲在棧(stack)和堆(heap)中,占據(jù)空間大、大小不固定。引用數(shù)據(jù)類型在棧中存儲了指針,該指針指向堆中該實(shí)體的起始地址。當(dāng)解釋器尋找引用值時(shí),會首先檢索其在棧中的地址,取得地址后從堆中獲得實(shí)體。
2. && 、 ||和!! 運(yùn)算符分別能做什么
&& 叫邏輯與,在其操作數(shù)中找到第一個(gè)虛值表達(dá)式并返回它,如果沒有找到任何虛值表達(dá)式,則返回最后一個(gè)真值表達(dá)式。它采用短路來防止不必要的工作。
|| 叫邏輯或,在其操作數(shù)中找到第一個(gè)真值表達(dá)式并返回它。這也使用了短路來防止不必要的工作。在支持 ES6 默認(rèn)函數(shù)參數(shù)之前,它用于初始化函數(shù)中的默認(rèn)參數(shù)值。
!! 運(yùn)算符可以將右側(cè)的值強(qiáng)制轉(zhuǎn)換為布爾值,這也是將值轉(zhuǎn)換為布爾值的一種簡單方法。
3. js的數(shù)據(jù)類型的轉(zhuǎn)換
在 JS 中類型轉(zhuǎn)換只有三種情況,分別是:
轉(zhuǎn)換為布爾值(調(diào)用Boolean()方法)
轉(zhuǎn)換為數(shù)字(調(diào)用Number()、parseInt()和parseFloat()方法)
轉(zhuǎn)換為字符串(調(diào)用.toString()或者String()方法)
null和underfined沒有.toString方法
此外還有一些操作符會存在隱式轉(zhuǎn)換,此處不做展開,可自行百度00
4. JS中數(shù)據(jù)類型的判斷( 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 []數(shù)組的數(shù)據(jù)類型在 typeof 中被解釋為 object console.log(typeof function(){}); // function console.log(typeof {}); // object console.log(typeof undefined); // undefined console.log(typeof null); // object null 的數(shù)據(jù)類型被 typeof 解釋為 object
typeof 對于對象來說,除了函數(shù)都會顯示 object,所以說 typeof 并不能準(zhǔn)確判斷變量到底是什么類型,所以想判斷一個(gè)對象的正確類型,這時(shí)候可以考慮使用 instanceof
(2)instanceof
instanceof 可以正確的判斷對象的類型,因?yàn)閮?nèi)部機(jī)制是通過判斷對象的原型鏈中是不是能找到類型的 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);
可以看出直接的字面量值判斷數(shù)據(jù)類型,instanceof可以精準(zhǔn)判斷引用數(shù)據(jù)類型(Array,F(xiàn)unction,Object),而基本數(shù)據(jù)類型不能被instanceof精準(zhǔn)判斷。
我們來看一下 instanceof 在MDN中的解釋:instanceof 運(yùn)算符用來測試一個(gè)對象在其原型鏈中是否存在一個(gè)構(gòu)造函數(shù)的 prototype 屬性。其意思就是判斷對象是否是某一數(shù)據(jù)類型(如Array)的實(shí)例,請重點(diǎn)關(guān)注一下是判斷一個(gè)對象是否是數(shù)據(jù)類型的實(shí)例。在這里字面量值,2, true ,'str'不是實(shí)例,所以判斷值為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 這里有一個(gè)坑,如果我創(chuàng)建一個(gè)對象,更改它的原型,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 進(jìn)行貍貓換太子,借用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 有哪些內(nèi)置對象?
涉及知識點(diǎn):
全局的對象( global objects )或稱標(biāo)準(zhǔn)內(nèi)置對象,不要和 "全局對象(global object)" 混淆。這里說的全局的對象是說在 全局作用域里的對象。全局作用域中的其他對象可以由用戶的腳本創(chuàng)建或由宿主程序提供。 標(biāo)準(zhǔn)內(nèi)置對象的分類 (1)值屬性,這些全局屬性返回一個(gè)簡單值,這些值沒有自己的屬性和方法。 例如 Infinity、NaN、undefined、null 字面量 (2)函數(shù)屬性,全局函數(shù)可以直接調(diào)用,不需要在調(diào)用時(shí)指定所屬對象,執(zhí)行結(jié)束后會將結(jié)果直接返回給調(diào)用者。 例如 eval()、parseFloat()、parseInt() 等 (3)基本對象,基本對象是定義或使用其他對象的基礎(chǔ)?;緦ο蟀ㄒ话銓ο?、函數(shù)對象和錯(cuò)誤對象。 例如 Object、Function、Boolean、Symbol、Error 等 (4)數(shù)字和日期對象,用來表示數(shù)字、日期和執(zhí)行數(shù)學(xué)計(jì)算的對象。 例如 Number、Math、Date (5)字符串,用來表示和操作字符串的對象。 例如 String、RegExp (6)可索引的集合對象,這些對象表示按照索引值來排序的數(shù)據(jù)集合,包括數(shù)組和類型數(shù)組,以及類數(shù)組結(jié)構(gòu)的對象。例如 Array (7)使用鍵的集合對象,這些集合對象在存儲數(shù)據(jù)時(shí)會使用到鍵,支持按照插入順序來迭代元素。 例如 Map、Set、WeakMap、WeakSet (8)矢量集合,SIMD 矢量集合中的數(shù)據(jù)會被組織為一個(gè)數(shù)據(jù)序列。 例如 SIMD 等 (9)結(jié)構(gòu)化數(shù)據(jù),這些對象用來表示和操作結(jié)構(gòu)化的緩沖區(qū)數(shù)據(jù),或使用 JSON 編碼的數(shù)據(jù)。 例如 JSON 等 (10)控制抽象對象 例如 Promise、Generator 等 (11)反射 例如 Reflect、Proxy (12)國際化,為了支持多語言處理而加入 ECMAScript 的對象。 例如 Intl、Intl.Collator 等 (13)WebAssembly (14)其他 例如 arguments
js 中的內(nèi)置對象主要指的是在程序執(zhí)行前存在全局作用域里的由 js
定義的一些全局值屬性、函數(shù)和用來實(shí)例化其他對象的構(gòu)造函
數(shù)對象。一般我們經(jīng)常用到的如全局變量值 NaN、undefined,全局函數(shù)如 parseInt()、parseFloat() 用來實(shí)例化對象的構(gòu)
造函數(shù)如 Date、Object 等,還有提供數(shù)學(xué)計(jì)算的單體內(nèi)置對象如 Math 對象。
詳細(xì)資料可以參考:
《標(biāo)準(zhǔn)內(nèi)置對象的分類》
《JS 所有內(nèi)置對象屬性和方法匯總》
6. undefined 與 undeclared 的區(qū)別?
已在作用域中聲明但還沒有賦值的變量,是 undefined 的。相反,還沒有在作用域中聲明過的變量,是 undeclared 的。
對于 undeclared 變量的引用,瀏覽器會報(bào)引用錯(cuò)誤,如 ReferenceError: b is not defined 。但是我們可以使用 typ
eof 的安全防范機(jī)制來避免報(bào)錯(cuò),因?yàn)閷τ?undeclared(或者 not defined )變量,typeof 會返回 "undefined"。
7. null 和 undefined 的區(qū)別?
首先 Undefined 和 Null 都是基本數(shù)據(jù)類型,這兩個(gè)基本數(shù)據(jù)類型分別都只有一個(gè)值,就是 undefined 和 null。
undefined 代表的含義是未定義,
null 代表的含義是空對象。一般變量聲明了但還沒有定義的時(shí)候會返回 undefined,null
主要用于賦值給一些可能會返回對象的變量,作為初始化。
undefined 在 js 中不是一個(gè)保留字,這意味著我們可以使用 undefined 來作為一個(gè)變量名,這樣的做法是非常危險(xiǎn)的,它
會影響我們對 undefined 值的判斷。但是我們可以通過一些方法獲得安全的 undefined 值,比如說 void 0。
當(dāng)我們對兩種類型使用 typeof 進(jìn)行判斷的時(shí)候,Null 類型化會返回 “object”,這是一個(gè)歷史遺留的問題。當(dāng)我們使用雙等
號對兩種類型的值進(jìn)行比較時(shí)會返回 true,使用三個(gè)等號時(shí)會返回 false。
詳細(xì)資料可以參考:
《JavaScript 深入理解之 undefined 與 null》
8. {} 和 [] 的 valueOf 和 toString 的結(jié)果是什么?
{} 的 valueOf 結(jié)果為 {} ,toString 的結(jié)果為 "[object Object]" [] 的 valueOf 結(jié)果為 [] ,toString 的結(jié)果為 ""
9. Javascript 的作用域和作用域鏈
作用域: 作用域是定義變量的區(qū)域,它有一套訪問變量的規(guī)則,這套規(guī)則來管理瀏覽器引擎如何在當(dāng)前作用域以及嵌套的作用域中根據(jù)變量(標(biāo)識符)進(jìn)行變量查找。
作用域鏈: 作用域鏈的作用是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問,通過作用域鏈,我們可以訪問到外層環(huán)境的變量和
函數(shù)。
作用域鏈的本質(zhì)上是一個(gè)指向變量對象的指針列表。變量對象是一個(gè)包含了執(zhí)行環(huán)境中所有變量和函數(shù)的對象。作用域鏈的前
端始終都是當(dāng)前執(zhí)行上下文的變量對象。全局執(zhí)行上下文的變量對象(也就是全局對象)始終是作用域鏈的最后一個(gè)對象。
當(dāng)我們查找一個(gè)變量時(shí),如果當(dāng)前執(zhí)行環(huán)境中沒有找到,我們可以沿著作用域鏈向后查找。
作用域鏈的創(chuàng)建過程跟執(zhí)行上下文的建立有關(guān)….
詳細(xì)資料可以參考:
《JavaScript 深入理解之作用域鏈》
也可以看看我的文章:「前端料包」深究JavaScript作用域(鏈)知識點(diǎn)和閉包
10. javascript 創(chuàng)建對象的幾種方式?
我們一般使用字面量的形式直接創(chuàng)建對象,但是這種創(chuàng)建方式對于創(chuàng)建大量相似對象的時(shí)候,會產(chǎn)生大量的重復(fù)代碼。但 js 和一般的面向?qū)ο蟮恼Z言不同,在 ES6 之前它沒有類的概念。但是我們可以使用函數(shù)來進(jìn)行模擬,從而產(chǎn)生出可復(fù)用的對象 創(chuàng)建方式,我了解到的方式有這么幾種: (1)第一種是工廠模式,工廠模式的主要工作原理是用函數(shù)來封裝創(chuàng)建對象的細(xì)節(jié),從而通過調(diào)用函數(shù)來達(dá)到復(fù)用的目的。但是它有一個(gè)很大的問題就是創(chuàng)建出來的對象無法和某個(gè)類型聯(lián)系起來,它只是簡單的封裝了復(fù)用代碼,而沒有建立起對象和類型間的關(guān)系。 (2)第二種是構(gòu)造函數(shù)模式。js 中每一個(gè)函數(shù)都可以作為構(gòu)造函數(shù),只要一個(gè)函數(shù)是通過 new 來調(diào)用的,那么我們就可以把它稱為構(gòu)造函數(shù)。執(zhí)行構(gòu)造函數(shù)首先會創(chuàng)建一個(gè)對象,然后將對象的原型指向構(gòu)造函數(shù)的 prototype 屬性,然后將執(zhí)行上下文中的 this 指向這個(gè)對象,最后再執(zhí)行整個(gè)函數(shù),如果返回值不是對象,則返回新建的對象。因?yàn)?nbsp;this 的值指向了新建的對象,因此我們可以使用 this 給對象賦值。構(gòu)造函數(shù)模式相對于工廠模式的優(yōu)點(diǎn)是,所創(chuàng)建的對象和構(gòu)造函數(shù)建立起了聯(lián)系,因此我們可以通過原型來識別對象的類型。但是構(gòu)造函數(shù)存在一個(gè)缺點(diǎn)就是,造成了不必要的函數(shù)對象的創(chuàng)建,因?yàn)樵?nbsp;js 中函數(shù)也是一個(gè)對象,因此如果對象屬性中如果包含函數(shù)的話,那么每次我們都會新建一個(gè)函數(shù)對象,浪費(fèi)了不必要的內(nèi)存空間,因?yàn)楹瘮?shù)是所有的實(shí)例都可以通用的。 (3)第三種模式是原型模式,因?yàn)槊恳粋€(gè)函數(shù)都有一個(gè) prototype 屬性,這個(gè)屬性是一個(gè)對象,它包含了通過構(gòu)造函數(shù)創(chuàng)建的所有實(shí)例都能共享的屬性和方法。因此我們可以使用原型對象來添加公用屬性和方法,從而實(shí)現(xiàn)代碼的復(fù)用。這種方式相對于構(gòu)造函數(shù)模式來說,解決了函數(shù)對象的復(fù)用問題。但是這種模式也存在一些問題,一個(gè)是沒有辦法通過傳入?yún)?shù)來初始化值,另一個(gè)是如果存在一個(gè)引用類型如 Array 這樣的值,那么所有的實(shí)例將共享一個(gè)對象,一個(gè)實(shí)例對引用類型值的改變會影響所有的實(shí)例。 (4)第四種模式是組合使用構(gòu)造函數(shù)模式和原型模式,這是創(chuàng)建自定義類型的最常見方式。因?yàn)闃?gòu)造函數(shù)模式和原型模式分開使用都存在一些問題,因此我們可以組合使用這兩種模式,通過構(gòu)造函數(shù)來初始化對象的屬性,通過原型對象來實(shí)現(xiàn)函數(shù)方法的復(fù)用。這種方法很好的解決了兩種模式單獨(dú)使用時(shí)的缺點(diǎn),但是有一點(diǎn)不足的就是,因?yàn)槭褂昧藘煞N不同的模式,所以對于代碼的封裝性不夠好。 (5)第五種模式是動態(tài)原型模式,這一種模式將原型方法賦值的創(chuàng)建過程移動到了構(gòu)造函數(shù)的內(nèi)部,通過對屬性是否存在的判斷,可以實(shí)現(xiàn)僅在第一次調(diào)用函數(shù)時(shí)對原型對象賦值一次的效果。這一種方式很好地對上面的混合模式進(jìn)行了封裝。 (6)第六種模式是寄生構(gòu)造函數(shù)模式,這一種模式和工廠模式的實(shí)現(xiàn)基本相同,我對這個(gè)模式的理解是,它主要是基于一個(gè)已有的類型,在實(shí)例化時(shí)對實(shí)例化的對象進(jìn)行擴(kuò)展。這樣既不用修改原來的構(gòu)造函數(shù),也達(dá)到了擴(kuò)展對象的目的。它的一個(gè)缺點(diǎn)和工廠模式一樣,無法實(shí)現(xiàn)對象的識別。 嗯我目前了解到的就是這么幾種方式。
詳細(xì)資料可以參考:
《JavaScript 深入理解之對象創(chuàng)建》
11. JavaScript 繼承的幾種實(shí)現(xiàn)方式?
我了解的 js 中實(shí)現(xiàn)繼承的幾種方式有: (1)第一種是以原型鏈的方式來實(shí)現(xiàn)繼承,但是這種實(shí)現(xiàn)方式存在的缺點(diǎn)是,在包含有引用類型的數(shù)據(jù)時(shí),會被所有的實(shí)例對象所共享,容易造成修改的混亂。還有就是在創(chuàng)建子類型的時(shí)候不能向超類型傳遞參數(shù)。 (2)第二種方式是使用借用構(gòu)造函數(shù)的方式,這種方式是通過在子類型的函數(shù)中調(diào)用超類型的構(gòu)造函數(shù)來實(shí)現(xiàn)的,這一種方法解決了不能向超類型傳遞參數(shù)的缺點(diǎn),但是它存在的一個(gè)問題就是無法實(shí)現(xiàn)函數(shù)方法的復(fù)用,并且超類型原型定義的方法子類型也沒有辦法訪問到。 (3)第三種方式是組合繼承,組合繼承是將原型鏈和借用構(gòu)造函數(shù)組合起來使用的一種方式。通過借用構(gòu)造函數(shù)的方式來實(shí)現(xiàn)類型的屬性的繼承,通過將子類型的原型設(shè)置為超類型的實(shí)例來實(shí)現(xiàn)方法的繼承。這種方式解決了上面的兩種模式單獨(dú)使用時(shí)的問題,但是由于我們是以超類型的實(shí)例來作為子類型的原型,所以調(diào)用了兩次超類的構(gòu)造函數(shù),造成了子類型的原型中多了很多不必要的屬性。 (4)第四種方式是原型式繼承,原型式繼承的主要思路就是基于已有的對象來創(chuàng)建新的對象,實(shí)現(xiàn)的原理是,向函數(shù)中傳入一個(gè)對象,然后返回一個(gè)以這個(gè)對象為原型的對象。這種繼承的思路主要不是為了實(shí)現(xiàn)創(chuàng)造一種新的類型,只是對某個(gè)對象實(shí)現(xiàn)一種簡單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實(shí)現(xiàn)。缺點(diǎn)與原型鏈方式相同。 (5)第五種方式是寄生式繼承,寄生式繼承的思路是創(chuàng)建一個(gè)用于封裝繼承過程的函數(shù),通過傳入一個(gè)對象,然后復(fù)制一個(gè)對象的副本,然后對象進(jìn)行擴(kuò)展,最后返回這個(gè)對象。這個(gè)擴(kuò)展的過程就可以理解是一種繼承。這種繼承的優(yōu)點(diǎn)就是對一個(gè)簡單對象實(shí)現(xiàn)繼承,如果這個(gè)對象不是我們的自定義類型時(shí)。缺點(diǎn)是沒有辦法實(shí)現(xiàn)函數(shù)的復(fù)用。 (6)第六種方式是寄生式組合繼承,組合繼承的缺點(diǎn)就是使用超類型的實(shí)例做為子類型的原型,導(dǎo)致添加了不必要的原型屬性。寄生式組合繼承的方式是使用超類型的原型的副本來作為子類型的原型,這樣就避免了創(chuàng)建不必要的屬性。
詳細(xì)資料可以參考:
《JavaScript 深入理解之繼承》
12. 寄生式組合繼承的實(shí)現(xiàn)?
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); StudentStudent.prototype.constructor = Student; Student.prototype.sayMyGrade = function() { console.log("My grade is " + this.grade + "."); };
13. 談?wù)勀銓his、call、apply和bind的理解
詳情可看我之前的文章:「前端料包」一文徹底搞懂JavaScript中的this、call、apply和bind
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
在瀏覽器里,在全局范圍內(nèi)this 指向window對象;
在函數(shù)中,this永遠(yuǎn)指向最后調(diào)用他的那個(gè)對象;
構(gòu)造函數(shù)中,this指向new出來的那個(gè)新的對象;
call、apply、bind中的this被強(qiáng)綁定在指定的那個(gè)對象上;
箭頭函數(shù)中this比較特殊,箭頭函數(shù)this為父作用域的this,不是調(diào)用時(shí)的this.要知道前四種方式,都是調(diào)用時(shí)確定,也就是動態(tài)的,而箭頭函數(shù)的this指向是靜態(tài)的,聲明的時(shí)候就確定了下來;
apply、call、bind都是js給函數(shù)內(nèi)置的一些API,調(diào)用他們可以為函數(shù)指定this的執(zhí)行,同時(shí)也可以傳參。
14. JavaScript 原型,原型鏈?有什么特點(diǎn)?
在 js 中我們是使用構(gòu)造函數(shù)來新建一個(gè)對象的,每一個(gè)構(gòu)造函數(shù)的內(nèi)部都有一個(gè) prototype 屬性值,這個(gè)屬性值是一個(gè)對
象,這個(gè)對象包含了可以由該構(gòu)造函數(shù)的所有實(shí)例共享的屬性和方法。當(dāng)我們使用構(gòu)造函數(shù)新建一個(gè)對象后,在這個(gè)對象的內(nèi)部
將包含一個(gè)指針,這個(gè)指針指向構(gòu)造函數(shù)的 prototype 屬性對應(yīng)的值,在 ES5 中這個(gè)指針被稱為對象的原型。一般來說我們
是不應(yīng)該能夠獲取到這個(gè)值的,但是現(xiàn)在瀏覽器中都實(shí)現(xiàn)了 proto 屬性來讓我們訪問這個(gè)屬性,但是我們最好不要使用這
個(gè)屬性,因?yàn)樗皇且?guī)范中規(guī)定的。ES5 中新增了一個(gè) Object.getPrototypeOf() 方法,我們可以通過這個(gè)方法來獲取對
象的原型。
當(dāng)我們訪問一個(gè)對象的屬性時(shí),如果這個(gè)對象內(nèi)部不存在這個(gè)屬性,那么它就會去它的原型對象里找這個(gè)屬性,這個(gè)原型對象又
會有自己的原型,于是就這樣一直找下去,也就是原型鏈的概念。原型鏈的盡頭一般來說都是 Object.prototype 所以這就
是我們新建的對象為什么能夠使用 toString() 等方法的原因。
特點(diǎn):
JavaScript 對象是通過引用來傳遞的,我們創(chuàng)建的每個(gè)新對象實(shí)體中并沒有一份屬于自己的原型副本。當(dāng)我們修改原型時(shí),與
之相關(guān)的對象也會繼承這一改變。
參考文章:
《JavaScript 深入理解之原型與原型鏈》
也可以看看我寫的:「前端料包」深入理解JavaScript原型和原型鏈
15. js 獲取原型的方法?
p.proto
p.constructor.prototype
Object.getPrototypeOf(p)
16. 什么是閉包,為什么要用它?
閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域內(nèi)變量的函數(shù),創(chuàng)建閉包的最常見的方式就是在一個(gè)函數(shù)內(nèi)創(chuàng)建另一個(gè)函數(shù),創(chuàng)建的函數(shù)可以
訪問到當(dāng)前函數(shù)的局部變量。
閉包有兩個(gè)常用的用途。
閉包的第一個(gè)用途是使我們在函數(shù)外部能夠訪問到函數(shù)內(nèi)部的變量。通過使用閉包,我們可以通過在外部調(diào)用閉包函數(shù),從而在外部訪問到函數(shù)內(nèi)部的變量,可以使用這種方法來創(chuàng)建私有變量。
函數(shù)的另一個(gè)用途是使已經(jīng)運(yùn)行結(jié)束的函數(shù)上下文中的變量對象繼續(xù)留在內(nèi)存中,因?yàn)殚]包函數(shù)保留了這個(gè)變量對象的引用,所以這個(gè)變量對象不會被回收。
function a(){ var n = 0; function add(){ n++; console.log(n); } return add; } var aa1 = a(); //注意,函數(shù)名只是一個(gè)標(biāo)識(指向函數(shù)的指針),而()才是執(zhí)行函數(shù); a1(); //1 a1(); //2 第二次調(diào)用n變量還在內(nèi)存中
其實(shí)閉包的本質(zhì)就是作用域鏈的一個(gè)特殊的應(yīng)用,只要了解了作用域鏈的創(chuàng)建過程,就能夠理解閉包的實(shí)現(xiàn)原理。
17. 什么是 DOM 和 BOM?
DOM 指的是文檔對象模型,它指的是把文檔當(dāng)做一個(gè)對象來對待,這個(gè)對象主要定義了處理網(wǎng)頁內(nèi)容的方法和接口。
BOM 指的是瀏覽器對象模型,它指的是把瀏覽器當(dāng)做一個(gè)對象來對待,這個(gè)對象主要定義了與瀏覽器進(jìn)行交互的法和接口。BOM
的核心是 window,而 window 對象具有雙重角色,它既是通過 js 訪問瀏覽器窗口的一個(gè)接口,又是一個(gè) Global(全局)
對象。這意味著在網(wǎng)頁中定義的任何對象,變量和函數(shù),都作為全局對象的一個(gè)屬性或者方法存在。window 對象含有 locati
on 對象、navigator 對象、screen 對象等子對象,并且 DOM 的最根本的對象 document 對象也是 BOM 的 window 對
象的子對象。
相關(guān)資料:
《DOM, DOCUMENT, BOM, WINDOW 有什么區(qū)別?》
《Window 對象》
《DOM 與 BOM 分別是什么,有何關(guān)聯(lián)?》
《JavaScript 學(xué)習(xí)總結(jié)(三)BOM 和 DOM 詳解》
18. 三種事件模型是什么?
事件 是用戶操作網(wǎng)頁時(shí)發(fā)生的交互動作或者網(wǎng)頁本身的一些操作,現(xiàn)代瀏覽器一共有三種事件模型。
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
DOM0級模型: ,這種模型不會傳播,所以沒有事件流的概念,但是現(xiàn)在有的瀏覽器支持以冒泡的方式實(shí)現(xiàn),它可以在網(wǎng)頁中直接定義監(jiān)聽函數(shù),也可以通過 js屬性來指定監(jiān)聽函數(shù)。這種方式是所有瀏覽器都兼容的。
2. IE 事件模型: 在該事件模型中,一次事件共有兩個(gè)過程,事件處理階段,和事件冒泡階段。事件處理階段會首先執(zhí)行目標(biāo)元素綁定的監(jiān)聽事件。然后是事件冒泡階段,冒泡指的是事件從目標(biāo)元素冒泡到 document,依次檢查經(jīng)過的節(jié)點(diǎn)是否綁定了事件監(jiān)聽函數(shù),如果有則執(zhí)行。這種模型通過 attachEvent 來添加監(jiān)聽函數(shù),可以添加多個(gè)監(jiān)聽函數(shù),會按順序依次執(zhí)行。
3. DOM2 級事件模型: 在該事件模型中,一次事件共有三個(gè)過程,第一個(gè)過程是事件捕獲階段。捕獲指的是事件從 document 一直向下傳播到目標(biāo)元素,依次檢查經(jīng)過的節(jié)點(diǎn)是否綁定了事件監(jiān)聽函數(shù),如果有則執(zhí)行。后面兩個(gè)階段和 IE 事件模型的兩個(gè)階段相同。這種事件模型,事件綁定的函數(shù)是 addEventListener,其中第三個(gè)參數(shù)可以指定事件是否在捕獲階段執(zhí)行。
相關(guān)資料:
《一個(gè) DOM 元素綁定多個(gè)事件時(shí),先執(zhí)行冒泡還是捕獲》
19. 事件委托是什么?
事件委托 本質(zhì)上是利用了瀏覽器事件冒泡的機(jī)制。因?yàn)槭录诿芭葸^程中會上傳到父節(jié)點(diǎn),并且父節(jié)點(diǎn)可以通過事件對象獲取到
目標(biāo)節(jié)點(diǎn),因此可以把子節(jié)點(diǎn)的監(jiān)聽函數(shù)定義在父節(jié)點(diǎn)上,由父節(jié)點(diǎn)的監(jiān)聽函數(shù)統(tǒng)一處理多個(gè)子元素的事件,這種方式稱為事件代理。
使用事件代理我們可以不必要為每一個(gè)子元素都綁定一個(gè)監(jiān)聽事件,這樣減少了內(nèi)存上的消耗。并且使用事件代理我們還可以實(shí)現(xiàn)事件的動態(tài)綁定,比如說新增了一個(gè)子節(jié)點(diǎn),我們并不需要單獨(dú)地為它添加一個(gè)監(jiān)聽事件,它所發(fā)生的事件會交給父元素中的監(jiān)聽函數(shù)來處理。
相關(guān)資料:
《JavaScript 事件委托詳解》
20. 什么是事件傳播?
當(dāng)事件發(fā)生在DOM元素上時(shí),該事件并不完全發(fā)生在那個(gè)元素上。在“當(dāng)事件發(fā)生在DOM元素上時(shí),該事件并不完全發(fā)生在那個(gè)元素上。在“冒泡階段”中,事件冒泡或向上傳播至父級,祖父母,祖父母或父級,直到到達(dá)window為止;而在“捕獲階段”中,事件從window開始向下觸發(fā)元素 事件或event.target。
事件傳播有三個(gè)階段:”中,事件冒泡或向上傳播至父級,祖父母,祖父母或父級,直到到達(dá)window為止;而在“捕獲階段”中,事件從window開始向下觸發(fā)元素 事件或event.target。
事件傳播有三個(gè)階段:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
捕獲階段–事件從 window 開始,然后向下到每個(gè)元素,直到到達(dá)目標(biāo)元素。
目標(biāo)階段–事件已達(dá)到目標(biāo)元素。
冒泡階段–事件從目標(biāo)元素冒泡,然后上升到每個(gè)元素,直到到達(dá) window。
21. 什么是事件捕獲?
當(dāng)事件發(fā)生在 DOM 元素上時(shí),該事件并不完全發(fā)生在那個(gè)元素上。在捕獲階段,事件從window開始,一直到觸發(fā)事件的元素。window----> document----> html----> body ---->目標(biāo)元素
假設(shè)有如下的 HTML 結(jié)構(gòu):
<div class="grandparent"> <div class="parent"> <div class="child">1</div> </div> </div>
對應(yīng)的 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方法具有第三個(gè)可選參數(shù)useCapture,其默認(rèn)值為false,事件將在冒泡階段中發(fā)生,如果為true,則事件將在捕獲階段中發(fā)生。如果單擊child元素,它將分別在控制臺上打印window,document,html,grandparent和parent,這就是事件捕獲。
22. 什么是事件冒泡?
事件冒泡剛好與事件捕獲相反,當(dāng)前元素---->body ----> html---->document ---->window。當(dāng)事件發(fā)生在DOM元素上時(shí),該事件并不完全發(fā)生在那個(gè)元素上。在冒泡階段,事件冒泡,或者事件發(fā)生在它的父代,祖父母,祖父母的父代,直到到達(dá)window為止。
假設(shè)有如下的 HTML 結(jié)構(gòu):
<div class="grandparent"> <div class="parent"> <div class="child">1</div> </div> </div>
對應(yīng)的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方法具有第三個(gè)可選參數(shù)useCapture,其默認(rèn)值為false,事件將在冒泡階段中發(fā)生,如果為true,則事件將在捕獲階段中發(fā)生。如果單擊child元素,它將分別在控制臺上打印child,parent,grandparent,html,document和window,這就是事件冒泡。
23. DOM 操作——怎樣添加、移除、移動、復(fù)制、創(chuàng)建和查找節(jié)點(diǎn)?
(1)創(chuàng)建新節(jié)點(diǎn)
createDocumentFragment() //創(chuàng)建一個(gè)DOM片段 createElement() //創(chuàng)建一個(gè)具體的元素 createTextNode() //創(chuàng)建一個(gè)文本節(jié)點(diǎn)
(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);
相關(guān)資料:
《DOM 概述》
《原生 JavaScript 的 DOM 操作匯總》
《原生 JS 中 DOM 節(jié)點(diǎn)相關(guān) API 合集》
24. js數(shù)組和對象有哪些原生方法,列舉一下
25. 常用的正則表達(dá)式
//(1)匹配 16 進(jìn)制顏色值 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)手機(jī)號碼正則 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 = /^[京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領(lǐng)A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9掛學(xué)警港澳]{1}$/; // (11)強(qiáng)密碼(必須包含大小寫字母和數(shù)字的組合,不能使用特殊字符,長度在8-10之間):var pwd = /^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$/
26. Ajax 是什么? 如何創(chuàng)建一個(gè) Ajax?
我對 ajax 的理解是,它是一種異步通信的方法,通過直接由 js 腳本向服務(wù)器發(fā)起 http 通信,然后根據(jù)服務(wù)器返回的數(shù)據(jù),更新網(wǎng)頁的相應(yīng)部分,而不用刷新整個(gè)頁面的一種方法。
創(chuàng)建步驟:
面試手寫(原生):
//1:創(chuàng)建Ajax對象 var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本 //2:配置 Ajax請求地址 xhr.open('get','index.xml',true); //3:發(fā)送請求 xhr.send(null); // 嚴(yán)謹(jǐn)寫法 //4:監(jiān)聽請求,接受響應(yīng) 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 封裝實(shí)現(xiàn):
// promise 封裝實(shí)現(xiàn): function getJSON(url) { // 創(chuàng)建一個(gè) promise 對象 let promise = new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest(); // 新建一個(gè) http 請求 xhr.open("GET", url, true); // 設(shè)置狀態(tài)的監(jiān)聽函數(shù) xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 當(dāng)請求成功或失敗時(shí),改變 promise 的狀態(tài) if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; // 設(shè)置錯(cuò)誤監(jiān)聽函數(shù) xhr.onerror = function() { reject(new Error(this.statusText)); }; // 設(shè)置響應(yīng)的數(shù)據(jù)類型 xhr.responseType = "json"; // 設(shè)置請求頭信息 xhr.setRequestHeader("Accept", "application/json"); // 發(fā)送 http 請求 xhr.send(null); }); return promise; }
27. js 延遲加載的方式有哪些?
js 的加載、解析和執(zhí)行會阻塞頁面的渲染過程,因此我們希望 js 腳本能夠盡可能的延遲加載,提高頁面的渲染速度。
我了解到的幾種方式是:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
將 js 腳本放在文檔的底部,來使 js 腳本盡可能的在最后來加載執(zhí)行。
給 js 腳本添加 defer屬性,這個(gè)屬性會讓腳本的加載與文檔的解析同步解析,然后在文檔解析完成后再執(zhí)行這個(gè)腳本文件,這樣的話就能使頁面的渲染不被阻塞。多個(gè)設(shè)置了 defer 屬性的腳本按規(guī)范來說最后是順序執(zhí)行的,但是在一些瀏覽器中可能不是這樣。
3. 給 js 腳本添加 async屬性,這個(gè)屬性會使腳本異步加載,不會阻塞頁面的解析過程,但是當(dāng)腳本加載完成后立即執(zhí)行 js腳本,這個(gè)時(shí)候如果文檔沒有解析完成的話同樣會阻塞。多個(gè) async 屬性的腳本的執(zhí)行順序是不可預(yù)測的,一般不會按照代碼的順序依次執(zhí)行。
4. 動態(tài)創(chuàng)建 DOM 標(biāo)簽的方式,我們可以對文檔的加載事件進(jìn)行監(jiān)聽,當(dāng)文檔加載完成后再動態(tài)的創(chuàng)建 script 標(biāo)簽來引入 js 腳本。
相關(guān)資料:
《JS 延遲加載的幾種方式》
《HTML 5 <script> async 屬性》
28. 談?wù)勀銓δK化開發(fā)的理解?
我對模塊的理解是,一個(gè)模塊是實(shí)現(xiàn)一個(gè)特定功能的一組方法。在最開始的時(shí)候,js 只實(shí)現(xiàn)一些簡單的功能,所以并沒有模塊的概念
,但隨著程序越來越復(fù)雜,代碼的模塊化開發(fā)變得越來越重要。
由于函數(shù)具有獨(dú)立作用域的特點(diǎn),最原始的寫法是使用函數(shù)來作為模塊,幾個(gè)函數(shù)作為一個(gè)模塊,但是這種方式容易造成全局變量的污
染,并且模塊間沒有聯(lián)系。
后面提出了對象寫法,通過將函數(shù)作為一個(gè)對象的方法來實(shí)現(xiàn),這樣解決了直接使用函數(shù)作為模塊的一些缺點(diǎn),但是這種辦法會暴露所
有的所有的模塊成員,外部代碼可以修改內(nèi)部屬性的值。
現(xiàn)在最常用的是立即執(zhí)行函數(shù)的寫法,通過利用閉包來實(shí)現(xiàn)模塊私有作用域的建立,同時(shí)不會對全局作用域造成污染。
相關(guān)資料:
《淺談模塊化開發(fā)》
《Javascript 模塊化編程(一):模塊的寫法》
《前端模塊化:CommonJS,AMD,CMD,ES6》
《Module 的語法》
29. js 的幾種模塊規(guī)范?
js 中現(xiàn)在比較成熟的有四種模塊加載方案:
第一種是 CommonJS 方案,它通過 require 來引入模塊,通過 module.exports 定義模塊的輸出接口。這種模塊加載方案是服務(wù)器端的解決方案,它是以同步的方式來引入模塊的,因?yàn)樵诜?wù)端文件都存儲在本地磁盤,所以讀取非???,所以以同步的方式加載沒有問題。但如果是在瀏覽器端,由于模塊的加載是使用網(wǎng)絡(luò)請求,因此使用異步加載的方式更加合適。
第二種是 AMD 方案,這種方案采用異步加載的方式來加載模塊,模塊的加載不影響后面語句的執(zhí)行,所有依賴這個(gè)模塊的語句都定義在一個(gè)回調(diào)函數(shù)里,等到加載完成后再執(zhí)行回調(diào)函數(shù)。require.js 實(shí)現(xiàn)了 AMD 規(guī)范。
第三種是 CMD 方案,這種方案和 AMD 方案都是為了解決異步模塊加載的問題,sea.js 實(shí)現(xiàn)了 CMD 規(guī)范。它和require.js的區(qū)別在于模塊定義時(shí)對依賴的處理不同和對依賴模塊的執(zhí)行時(shí)機(jī)的處理不同。
第四種方案是 ES6 提出的方案,使用 import 和 export 的形式來導(dǎo)入導(dǎo)出模塊。這種方案和上面三種方案都不同。參考 61。
30. AMD 和 CMD 規(guī)范的區(qū)別?
它們之間的主要區(qū)別有兩個(gè)方面。
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
第一個(gè)方面是在模塊定義時(shí)對依賴的處理不同。AMD推崇依賴前置,在定義模塊的時(shí)候就要聲明其依賴的模塊。而 CMD 推崇就近依賴,只有在用到某個(gè)模塊的時(shí)候再去 require。
第二個(gè)方面是對依賴模塊的執(zhí)行時(shí)機(jī)處理不同。首先 AMD 和 CMD 對于模塊的加載方式都是異步加載,不過它們的區(qū)別在于
模塊的執(zhí)行時(shí)機(jī),AMD 在依賴模塊加載完成后就直接執(zhí)行依賴模塊,依賴模塊的執(zhí)行順序和我們書寫的順序不一定一致。而 CMD
在依賴模塊加載完成后并不執(zhí)行,只是下載而已,等到所有的依賴模塊都加載好后,進(jìn)入回調(diào)函數(shù)邏輯,遇到 require 語句
的時(shí)候才執(zhí)行對應(yīng)的模塊,這樣模塊的執(zhí)行順序就和我們書寫的順序保持一致了。
// CMD define(function(require, exports, module) { var a = require("./a"); a.doSomething(); // 此處略去 100 行 var b = require("./b"); // 依賴可以就近書寫 b.doSomething(); // ... }); // AMD 默認(rèn)推薦 define(["./a", "./b"], function(a, b) { // 依賴必須一開始就寫好 a.doSomething(); // 此處略去 100 行 b.doSomething(); // ... });
相關(guān)資料:
《前端模塊化,AMD 與 CMD 的區(qū)別》
31. ES6 模塊與 CommonJS 模塊、AMD、CMD 的差異。
1.CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用。CommonJS 模塊輸出的是值的
,也就是說,一旦輸出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值。ES6 模塊的運(yùn)行機(jī)制與 CommonJS 不一樣。JS 引擎對腳本靜態(tài)分析的時(shí)候,遇到模塊加載命令 import,就會生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí),再根據(jù)這個(gè)只讀引用,到被加載的那個(gè)模塊里面去取值。
2.CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出接口。CommonJS 模塊就是對象,即在輸入時(shí)是先加載整個(gè)模塊,生成一個(gè)對象,然后再從這個(gè)對象上面讀取方法,這種加載稱為“運(yùn)行時(shí)加載”。而 ES6 模塊不是對象,它的對外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會生成。
32. requireJS的核心原理是什么?
require.js 的核心原理是通過動態(tài)創(chuàng)建 script 腳本來異步引入模塊,然后對每個(gè)腳本的 load 事件進(jìn)行監(jiān)聽,如果每個(gè)腳本都加載完成了,再調(diào)用回調(diào)函數(shù)。
詳細(xì)資料可以參考:
《requireJS 的用法和原理分析》https://github.com/HRFE/blog/issues/10
《requireJS 的核心原理是什么?》https://zhuanlan.zhihu.com/p/55039478
《requireJS 原理分析》]https://www.jianshu.com/p/5a39535909e4
33. 談?wù)凧S的運(yùn)行機(jī)制
1. js單線程
JavaScript語言的一大特點(diǎn)就是單線程,即同一時(shí)間只能做一件事情。
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復(fù)雜的同步問題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會改變。
2. js事件循環(huán)
js代碼執(zhí)行過程中會有很多任務(wù),這些任務(wù)總的分成兩類:
同步任務(wù)
異步任務(wù)
當(dāng)我們打開網(wǎng)站時(shí),網(wǎng)頁的渲染過程就是一大堆同步任務(wù),比如頁面骨架和頁面元素的渲染。而像加載圖片音樂之類占用資源大耗時(shí)久的任務(wù),就是異步任務(wù)。,我們用導(dǎo)圖來說明:
我們解釋一下這張圖:
同步和異步任務(wù)分別進(jìn)入不同的執(zhí)行"場所",同步的進(jìn)入主線程,異步的進(jìn)入Event Table并注冊函數(shù)。
當(dāng)指定的事情完成時(shí),Event Table會將這個(gè)函數(shù)移入Event Queue。
主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會去Event Queue讀取對應(yīng)的函數(shù),進(jìn)入主線程執(zhí)行。
上述過程會不斷重復(fù),也就是常說的Event Loop(事件循環(huán))。
那主線程執(zhí)行棧何時(shí)為空呢?js引擎存在monitoring process進(jìn)程,會持續(xù)不斷的檢查主線程執(zhí)行棧是否為空,一旦為空,就會去Event Queue那里檢查是否有等待被調(diào)用的函數(shù)。
以上就是js運(yùn)行的整體流程
需要注意的是除了同步任務(wù)和異步任務(wù),任務(wù)還可以更加細(xì)分為macrotask(宏任務(wù))和microtask(微任務(wù)),js引擎會優(yōu)先執(zhí)行微任務(wù)
微任務(wù)包括了 promise 的回調(diào)、node 中的 process.nextTick 、對 Dom 變化監(jiān)聽的 MutationObserver。 宏任務(wù)包括了 script 腳本的執(zhí)行、setTimeout ,setInterval ,setImmediate 一類的定時(shí)事件,還有如 I/O 操作、UI 渲 染等。
面試中該如何回答呢?
下面是我個(gè)人推薦的回答:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
首先js 是單線程運(yùn)行的,在代碼執(zhí)行的時(shí)候,通過將不同函數(shù)的執(zhí)行上下文壓入執(zhí)行棧中來保證代碼的有序執(zhí)行。
在執(zhí)行同步代碼的時(shí)候,如果遇到了異步事件,js 引擎并不會一直等待其返回結(jié)果,而是會將這個(gè)事件掛起,繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)
當(dāng)同步事件執(zhí)行完畢后,再將異步事件對應(yīng)的回調(diào)加入到與當(dāng)前執(zhí)行棧中不同的另一個(gè)任務(wù)隊(duì)列中等待執(zhí)行。
任務(wù)隊(duì)列可以分為宏任務(wù)對列和微任務(wù)對列,當(dāng)當(dāng)前執(zhí)行棧中的事件執(zhí)行完畢后,js 引擎首先會判斷微任務(wù)對列中是否有任務(wù)可以執(zhí)行,如果有就將微任務(wù)隊(duì)首的事件壓入棧中執(zhí)行。
當(dāng)微任務(wù)對列中的任務(wù)都執(zhí)行完成后再去判斷宏任務(wù)對列中的任務(wù)。
最后可以用下面一道題檢測一下收獲:
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)
第一輪:主線程開始執(zhí)行,遇到setTimeout,將setTimeout的回調(diào)函數(shù)丟到宏任務(wù)隊(duì)列中,在往下執(zhí)行new Promise立即執(zhí)行,輸出2,then的回調(diào)函數(shù)丟到微任務(wù)隊(duì)列中,再繼續(xù)執(zhí)行,遇到process.nextTick,同樣將回調(diào)函數(shù)扔到為任務(wù)隊(duì)列,再繼續(xù)執(zhí)行,輸出5,當(dāng)所有同步任務(wù)執(zhí)行完成后看有沒有可以執(zhí)行的微任務(wù),發(fā)現(xiàn)有then函數(shù)和nextTick兩個(gè)微任務(wù),先執(zhí)行哪個(gè)呢?process.nextTick指定的異步任務(wù)總是發(fā)生在所有異步任務(wù)之前,因此先執(zhí)行process.nextTick輸出4然后執(zhí)行then函數(shù)輸出3,第一輪執(zhí)行結(jié)束。
第二輪:從宏任務(wù)隊(duì)列開始,發(fā)現(xiàn)setTimeout回調(diào),輸出1執(zhí)行完畢,因此結(jié)果是25431
相關(guān)資料:
《瀏覽器事件循環(huán)機(jī)制(event loop)》
《詳解 JavaScript 中的 Event Loop(事件循環(huán))機(jī)制》
《什么是 Event Loop?》
《這一次,徹底弄懂 JavaScript 執(zhí)行機(jī)制》
34. arguments 的對象是什么?
arguments對象是函數(shù)中傳遞的參數(shù)值的集合。它是一個(gè)類似數(shù)組的對象,因?yàn)樗幸粋€(gè)length屬性,我們可以使用數(shù)組索引表示法arguments[1]來訪問單個(gè)值,但它沒有數(shù)組中的內(nèi)置方法,如:forEach、reduce、filter和map。
我們可以使用Array.prototype.slice將arguments對象轉(zhuǎn)換成一個(gè)數(shù)組。
function one() { return Array.prototype.slice.call(arguments); }
注意:箭頭函數(shù)中沒有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
當(dāng)我們調(diào)用函數(shù)four時(shí),它會拋出一個(gè)ReferenceError: arguments is not defined error。使用rest語法,可以解決這個(gè)問題。
const four = (...args) => args;
這會自動將所有參數(shù)值放入數(shù)組中。
35. 為什么在調(diào)用這個(gè)函數(shù)時(shí),代碼中的`b`會變成一個(gè)全局變量?
function myFunc() { let a = b = 0; } myFunc();
原因是賦值運(yùn)算符是從右到左的求值的。這意味著當(dāng)多個(gè)賦值運(yùn)算符出現(xiàn)在一個(gè)表達(dá)式中時(shí),它們是從右向左求值的。所以上面代碼變成了這樣:
function myFunc() { let a = (b = 0); } myFunc();
首先,表達(dá)式b = 0求值,在本例中b沒有聲明。因此,JS引擎在這個(gè)函數(shù)外創(chuàng)建了一個(gè)全局變量b,之后表達(dá)式b = 0的返回值為0,并賦給新的局部變量a。
我們可以通過在賦值之前先聲明變量來解決這個(gè)問題。
function myFunc() { let a,b; a = b = 0; } myFunc();
36. 簡單介紹一下 V8 引擎的垃圾回收機(jī)制
v8 的垃圾回收機(jī)制基于分代回收機(jī)制,這個(gè)機(jī)制又基于世代假說,這個(gè)假說有兩個(gè)特點(diǎn),一是新生的對象容易早死,另一個(gè)是不死的對象會活得更久?;谶@個(gè)假說,v8 引擎將內(nèi)存分為了新生代和老生代。 新創(chuàng)建的對象或者只經(jīng)歷過一次的垃圾回收的對象被稱為新生代。經(jīng)歷過多次垃圾回收的對象被稱為老生代。 新生代被分為 From 和 To 兩個(gè)空間,To 一般是閑置的。當(dāng) From 空間滿了的時(shí)候會執(zhí)行 Scavenge 算法進(jìn)行垃圾回收。當(dāng)我們執(zhí)行垃圾回收算法的時(shí)候應(yīng)用邏輯將會停止,等垃圾回收結(jié)束后再繼續(xù)執(zhí)行。這個(gè)算法分為三步: (1)首先檢查 From 空間的存活對象,如果對象存活則判斷對象是否滿足晉升到老生代的條件,如果滿足條件則晉升到老生代。如果不滿足條件則移動 To 空間。 (2)如果對象不存活,則釋放對象的空間。 (3)最后將 From 空間和 To 空間角色進(jìn)行交換。 新生代對象晉升到老生代有兩個(gè)條件: (1)第一個(gè)是判斷是對象否已經(jīng)經(jīng)過一次 Scavenge 回收。若經(jīng)歷過,則將對象從 From 空間復(fù)制到老生代中;若沒有經(jīng)歷,則復(fù)制到 To 空間。 (2)第二個(gè)是 To 空間的內(nèi)存使用占比是否超過限制。當(dāng)對象從 From 空間復(fù)制到 To 空間時(shí),若 To 空間使用超過 25%,則對象直接晉升到老生代中。設(shè)置 25% 的原因主要是因?yàn)樗惴ńY(jié)束后,兩個(gè)空間結(jié)束后會交換位置,如果 To 空間的內(nèi)存太小,會影響后續(xù)的內(nèi)存分配。 老生代采用了標(biāo)記清除法和標(biāo)記壓縮法。標(biāo)記清除法首先會對內(nèi)存中存活的對象進(jìn)行標(biāo)記,標(biāo)記結(jié)束后清除掉那些沒有標(biāo)記的對象。由于標(biāo)記清除后會造成很多的內(nèi)存碎片,不便于后面的內(nèi)存分配。所以了解決內(nèi)存碎片的問題引入了標(biāo)記壓縮法。 由于在進(jìn)行垃圾回收的時(shí)候會暫停應(yīng)用的邏輯,對于新生代方法由于內(nèi)存小,每次停頓的時(shí)間不會太長,但對于老生代來說每次垃圾回收的時(shí)間長,停頓會造成很大的影響。 為了解決這個(gè)問題 V8 引入了增量標(biāo)記的方法,將一次停頓進(jìn)行的過程分為了多步,每次執(zhí)行完一小步就讓運(yùn)行邏輯執(zhí)行一會,就這樣交替運(yùn)行。
相關(guān)資料:
《深入理解 V8 的垃圾回收原理》
《JavaScript 中的垃圾回收》
37. 哪些操作會造成內(nèi)存泄漏?
1.意外的全局變量
2.被遺忘的計(jì)時(shí)器或回調(diào)函數(shù)
3.脫離 DOM 的引用
4.閉包
第一種情況是我們由于使用未聲明的變量,而意外的創(chuàng)建了一個(gè)全局變量,而使這個(gè)變量一直留在內(nèi)存中無法被回收。
第二種情況是我們設(shè)置了setInterval定時(shí)器,而忘記取消它,如果循環(huán)函數(shù)有對外部變量的引用的話,那么這個(gè)變量會被一直留在內(nèi)存中,而無法被回收。
第三種情況是我們獲取一個(gè)DOM元素的引用,而后面這個(gè)元素被刪除,由于我們一直保留了對這個(gè)元素的引用,所以它也無法被回收。
第四種情況是不合理的使用閉包,從而導(dǎo)致某些變量一直被留在內(nèi)存當(dāng)中。
相關(guān)資料:
《JavaScript 內(nèi)存泄漏教程》
《4 類 JavaScript 內(nèi)存泄漏及如何避免》
《杜絕 js 中四種內(nèi)存泄漏類型的發(fā)生》
《javascript 典型內(nèi)存泄漏及 chrome 的排查方法》
以下38~46條是ECMAScript 2015(ES6)中常考的基礎(chǔ)知識點(diǎn)
38. ECMAScript 是什么?
ECMAScript 是編寫腳本語言的標(biāo)準(zhǔn),這意味著JavaScript遵循ECMAScript標(biāo)準(zhǔn)中的規(guī)范變化,因?yàn)樗荍avaScript的藍(lán)圖。
ECMAScript 和 Javascript,本質(zhì)上都跟一門語言有關(guān),一個(gè)是語言本身的名字,一個(gè)是語言的約束條件
只不過發(fā)明JavaScript的那個(gè)人(Netscape公司),把東西交給了ECMA(European Computer Manufacturers Association),這個(gè)人規(guī)定一下他的標(biāo)準(zhǔn),因?yàn)楫?dāng)時(shí)有java語言了,又想強(qiáng)調(diào)這個(gè)東西是讓ECMA這個(gè)人定的規(guī)則,所以就這樣一個(gè)神奇的東西誕生了,這個(gè)東西的名稱就叫做ECMAScript。
javaScript = ECMAScript + DOM + BOM(自認(rèn)為是一種廣義的JavaScript)
ECMAScript說什么JavaScript就得做什么!
JavaScript(狹義的JavaScript)做什么都要問問ECMAScript我能不能這樣干!如果不能我就錯(cuò)了!能我就是對的!
——突然感覺JavaScript好沒有尊嚴(yán),為啥要搞個(gè)人出來約束自己,
那個(gè)人被創(chuàng)造出來也好委屈,自己被創(chuàng)造出來完全是因?yàn)橐s束JavaScript。
39. ECMAScript 2015(ES6)有哪些新特性?
塊作用域
類
箭頭函數(shù)
模板字符串
加強(qiáng)的對象字面
對象解構(gòu)
Promise
模塊
Symbol
代理(proxy)Set
函數(shù)默認(rèn)參數(shù)
rest 和展開
40. `var`,`let`和`const`的區(qū)別是什么?
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已聲明還沒賦值,默認(rèn)得到undefined值 var a = 100; console.log(b); // 報(bào)錯(cuò):b is not defined ===> 找不到b這個(gè)變量 let b = 10; console.log(c); // 報(bào)錯(cuò):c is not defined ===> 找不到c這個(gè)變量 const c = 10;
let和const聲明形成塊作用域
if(1){ var a = 100; let b = 10; } console.log(a); // 100 console.log(b) // 報(bào)錯(cuò):b is not defined ===> 找不到b這個(gè)變量 ------------------------------------------------------------- if(1){ var a = 100; const c = 1; } console.log(a); // 100 console.log(c) // 報(bào)錯(cuò):c is not defined ===> 找不到c這個(gè)變量
同一作用域下let和const不能聲明同名變量,而var可以
var a = 100; console.log(a); // 100 var a = 10; console.log(a); // 10 ------------------------------------- let a = 100; let a = 10; // 控制臺報(bào)錯(cuò):Identifier 'a' has already been declared ===> 標(biāo)識符a已經(jīng)被聲明了。
暫存死區(qū)
var a = 100; if(1){ a = 10; //在當(dāng)前塊作用域中存在a使用let/const聲明的情況下,給a賦值10時(shí),只會在當(dāng)前作用域找變量a, // 而這時(shí),還未到聲明時(shí)候,所以控制臺Error:a is not defined let a = 1; }
const
/* * 1、一旦聲明必須賦值,不能使用null占位。 * * 2、聲明后不能再修改 * * 3、如果聲明的是復(fù)合類型數(shù)據(jù),可以修改其屬性 * * */ const a = 100; const list = []; list[0] = 10; console.log(list);// [10] const obj = {a:100}; obj.name = 'apple'; obj.a = 10000; console.log(obj);// {a:10000,name:'apple'}
41. 什么是箭頭函數(shù)?
箭頭函數(shù)表達(dá)式的語法比函數(shù)表達(dá)式更簡潔,并且沒有自己的this,arguments,super或new.target。箭頭函數(shù)表達(dá)式更適用于那些本來需要匿名函數(shù)的地方,并且它不能用作構(gòu)造函數(shù)。
//ES5 Version var getCurrentDate = function (){ return new Date(); } //ES6 Version const getCurrentDate = () => new Date();
在本例中,ES5 版本中有function(){}聲明和return關(guān)鍵字,這兩個(gè)關(guān)鍵字分別是創(chuàng)建函數(shù)和返回值所需要的。在箭頭函數(shù)版本中,我們只需要()括號,不需要 return 語句,因?yàn)槿绻覀冎挥幸粋€(gè)表達(dá)式或值需要返回,箭頭函數(shù)就會有一個(gè)隱式的返回。
//ES5 Version function greet(name) { return 'Hello ' + name + '!'; } //ES6 Version const greet = (name) => `Hello ${name}`; const greet2 = name => `Hello ${name}`;
我們還可以在箭頭函數(shù)中使用與函數(shù)表達(dá)式和函數(shù)聲明相同的參數(shù)。如果我們在一個(gè)箭頭函數(shù)中有一個(gè)參數(shù),則可以省略括號。
const getArgs = () => arguments const getArgs2 = (...rest) => rest
箭頭函數(shù)不能訪問arguments對象。所以調(diào)用第一個(gè)getArgs函數(shù)會拋出一個(gè)錯(cuò)誤。相反,我們可以使用rest參數(shù)來獲得在箭頭函數(shù)中傳遞的所有參數(shù)。
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(); } };
箭頭函數(shù)沒有自己的this值。它捕獲詞法作用域函數(shù)的this值,在此示例中,addAll函數(shù)將復(fù)制computeResult 方法中的this值,如果我們在全局作用域聲明箭頭函數(shù),則this值為 window 對象。
42. 什么是類?
類(class)是在 JS 中編寫構(gòu)造函數(shù)的新方法。它是使用構(gòu)造函數(shù)的語法糖,在底層中使用仍然是原型和基于原型的繼承。
//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}`; } }
重寫方法并從另一個(gè)類繼承。
//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]"; } }
所以我們要怎么知道它在內(nèi)部使用原型?
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
相關(guān)資料:
《ECMAScript 6 實(shí)現(xiàn)了 class,對 JavaScript 前端開發(fā)有什么意義?》
《Class 的基本語法》
43. 什么是模板字符串?
模板字符串是在 JS 中創(chuàng)建字符串的一種新方法。我們可以通過使用反引號使模板字符串化。
//ES5 Version var greet = 'Hi I\'m Mark'; //ES6 Version let greet = `Hi I'm Mark`;
在 ES5 中我們需要使用一些轉(zhuǎn)義字符來達(dá)到多行的效果,在模板字符串不需要這么麻煩:
//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 版本中,如果需要在字符串中添加表達(dá)式或值,則需要使用+運(yùn)算符。在模板字符串s中,我們可以使用${expr}嵌入一個(gè)表達(dá)式,這使其比 ES5 版本更整潔。
44. 什么是對象解構(gòu)?
對象析構(gòu)是從對象或數(shù)組中獲取或提取值的一種新的、更簡潔的方法。假設(shè)有如下的對象:
const employee = { firstName: "Marko", lastName: "Polo", position: "Software Developer", yearHired: 2017 };
從對象獲取屬性,早期方法是創(chuàng)建一個(gè)與對象屬性同名的變量。這種方法很麻煩,因?yàn)槲覀円獮槊總€(gè)屬性創(chuàng)建一個(gè)新變量。假設(shè)我們有一個(gè)大對象,它有很多屬性和方法,用這種方法提取屬性會很麻煩。
var firstName = employee.firstName; var lastName = employee.lastName; var position = employee.position; var yearHired = employee.yearHired;
使用解構(gòu)方式語法就變得簡潔多了:
{ firstName, lastName, position, yearHired } = employee;
我們還可以為屬性取別名:
let { firstName: fName, lastName: lName, position, yearHired } = employee;
當(dāng)然如果屬性值為 undefined 時(shí),我們還可以指定默認(rèn)值:
let { firstName = "Mark", lastName: lName, position, yearHired } = employee;
45. 什么是`Set`對象,它是如何工作的?
Set 對象允許你存儲任何類型的唯一值,無論是原始值或者是對象引用。
我們可以使用Set構(gòu)造函數(shù)創(chuàng)建Set實(shí)例。
const set1 = new Set(); const set2 = new Set(["a","b","c","d","d","e"]);
我們可以使用add方法向Set實(shí)例中添加一個(gè)新值,因?yàn)閍dd方法返回Set對象,所以我們可以以鏈?zhǔn)降姆绞皆俅问褂胊dd。如果一個(gè)值已經(jīng)存在于Set對象中,那么它將不再被添加。
set2.add("f"); set2.add("g").add("h").add("i").add("j").add("k").add("k"); // 后一個(gè)“k”不會被添加到set對象中,因?yàn)樗呀?jīng)存在了
我們可以使用has方法檢查Set實(shí)例中是否存在特定的值。
set2.has("a") // true set2.has("z") // true
我們可以使用size屬性獲得Set實(shí)例的長度。
set2.size // returns 10
可以使用clear方法刪除 Set 中的數(shù)據(jù)。
set2.clear();
我們可以使用Set對象來刪除數(shù)組中重復(fù)的元素。
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 類似,也是不重復(fù)的值的集合。但是 WeakSet 的成員只能是對象,而不能是其他類型的值。WeakSet 中的對象都是弱引用,即垃圾回收機(jī)制不考慮 WeakSet對該對象的引用。
Map 數(shù)據(jù)結(jié)構(gòu)。它類似于對象,也是鍵值對的集合,但是“鍵”的范圍不限于字符串,各種類型的值(包括對象)都可以當(dāng)作鍵。
WeakMap 結(jié)構(gòu)與 Map 結(jié)構(gòu)類似,也是用于生成鍵值對的集合。但是 WeakMap 只接受對象作為鍵名( null 除外),不接受其他類型的值作為鍵名。而且 WeakMap 的鍵名所指向的對象,不計(jì)入垃圾回收機(jī)制。
46. 什么是Proxy?
Proxy 用于修改某些操作的默認(rèn)行為,等同于在語言層面做出修改,所以屬于一種“元編程”,即對編程語言進(jìn)行編程。
Proxy 可以理解成,在目標(biāo)對象之前架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對外界的訪問進(jìn)行過濾和改寫。Proxy 這個(gè)詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
高能預(yù)警 , 以下47~64條是JavaScript中比較難的高級知識及相關(guān)手寫實(shí)現(xiàn),各位看官需慢慢細(xì)品
47. 寫一個(gè)通用的事件偵聽器函數(shù)
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; } }, // 獲取事件目標(biāo) getTarget: function(event) { return event.target || event.srcElement; }, // 獲取 event 對象的引用,取到事件的所有信息,確保隨時(shí)能使用 event getEvent: function(event) { return event || window.event; }, // 阻止事件(主要是事件冒泡,因?yàn)?nbsp;IE 不支持事件捕獲) stopPropagation: function(event) { if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } }, // 取消事件的默認(rèn)行為 preventDefault: function(event) { if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } } };
48. 什么是函數(shù)式編程? JavaScript的哪些特性使其成為函數(shù)式語言的候選語言?
函數(shù)式編程(通常縮寫為FP)是通過編寫純函數(shù),避免共享狀態(tài)、可變數(shù)據(jù)、副作用 來構(gòu)建軟件的過程。數(shù)式編程是聲明式 的而不是命令式 的,應(yīng)用程序的狀態(tài)是通過純函數(shù)流動的。與面向?qū)ο缶幊绦纬蓪Ρ?,面向?qū)ο笾袘?yīng)用程序的狀態(tài)通常與對象中的方法共享和共處。
函數(shù)式編程是一種編程范式 ,這意味著它是一種基于一些基本的定義原則(如上所列)思考軟件構(gòu)建的方式。當(dāng)然,編程范式的其他示例也包括面向?qū)ο缶幊毯瓦^程編程。
函數(shù)式的代碼往往比命令式或面向?qū)ο蟮拇a更簡潔,更可預(yù)測,更容易測試 - 但如果不熟悉它以及與之相關(guān)的常見模式,函數(shù)式的代碼也可能看起來更密集雜亂,并且 相關(guān)文獻(xiàn)對新人來說是不好理解的。
49. 什么是高階函數(shù)?
高階函數(shù)只是將函數(shù)作為參數(shù)或返回值的函數(shù)。
function higherOrderFunction(param,callback){ return callback(param); }
50. 為什么函數(shù)被稱為一等公民?
在JavaScript中,函數(shù)不僅擁有一切傳統(tǒng)函數(shù)的使用方式(聲明和調(diào)用),而且可以做到像簡單值一樣:
賦值(var func = function(){})、
傳參(function func(x,callback){callback();})、
返回(function(){return function(){}}),
這樣的函數(shù)也稱之為第一級函數(shù)(First-class Function)。不僅如此,JavaScript中的函數(shù)還充當(dāng)了類的構(gòu)造函數(shù)的作用,同時(shí)又是一個(gè)Function類的實(shí)例(instance)。這樣的多重身份讓JavaScript的函數(shù)變得非常重要。
51. 手動實(shí)現(xiàn) `Array.prototype.map 方法`
map() 方法創(chuàng)建一個(gè)新數(shù)組,其結(jié)果是該數(shù)組中的每個(gè)元素都調(diào)用一個(gè)提供的函數(shù)后返回的結(jié)果。
function map(arr, mapCallback) { // 首先,檢查傳遞的參數(shù)是否正確。 if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') { return []; } else { let result = []; // 每次調(diào)用此函數(shù)時(shí),我們都會創(chuàng)建一個(gè) result 數(shù)組 // 因?yàn)槲覀儾幌敫淖冊紨?shù)組。 for (let i = 0, len = arr.length; i < len; i++) { result.push(mapCallback(arr[i], i, arr)); // 將 mapCallback 返回的結(jié)果 push 到 result 數(shù)組中 } return result; } }
52. 手動實(shí)現(xiàn)`Array.prototype.filter`方法
filter()方法創(chuàng)建一個(gè)新數(shù)組, 其包含通過所提供函數(shù)實(shí)現(xiàn)的測試的所有元素。
function filter(arr, filterCallback) { // 首先,檢查傳遞的參數(shù)是否正確。 if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function') { return []; } else { let result = []; // 每次調(diào)用此函數(shù)時(shí),我們都會創(chuàng)建一個(gè) result 數(shù)組 // 因?yàn)槲覀儾幌敫淖冊紨?shù)組。 for (let i = 0, len = arr.length; i < len; i++) { // 檢查 filterCallback 的返回值是否是真值 if (filterCallback(arr[i], i, arr)) { // 如果條件為真,則將數(shù)組元素 push 到 result 中 result.push(arr[i]); } } return result; // return the result array } }
53. 手動實(shí)現(xiàn)`Array.prototype.reduce`方法
[reduce()]() 方法對數(shù)組中的每個(gè)元素執(zhí)行一個(gè)由您提供的reducer函數(shù)(升序執(zhí)行),將其結(jié)果匯總為單個(gè)返回值。
function reduce(arr, reduceCallback, initialValue) { // 首先,檢查傳遞的參數(shù)是否正確。 if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function') { return []; } else { // 如果沒有將initialValue傳遞給該函數(shù),我們將使用第一個(gè)數(shù)組項(xiàng)作為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的深淺拷貝一直是個(gè)難點(diǎn),如果現(xiàn)在面試官讓我寫一個(gè)深拷貝,我可能也只是能寫出個(gè)基礎(chǔ)版的。所以在寫這條之前我拜讀了收藏夾里各路大佬寫的博文。具體可以看下面我貼的鏈接,這里只做簡單的總結(jié)。
淺拷貝: 創(chuàng)建一個(gè)新對象,這個(gè)對象有著原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值,如果屬性是引用類型,拷貝的就是內(nèi)存地址 ,所以如果其中一個(gè)對象改變了這個(gè)地址,就會影響到另一個(gè)對象。
深拷貝: 將一個(gè)對象從內(nèi)存中完整的拷貝一份出來,從堆內(nèi)存中開辟一個(gè)新的區(qū)域存放新對象,且修改新對象不會影響原對象。
淺拷貝的實(shí)現(xiàn)方式:
Object.assign() 方法: 用于將所有可枚舉屬性的值從一個(gè)或多個(gè)源對象復(fù)制到目標(biāo)對象。它將返回目標(biāo)對象。
Array.prototype.slice():slice() 方法返回一個(gè)新的數(shù)組對象,這一對象是一個(gè)由 begin和end(不包括end)決定的原數(shù)組的淺拷貝。原始數(shù)組不會被改變。
拓展運(yùn)算符`…`:
let a = { name: "Jake", flag: { title: "better day by day", time: "2020-05-31" } } let b = {...a};
深拷貝的實(shí)現(xiàn)方式:
乞丐版: JSON.parse(JSON.stringify(object)),缺點(diǎn)諸多(會忽略undefined、symbol、函數(shù);不能解決循環(huán)引用;不能處理正則、new Date())
基礎(chǔ)版(面試夠用): 淺拷貝+遞歸 (只考慮了普通的 object和 array兩種數(shù)據(jù)類型)
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 funcfuncString = func.toString(); if (func.prototype) { const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if (body) { if (param) { const paramparamArr = 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); } // 防止循環(huán)引用 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; } // 克隆對象和數(shù)組 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 };
參考文章:
如何寫出一個(gè)驚艷面試官的深拷貝
深拷貝的終極探索(99%的人都不知道)
55. 手寫call、apply及bind函數(shù)
call 函數(shù)的實(shí)現(xiàn)步驟:
1.判斷調(diào)用對象是否為函數(shù),即使我們是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
2.判斷傳入上下文對象是否存在,如果不存在,則設(shè)置為 window 。
3.處理傳入的參數(shù),截取第一個(gè)參數(shù)后的所有參數(shù)。
4.將函數(shù)作為上下文對象的一個(gè)屬性。
5.使用上下文對象來調(diào)用這個(gè)方法,并保存返回結(jié)果。
6.刪除剛才新增的屬性。
7.返回結(jié)果。
// call函數(shù)實(shí)現(xiàn) Function.prototype.myCall = function(context) { // 判斷調(diào)用對象 if (typeof this !== "function") { console.error("type error"); } // 獲取參數(shù) let args = [...arguments].slice(1), result = null; // 判斷 context 是否傳入,如果未傳入則設(shè)置為 window contextcontext = context || window; // 將調(diào)用函數(shù)設(shè)為對象的方法 context.fn = this; // 調(diào)用函數(shù) result = context.fn(...args); // 將屬性刪除 delete context.fn; return result; };
apply 函數(shù)的實(shí)現(xiàn)步驟:
1. 判斷調(diào)用對象是否為函數(shù),即使我們是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
2. 判斷傳入上下文對象是否存在,如果不存在,則設(shè)置為 window 。
3. 將函數(shù)作為上下文對象的一個(gè)屬性。
4. 判斷參數(shù)值是否傳入
4. 使用上下文對象來調(diào)用這個(gè)方法,并保存返回結(jié)果。
5. 刪除剛才新增的屬性
6. 返回結(jié)果
// apply 函數(shù)實(shí)現(xiàn) Function.prototype.myApply = function(context) { // 判斷調(diào)用對象是否為函數(shù) if (typeof this !== "function") { throw new TypeError("Error"); } let result = null; // 判斷 context 是否存在,如果未傳入則為 window contextcontext = context || window; // 將函數(shù)設(shè)為對象的方法 context.fn = this; // 調(diào)用方法 if (arguments[1]) { result = context.fn(...arguments[1]); } else { result = context.fn(); } // 將屬性刪除 delete context.fn; return result; };
bind 函數(shù)的實(shí)現(xiàn)步驟:
1.判斷調(diào)用對象是否為函數(shù),即使我們是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
2.保存當(dāng)前函數(shù)的引用,獲取其余傳入?yún)?shù)值。
3.創(chuàng)建一個(gè)函數(shù)返回
4.函數(shù)內(nèi)部使用 apply 來綁定函數(shù)調(diào)用,需要判斷函數(shù)作為構(gòu)造函數(shù)的情況,這個(gè)時(shí)候需要傳入當(dāng)前函數(shù)的 this 給 apply 調(diào)用,其余情況都傳入指定的上下文對象。
// bind 函數(shù)實(shí)現(xiàn) Function.prototype.myBind = function(context) { // 判斷調(diào)用對象是否為函數(shù) if (typeof this !== "function") { throw new TypeError("Error"); } // 獲取參數(shù) var args = [...arguments].slice(1), fn = this; return function Fn() { // 根據(jù)調(diào)用方式,傳入不同綁定值 return fn.apply( this instanceof Fn ? this : context, args.concat(...arguments) ); }; };
參考文章:
《手寫 call、apply 及 bind 函數(shù)》
《JavaScript 深入之 call 和 apply 的模擬實(shí)現(xiàn)》
56. 函數(shù)柯里化的實(shí)現(xiàn)
// 函數(shù)柯里化指的是一種將使用多個(gè)參數(shù)的一個(gè)函數(shù)轉(zhuǎn)換成一系列使用一個(gè)參數(shù)的函數(shù)的技術(shù)。 function curry(fn, args) { // 獲取函數(shù)需要的參數(shù)長度 let length = fn.length; argsargs = args || []; return function() { let subArgs = args.slice(0); // 拼接得到現(xiàn)有的所有參數(shù) for (let i = 0; i < arguments.length; i++) { subArgs.push(arguments[i]); } // 判斷參數(shù)的長度是否已經(jīng)滿足函數(shù)所需參數(shù)的長度 if (subArgs.length >= length) { // 如果滿足,執(zhí)行函數(shù) return fn.apply(this, subArgs); } else { // 如果不滿足,遞歸返回科里化的函數(shù),等待參數(shù)的傳入 return curry.call(this, fn, subArgs); } }; } // es6 實(shí)現(xiàn) function curry(fn, ...args) { return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args); }
參考文章:
《JavaScript 專題之函數(shù)柯里化》https://github.com/mqyqingfeng/Blog/issues/42
57. js模擬new操作符的實(shí)現(xiàn)
這個(gè)問題如果你在掘金上搜,你可能會搜索到類似下面的回答:
說實(shí)話,看第一遍,我是不理解的,我需要去理一遍原型及原型鏈的知識才能理解。所以我覺得MDN對new的解釋更容易理解:
new 運(yùn)算符創(chuàng)建一個(gè)用戶定義的對象類型的實(shí)例或具有構(gòu)造函數(shù)的內(nèi)置對象的實(shí)例。new 關(guān)鍵字會進(jìn)行如下的操作:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
創(chuàng)建一個(gè)空的簡單JavaScript對象(即{});
鏈接該對象(即設(shè)置該對象的構(gòu)造函數(shù))到另一個(gè)對象 ;
將步驟1新創(chuàng)建的對象作為this的上下文 ;
如果該函數(shù)沒有返回對象,則返回this。
接下來我們看實(shí)現(xiàn):
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關(guān)鍵字的代碼,按照上述的1,2,3,4步來解析new背后的操作。
第一步:創(chuàng)建一個(gè)簡單空對象
var obj = {}
第二步:鏈接該對象到另一個(gè)對象(原型鏈)
// 設(shè)置原型鏈 obj.__proto__ = Dog.prototype
第三步:將步驟1新創(chuàng)建的對象作為 this 的上下文
// this指向obj對象 Dog.apply(obj, ['大黃', 'yellow', 3])
第四步:如果該函數(shù)沒有返回對象,則返回this
// 因?yàn)?nbsp;Dog() 沒有返回值,所以返回obj var dog = obj dog.getName() // '大黃'
需要注意的是如果 Dog() 有 return 則返回 return的值
var rtnObj = {} function Dog(name, color, age) { // ... //返回一個(gè)對象 return rtnObj } var dog = new Dog('大黃', 'yellow', 3) console.log(dog === rtnObj) // true
接下來我們將以上步驟封裝成一個(gè)對象實(shí)例化方法,即模擬new的操作:
function objectFactory(){ var obj = {}; //取得該方法的第一個(gè)參數(shù)(并刪除第一個(gè)參數(shù)),該參數(shù)是構(gòu)造函數(shù) var Constructor = [].shift.apply(arguments); //將新對象的內(nèi)部屬性__proto__指向構(gòu)造函數(shù)的原型,這樣新對象就可以訪問原型中的屬性和方法 obj.__proto__ = Constructor.prototype; //取得構(gòu)造函數(shù)的返回值 var ret = Constructor.apply(obj, arguments); //如果返回值是一個(gè)對象就返回該對象,否則返回構(gòu)造函數(shù)的一個(gè)實(shí)例對象 return typeof ret === "object" ? ret : obj; }
58. 什么是回調(diào)函數(shù)?回調(diào)函數(shù)有什么缺點(diǎn)
回調(diào)函數(shù)是一段可執(zhí)行的代碼段,它作為一個(gè)參數(shù)傳遞給其他的代碼,其作用是在需要的時(shí)候方便調(diào)用這段(回調(diào)函數(shù))代碼。
在JavaScript中函數(shù)也是對象的一種,同樣對象可以作為參數(shù)傳遞給函數(shù),因此函數(shù)也可以作為參數(shù)傳遞給另外一個(gè)函數(shù),這個(gè)作為參數(shù)的函數(shù)就是回調(diào)函數(shù)。
const btnAdd = document.getElementById('btnAdd'); btnAdd.addEventListener('click', function clickCallback(e) { // do something useless });
在本例中,我們等待id為btnAdd的元素中的click事件,如果它被單擊,則執(zhí)行clickCallback函數(shù)?;卣{(diào)函數(shù)向某些數(shù)據(jù)或事件添加一些功能。
回調(diào)函數(shù)有一個(gè)致命的弱點(diǎn),就是容易寫出回調(diào)地獄(Callback hell)。假設(shè)多個(gè)事件存在依賴性:
setTimeout(() => { console.log(1) setTimeout(() => { console.log(2) setTimeout(() => { console.log(3) },3000) },2000) },1000)
這就是典型的回調(diào)地獄,以上代碼看起來不利于閱讀和維護(hù),事件一旦多起來就更是亂糟糟,所以在es6中提出了Promise和async/await來解決回調(diào)地獄的問題。當(dāng)然,回調(diào)函數(shù)還存在著別的幾個(gè)缺點(diǎn),比如不能使用 try catch 捕獲錯(cuò)誤,不能直接 return。接下來的兩條就是來解決這些問題的,咱們往下看。
59. Promise是什么,可以手寫實(shí)現(xiàn)一下嗎?
Promise,翻譯過來是承諾,承諾它過一段時(shí)間會給你一個(gè)結(jié)果。從編程講Promise 是異步編程的一種解決方案。下面是Promise在MDN的相關(guān)說明:
Promise 對象是一個(gè)代理對象(代理一個(gè)值),被代理的值在Promise對象創(chuàng)建時(shí)可能是未知的。它允許你為異步操作的成功和失敗分別綁定相應(yīng)的處理方法(handlers)。這讓異步方法可以像同步方法那樣返回值,但并不是立即返回最終執(zhí)行結(jié)果,而是一個(gè)能代表未來出現(xiàn)的結(jié)果的promise對象。
一個(gè) Promise有以下幾種狀態(tài):
pending: 初始狀態(tài),既不是成功,也不是失敗狀態(tài)。
fulfilled: 意味著操作成功完成。
rejected: 意味著操作失敗。
這個(gè)承諾一旦從等待狀態(tài)變成為其他狀態(tài)就永遠(yuǎn)不能更改狀態(tài)了,也就是說一旦狀態(tài)變?yōu)?fulfilled/rejected 后,就不能再次改變。
可能光看概念大家不理解Promise,我們舉個(gè)簡單的栗子;
假如我有個(gè)女朋友,下周一是她生日,我答應(yīng)她生日給她一個(gè)驚喜,那么從現(xiàn)在開始這個(gè)承諾就進(jìn)入等待狀態(tài),等待下周一的到來,然后狀態(tài)改變。如果下周一我如約給了女朋友驚喜,那么這個(gè)承諾的狀態(tài)就會由pending切換為fulfilled,表示承諾成功兌現(xiàn),一旦是這個(gè)結(jié)果了,就不會再有其他結(jié)果,即狀態(tài)不會在發(fā)生改變;反之如果當(dāng)天我因?yàn)楣ぷ魈影?,把這事給忘了,說好的驚喜沒有兌現(xiàn),狀態(tài)就會由pending切換為rejected,時(shí)間不可倒流,所以狀態(tài)也不能再發(fā)生變化。
上一條我們說過Promise可以解決回調(diào)地獄的問題,沒錯(cuò),pending 狀態(tài)的 Promise 對象會觸發(fā) fulfilled/rejected 狀態(tài),一旦狀態(tài)改變,Promise 對象的 then 方法就會被調(diào)用;否則就會觸發(fā) catch。我們將上一條回調(diào)地獄的代碼改寫一下:
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) })
其實(shí)Promise也是存在一些缺點(diǎn)的,比如無法取消 Promise,錯(cuò)誤需要通過回調(diào)函數(shù)捕獲。
promise手寫實(shí)現(xiàn),面試夠用版:
function myPromise(constructor){ let self=this; self.status="pending" //定義狀態(tài)改變前的初始狀態(tài) self.value=undefined;//定義狀態(tài)為resolved的時(shí)候的狀態(tài) self.reason=undefined;//定義狀態(tài)為rejected的時(shí)候的狀態(tài) function resolve(value){ //兩個(gè)==="pending",保證了狀態(tài)的改變是不可逆的 if(self.status==="pending"){ self.value=value; self.status="resolved"; } } function reject(reason){ //兩個(gè)==="pending",保證了狀態(tài)的改變是不可逆的 if(self.status==="pending"){ self.reason=reason; self.status="rejected"; } } //捕獲構(gòu)造異常 try{ constructor(resolve,reject); }catch(e){ reject(e); } } // 定義鏈?zhǔn)秸{(diào)用的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: } }
關(guān)于Promise還有其他的知識,比如Promise.all()、Promise.race()等的運(yùn)用,由于篇幅原因就不再做展開,想要深入了解的可看下面的文章。
相關(guān)資料:
「硬核JS」深入了解異步解決方案:https://juejin.im/post/5e4613b36fb9a07ccc45e339#heading-69
【翻譯】Promises/A+規(guī)范:https://www.ituring.com.cn/article/66566#
60. `Iterator`是什么,有什么作用?
Iterator是理解第24條的先決知識,也許是我IQ不夠?,Iterator和Generator看了很多遍還是一知半解,即使當(dāng)時(shí)理解了,過一陣又忘得一干二凈。。。
Iterator(迭代器)是一種接口,也可以說是一種規(guī)范。為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署Iterator接口,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)。
Iterator語法:
const obj = { [Symbol.iterator]:function(){} }
[Symbol.iterator]屬性名是固定的寫法,只要擁有了該屬性的對象,就能夠用迭代器的方式進(jìn)行遍歷。
迭代器的遍歷方法是首先獲得一個(gè)迭代器的指針,初始時(shí)該指針指向第一條數(shù)據(jù)之前,接著通過調(diào)用 next 方法,改變指針的指向,讓其指向下一條數(shù)據(jù)
每一次的 next 都會返回一個(gè)對象,該對象有兩個(gè)屬性
value 代表想要獲取的數(shù)據(jù)
done 布爾值,false表示當(dāng)前指針指向的數(shù)據(jù)有值,true表示遍歷已經(jīng)結(jié)束
Iterator 的作用有三個(gè):
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
為各種數(shù)據(jù)結(jié)構(gòu),提供一個(gè)統(tǒng)一的、簡便的訪問接口;
使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列;
ES6 創(chuàng)造了一種新的遍歷命令for…of循環(huán),Iterator 接口主要供for…of消費(fèi)。
遍歷過程:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
創(chuàng)建一個(gè)指針對象,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置。也就是說,遍歷器對象本質(zhì)上,就是一個(gè)指針對象。
第一次調(diào)用指針對象的next方法,可以將指針指向數(shù)據(jù)結(jié)構(gòu)的第一個(gè)成員。
第二次調(diào)用指針對象的next方法,指針就指向數(shù)據(jù)結(jié)構(gòu)的第二個(gè)成員。
不斷調(diào)用指針對象的next方法,直到它指向數(shù)據(jù)結(jié)構(gòu)的結(jié)束位置。
每一次調(diào)用next方法,都會返回?cái)?shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息。具體來說,就是返回一個(gè)包含value和done兩個(gè)屬性的對象。其中,value屬性是當(dāng)前成員的值,done屬性是一個(gè)布爾值,表示遍歷是否結(jié)束。
let arr = [{num:1},2,3] let it = arr[Symbol.iterator]() // 獲取數(shù)組中的迭代器 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`函數(shù)是什么,有什么作用?
Generator函數(shù)可以說是Iterator接口的具體實(shí)現(xiàn)方式。Generator 最大的特點(diǎn)就是可以控制函數(shù)的執(zhí)行。
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}
上面這個(gè)示例就是一個(gè)Generator函數(shù),我們來分析其執(zhí)行過程:
首先 Generator 函數(shù)調(diào)用時(shí)它會返回一個(gè)迭代器
當(dāng)執(zhí)行第一次 next 時(shí),傳參會被忽略,并且函數(shù)暫停在 yield (x + 1) 處,所以返回 5 + 1 = 6
當(dāng)執(zhí)行第二次 next 時(shí),傳入的參數(shù)等于上一個(gè) yield 的返回值,如果你不傳參,yield 永遠(yuǎn)返回 undefined。此時(shí) let y = 2 * 12,所以第二個(gè) yield 等于 2 * 12 / 3 = 8
當(dāng)執(zhí)行第三次 next 時(shí),傳入的參數(shù)會傳遞給 z,所以 z = 13, x = 5, y = 24,相加等于 42
Generator 函數(shù)一般見到的不多,其實(shí)也于他有點(diǎn)繞有關(guān)系,并且一般會配合 co 庫去使用。當(dāng)然,我們可以通過 Generator 函數(shù)解決回調(diào)地獄的問題。
62. 什么是 `async/await` 及其如何工作,有什么優(yōu)缺點(diǎn)?
async/await是一種建立在Promise之上的編寫異步或非阻塞代碼的新方法,被普遍認(rèn)為是 JS異步操作的最終且最優(yōu)雅的解決方案。相對于 Promise 和回調(diào),它的可讀性和簡潔度都更高。畢竟一直then()也很煩。
async 是異步的意思,而 await 是 async wait的簡寫,即異步等待。
所以從語義上就很好理解 async 用于聲明一個(gè) function 是異步的,而await 用于等待一個(gè)異步方法執(zhí)行完成。
一個(gè)函數(shù)如果加上 async ,那么該函數(shù)就會返回一個(gè) Promise
async function test() { return "1" } console.log(test()) // -> Promise {<resolved>: "1"}
可以看到輸出的是一個(gè)Promise對象。所以,async 函數(shù)返回的是一個(gè) Promise 對象,如果在 async 函數(shù)中直接 return 一個(gè)直接量,async 會把這個(gè)直接量通過 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); }
現(xiàn)在分別用 Promise 和async/await來實(shí)現(xiàn)這三個(gè)步驟的處理。
使用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();
結(jié)果和之前的 Promise 實(shí)現(xiàn)是一樣的,但是這個(gè)代碼看起來是不是清晰得多,優(yōu)雅整潔,幾乎跟同步代碼一樣。
await關(guān)鍵字只能在async function中使用。在任何非async function的函數(shù)中使用await關(guān)鍵字都會拋出錯(cuò)誤。await關(guān)鍵字在執(zhí)行下一行代碼之前等待右側(cè)表達(dá)式(可能是一個(gè)Promise)返回。
優(yōu)缺點(diǎn):
async/await的優(yōu)勢在于處理 then 的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫出代碼,并且也能優(yōu)雅地解決回調(diào)地獄問題。當(dāng)然也存在一些缺點(diǎn),因?yàn)?await 將異步代碼改造成了同步代碼,如果多個(gè)異步代碼沒有依賴性卻使用了 await 會導(dǎo)致性能上的降低。
參考文章:
「硬核JS」深入了解異步解決方案:https://juejin.im/post/5e4613b36fb9a07ccc45e339#heading-69
以上21~25條就是JavaScript中主要的異步解決方案了,難度是有的,需要好好揣摩并加以練習(xí)。
63. instanceof的原理是什么,如何實(shí)現(xiàn)
instanceof 可以正確的判斷對象的類型,因?yàn)閮?nèi)部機(jī)制是通過判斷對象的原型鏈中是不是能找到類型的 prototype。
實(shí)現(xiàn) instanceof:
首先獲取類型的原型
然后獲得對象的原型
然后一直循環(huán)判斷對象的原型是否等于類型的原型,直到對象原型為 null,因?yàn)樵玩溩罱K為 null
function myInstanceof(left, right) { let prototype = right.prototype leftleft = left.__proto__ while (true) { if (left === null || left === undefined) return false if (prototype === left) return true leftleft = left.__proto__ } }
64. js 的節(jié)流與防抖
函數(shù)防抖 是指在事件被觸發(fā) n 秒后再執(zhí)行回調(diào),如果在這 n 秒內(nèi)事件又被觸發(fā),則重新計(jì)時(shí)。這可以使用在一些點(diǎn)擊請求的事件上,避免因?yàn)橛脩舻亩啻吸c(diǎn)擊向后端發(fā)送多次請求。
函數(shù)節(jié)流 是指規(guī)定一個(gè)單位時(shí)間,在這個(gè)單位時(shí)間內(nèi),只能有一次觸發(fā)事件的回調(diào)函數(shù)執(zhí)行,如果在同一個(gè)單位時(shí)間內(nèi)某事件被觸發(fā)多次,只有一次能生效。節(jié)流可以使用在 scroll 函數(shù)的事件監(jiān)聽上,通過事件節(jié)流來降低事件調(diào)用的頻率。
// 函數(shù)防抖的實(shí)現(xiàn) function debounce(fn, wait) { var timer = null; return function() { var context = this, args = arguments; // 如果此時(shí)存在定時(shí)器的話,則取消之前的定時(shí)器重新記時(shí) if (timer) { clearTimeout(timer); timer = null; } // 設(shè)置定時(shí)器,使事件間隔指定事件后執(zhí)行 timer = setTimeout(() => { fn.apply(context, args); }, wait); }; } // 函數(shù)節(jié)流的實(shí)現(xiàn); function throttle(fn, delay) { var preTime = Date.now(); return function() { var context = this, args = arguments, nowTime = Date.now(); // 如果兩次時(shí)間間隔超過了指定時(shí)間,則執(zhí)行函數(shù)。 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } }; }
65. 什么是設(shè)計(jì)模式?
1. 概念
設(shè)計(jì)模式是一套被反復(fù)使用的、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。使用設(shè)計(jì)模式是為了重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。毫無疑問,設(shè)計(jì)模式于己于他人于系統(tǒng)都是多贏的,設(shè)計(jì)模式使代碼編制真正工程化,設(shè)計(jì)模式是軟件工程的基石,如同大廈的一塊塊磚石一樣。
2. 設(shè)計(jì)原則
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
S – Single Responsibility Principle 單一職責(zé)原則
一個(gè)程序只做好一件事
如果功能過于復(fù)雜就拆分開,每個(gè)部分保持獨(dú)立
2. O – OpenClosed Principle 開放/封閉原則
對擴(kuò)展開放,對修改封閉
增加需求時(shí),擴(kuò)展新代碼,而非修改已有代碼
3. L – Liskov Substitution Principle 里氏替換原則
子類能覆蓋父類
父類能出現(xiàn)的地方子類就能出現(xiàn)
4. I – Interface Segregation Principle 接口隔離原則
保持接口的單一獨(dú)立
類似單一職責(zé)原則,這里更關(guān)注接口
5. D – Dependency Inversion Principle 依賴倒轉(zhuǎn)原則
面向接口編程,依賴于抽象而不依賴于具
使用方只關(guān)注接口而不關(guān)注具體類的實(shí)現(xiàn)
3. 設(shè)計(jì)模式的類型
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
結(jié)構(gòu)型模式(Structural Patterns): 通過識別系統(tǒng)中組件間的簡單關(guān)系來簡化系統(tǒng)的設(shè)計(jì)。
創(chuàng)建型模式(Creational Patterns): 處理對象的創(chuàng)建,根據(jù)實(shí)際情況使用合適的方式創(chuàng)建對象。常規(guī)的對象創(chuàng)建方式可能會導(dǎo)致設(shè)計(jì)上的問題,或增加設(shè)計(jì)的復(fù)雜度。創(chuàng)建型模式通過以某種方式控制對象的創(chuàng)建來解決問題。
3. 行為型模式(Behavioral Patterns): 用于識別對象之間常見的交互模式并加以實(shí)現(xiàn),如此,增加了這些交互的靈活性。
66. 9種前端常見的設(shè)計(jì)模式
1.外觀模式(Facade Pattern)
外觀模式是最常見的設(shè)計(jì)模式之一,它為子系統(tǒng)中的一組接口提供一個(gè)統(tǒng)一的高層接口,使子系統(tǒng)更容易使用。簡而言之外觀設(shè)計(jì)模式就是把多個(gè)子系統(tǒng)中復(fù)雜邏輯進(jìn)行抽象,從而提供一個(gè)更統(tǒng)一、更簡潔、更易用的API。很多我們常用的框架和庫基本都遵循了外觀設(shè)計(jì)模式,比如JQuery就把復(fù)雜的原生DOM操作進(jìn)行了抽象和封裝,并消除了瀏覽器之間的兼容問題,從而提供了一個(gè)更高級更易用的版本。其實(shí)在平時(shí)工作中我們也會經(jīng)常用到外觀模式進(jìn)行開發(fā),只是我們不自知而已。
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
兼容瀏覽器事件綁定
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 } };
2. 封裝接口
let myEvent = { // ... stop: e => { e.stopPropagation(); e.preventDefault(); } };
場景
設(shè)計(jì)初期,應(yīng)該要有意識地將不同的兩個(gè)層分離,比如經(jīng)典的三層結(jié)構(gòu),在數(shù)據(jù)訪問層和業(yè)務(wù)邏輯層、業(yè)務(wù)邏輯層和表示層之間建立外觀Facade
在開發(fā)階段,子系統(tǒng)往往因?yàn)椴粩嗟闹貥?gòu)演化而變得越來越復(fù)雜,增加外觀Facade可以提供一個(gè)簡單的接口,減少他們之間的依賴。
在維護(hù)一個(gè)遺留的大型系統(tǒng)時(shí),可能這個(gè)系統(tǒng)已經(jīng)很難維護(hù)了,這時(shí)候使用外觀Facade也是非常合適的,為系系統(tǒng)開發(fā)一個(gè)外觀Facade類,為設(shè)計(jì)粗糙和高度復(fù)雜的遺留代碼提供比較清晰的接口,讓新系統(tǒng)和Facade對象交互,F(xiàn)acade與遺留代碼交互所有的復(fù)雜工作。
優(yōu)點(diǎn)
減少系統(tǒng)相互依賴。
提高靈活性。
提高了安全性
缺點(diǎn)
不符合開閉原則,如果要改東西很麻煩,繼承重寫都不合適。
2. 代理模式(Proxy Pattern)
是為一個(gè)對象提供一個(gè)代用品或占位符,以便控制對它的訪問
假設(shè)當(dāng)A 在心情好的時(shí)候收到花,小明表白成功的幾率有
60%,而當(dāng)A 在心情差的時(shí)候收到花,小明表白的成功率無限趨近于0。
小明跟A 剛剛認(rèn)識兩天,還無法辨別A 什么時(shí)候心情好。如果不合時(shí)宜地把花送給A,花
被直接扔掉的可能性很大,這束花可是小明吃了7 天泡面換來的。
但是A 的朋友B 卻很了解A,所以小明只管把花交給B,B 會監(jiān)聽A 的心情變化,然后選
擇A 心情好的時(shí)候把花轉(zhuǎn)交給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()方法
優(yōu)點(diǎn)
代理模式能將代理對象與被調(diào)用對象分離,降低了系統(tǒng)的耦合度。代理模式在客戶端和目標(biāo)對象之間起到一個(gè)中介作用,這樣可以起到保護(hù)目標(biāo)對象的作用
代理對象可以擴(kuò)展目標(biāo)對象的功能;通過修改代理對象就可以了,符合開閉原則;
缺點(diǎn)
處理請求速度可能有差別,非直接訪問存在開銷
3. 工廠模式(Factory Pattern)
工廠模式定義一個(gè)用于創(chuàng)建對象的接口,這個(gè)接口由子類決定實(shí)例化哪一個(gè)類。該模式使一個(gè)類的實(shí)例化延遲到了子類。而子類可以重寫接口方法以便創(chuàng)建的時(shí)候指定自己的對象類型。
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()
場景
如果你不想讓某個(gè)子系統(tǒng)與較大的那個(gè)對象之間形成強(qiáng)耦合,而是想運(yùn)行時(shí)從許多子系統(tǒng)中進(jìn)行挑選的話,那么工廠模式是一個(gè)理想的選擇
將new操作簡單封裝,遇到new的時(shí)候就應(yīng)該考慮是否用工廠模式;
需要依賴具體環(huán)境創(chuàng)建不同實(shí)例,這些實(shí)例都有相同的行為,這時(shí)候我們可以使用工廠模式,簡化實(shí)現(xiàn)的過程,同時(shí)也可以減少每種對象所需的代碼量,有利于消除對象間的耦合,提供更大的靈活性
優(yōu)點(diǎn)
創(chuàng)建對象的過程可能很復(fù)雜,但我們只需要關(guān)心創(chuàng)建結(jié)果。
構(gòu)造函數(shù)和創(chuàng)建者分離, 符合“開閉原則”
一個(gè)調(diào)用者想創(chuàng)建一個(gè)對象,只要知道其名稱就可以了。
擴(kuò)展性高,如果想增加一個(gè)產(chǎn)品,只要擴(kuò)展一個(gè)工廠類就可以。
缺點(diǎn)
添加新產(chǎn)品時(shí),需要編寫新的具體產(chǎn)品類,一定程度上增加了系統(tǒng)的復(fù)雜度
考慮到系統(tǒng)的可擴(kuò)展性,需要引入抽象層,在客戶端代碼中均使用抽象層進(jìn)行定義,增加了系統(tǒng)的抽象性和理解難度
什么時(shí)候不用
當(dāng)被應(yīng)用到錯(cuò)誤的問題類型上時(shí),這一模式會給應(yīng)用程序引入大量不必要的復(fù)雜性.除非為創(chuàng)建對象提供一個(gè)接口是我們編寫的庫或者框架的一個(gè)設(shè)計(jì)上目標(biāo),否則我會建議使用明確的構(gòu)造器,以避免不必要的開銷。
由于對象的創(chuàng)建過程被高效的抽象在一個(gè)接口后面的事實(shí),這也會給依賴于這個(gè)過程可能會有多復(fù)雜的單元測試帶來問題。
4. 單例模式(Singleton Pattern)
顧名思義,單例模式中Class的實(shí)例個(gè)數(shù)最多為1。當(dāng)需要一個(gè)對象去貫穿整個(gè)系統(tǒng)執(zhí)行某些任務(wù)時(shí),單例模式就派上了用場。而除此之外的場景盡量避免單例模式的使用,因?yàn)閱卫J綍肴譅顟B(tài),而一個(gè)健康的系統(tǒng)應(yīng)該避免引入過多的全局狀態(tài)。
實(shí)現(xiàn)單例模式需要解決以下幾個(gè)問題:
如何確定Class只有一個(gè)實(shí)例?
如何簡便的訪問Class的唯一實(shí)例?
Class如何控制實(shí)例化的過程?
如何將Class的實(shí)例個(gè)數(shù)限制為1?
我們一般通過實(shí)現(xiàn)以下兩點(diǎn)來解決上述問題:
隱藏Class的構(gòu)造函數(shù),避免多次實(shí)例化
通過暴露一個(gè) getInstance() 方法來創(chuàng)建/獲取唯一實(shí)例
Javascript中單例模式可以通過以下方式實(shí)現(xiàn):
// 單例構(gòu)造器 const FooServiceSingleton = (function () { // 隱藏的Class的構(gòu)造函數(shù) function FooService() {} // 未初始化的單例對象 let fooService; return { // 創(chuàng)建/獲取單例對象的函數(shù) getInstance: function () { if (!fooService) { fooService = new FooService(); } return fooService; } } })();
實(shí)現(xiàn)的關(guān)鍵點(diǎn)有:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
使用 IIFE創(chuàng)建局部作用域并即時(shí)執(zhí)行;
getInstance()為一個(gè) 閉包 ,使用閉包保存局部作用域中的單例對象并返回。
我們可以驗(yàn)證下單例對象是否創(chuàng)建成功:
const fooService1 = FooServiceSingleton.getInstance(); const fooService2 = FooServiceSingleton.getInstance(); console.log(fooService1 === fooService2); // true
場景例子
定義命名空間和實(shí)現(xiàn)分支型方法
登錄框
vuex 和 redux中的store
優(yōu)點(diǎn)
劃分命名空間,減少全局變量
增強(qiáng)模塊性,把自己的代碼組織在一個(gè)全局變量名下,放在單一位置,便于維護(hù)
且只會實(shí)例化一次。簡化了代碼的調(diào)試和維護(hù)
缺點(diǎn)
由于單例模式提供的是一種單點(diǎn)訪問,所以它有可能導(dǎo)致模塊間的強(qiáng)耦合
從而不利于單元測試。無法單獨(dú)測試一個(gè)調(diào)用了來自單例的方法的類,而只能把它與那個(gè)單例作為一
個(gè)單元一起測試。
5. 策略模式(Strategy Pattern)
策略模式簡單描述就是:對象有某個(gè)行為,但是在不同的場景中,該行為有不同的實(shí)現(xiàn)算法。把它們一個(gè)個(gè)封裝起來,并且使它們可以互相替換
<html> <head> <title>策略模式-校驗(yàn)表單</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"> 手機(jī)號碼:<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; } } } // 驗(yàn)證類 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 } } } } // 調(diào)用代碼 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: '請輸入正確的手機(jī)號碼格式' }]) return validator.start() } registerForm.onsubmit = function() { let errorMsg = validataFunc() if (errorMsg) { alert(errorMsg) return false } } </script> </body> </html>
場景例子
如果在一個(gè)系統(tǒng)里面有許多類,它們之間的區(qū)別僅在于它們的'行為',那么使用策略模式可以動態(tài)地讓一個(gè)對象在許多行為中選擇一種行為。
一個(gè)系統(tǒng)需要動態(tài)地在幾種算法中選擇一種。
表單驗(yàn)證
優(yōu)點(diǎn)
利用組合、委托、多態(tài)等技術(shù)和思想,可以有效的避免多重條件選擇語句
提供了對開放-封閉原則的完美支持,將算法封裝在獨(dú)立的strategy中,使得它們易于切換,理解,易于擴(kuò)展
利用組合和委托來讓Context擁有執(zhí)行算法的能力,這也是繼承的一種更輕便的代替方案
缺點(diǎn)
會在程序中增加許多策略類或者策略對象
要使用策略模式,必須了解所有的strategy,必須了解各個(gè)strategy之間的不同點(diǎn),這樣才能選擇一個(gè)合適的strategy
6. 迭代器模式(Iterator Pattern)
如果你看到這,ES6中的迭代器 Iterator 相信你還是有點(diǎn)印象的,上面第60條已經(jīng)做過簡單的介紹。迭代器模式簡單的說就是提供一種方法順序一個(gè)聚合對象中各個(gè)元素,而又不暴露該對象的內(nèi)部表示。
迭代器模式解決了以下問題:
提供一致的遍歷各種數(shù)據(jù)結(jié)構(gòu)的方式,而不用了解數(shù)據(jù)的內(nèi)部結(jié)構(gòu)
提供遍歷容器(集合)的能力而無需改變?nèi)萜鞯慕涌?/p>
一個(gè)迭代器通常需要實(shí)現(xiàn)以下接口:
hasNext():判斷迭代是否結(jié)束,返回Boolean
next():查找并返回下一個(gè)元素
為Javascript的數(shù)組實(shí)現(xiàn)一個(gè)迭代器可以這么寫:
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++]; } }
驗(yàn)證一下迭代器是否工作:
const iterator = new Iterator(item); while(iterator.hasNext()){ console.log(iterator.next()); } //輸出:1, red, false, 3.14
ES6提供了更簡單的迭代循環(huán)語法 for…of,使用該語法的前提是操作對象需要實(shí)現(xiàn) 可迭代協(xié)議(The iterable protocol),簡單說就是該對象有個(gè)Key為 Symbol.iterator 的方法,該方法返回一個(gè)iterator對象。
比如我們實(shí)現(xiàn)一個(gè) Range 類用于在某個(gè)數(shù)字區(qū)間進(jìn)行迭代:
function Range(start, end) { return { [Symbol.iterator]: function () { return { next() { if (start < end) { return { value: start++, done: false }; } return { done: true, value: end }; } } } } }
驗(yàn)證一下:
for (num of Range(1, 5)) { console.log(num); } // 輸出:1, 2, 3, 4
7. 觀察者模式(Observer Pattern)
觀察者模式又稱發(fā)布-訂閱模式(Publish/Subscribe Pattern),是我們經(jīng)常接觸到的設(shè)計(jì)模式,日常生活中的應(yīng)用也比比皆是,比如你訂閱了某個(gè)博主的頻道,當(dāng)有內(nèi)容更新時(shí)會收到推送;又比如JavaScript中的事件訂閱響應(yīng)機(jī)制。觀察者模式的思想用一句話描述就是:被觀察對象(subject)維護(hù)一組觀察者(observer),當(dāng)被觀察對象狀態(tài)改變時(shí),通過調(diào)用觀察者的某個(gè)方法將這些變化通知到觀察者。
觀察者模式中Subject對象一般需要實(shí)現(xiàn)以下API:
subscribe(): 接收一個(gè)觀察者observer對象,使其訂閱自己
unsubscribe(): 接收一個(gè)觀察者observer對象,使其取消訂閱自己
fire(): 觸發(fā)事件,通知到所有觀察者
用JavaScript手動實(shí)現(xiàn)觀察者模式:
// 被觀察者 function Subject() { this.observers = []; } Subject.prototype = { // 訂閱 subscribe: function (observer) { this.observers.push(observer); }, // 取消訂閱 unsubscribe: function (observerToRemove) { thisthis.observers = this.observers.filter(observer => { return observer !== observerToRemove; }) }, // 事件觸發(fā) fire: function () { this.observers.forEach(observer => { observer.call(); }); } }
驗(yàn)證一下訂閱是否成功:
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!
驗(yàn)證一下取消訂閱是否成功:
subject.unsubscribe(observer2); subject.fire(); //輸出: Observer 1 Firing!
場景
DOM事件
document.body.addEventListener('click', function() { console.log('hello world!'); }); document.body.click()
vue 響應(yīng)式
優(yōu)點(diǎn)
支持簡單的廣播通信,自動通知所有已經(jīng)訂閱過的對象
目標(biāo)對象與觀察者之間的抽象耦合關(guān)系能單獨(dú)擴(kuò)展以及重用
增加了靈活性
觀察者模式所做的工作就是在解耦,讓耦合的雙方都依賴于抽象,而不是依賴于具體。從而使得各自的變化都不會影響到另一邊的變化。
缺點(diǎn)
過度使用會導(dǎo)致對象與對象之間的聯(lián)系弱化,會導(dǎo)致程序難以跟蹤維護(hù)和理解
8. 中介者模式(Mediator Pattern)
在中介者模式中,中介者(Mediator)包裝了一系列對象相互作用的方式,使得這些對象不必直接相互作用,而是由中介者協(xié)調(diào)它們之間的交互,從而使它們可以松散偶合。當(dāng)某些對象之間的作用發(fā)生改變時(shí),不會立即影響其他的一些對象之間的作用,保證這些作用可以彼此獨(dú)立的變化。
中介者模式和觀察者模式有一定的相似性,都是一對多的關(guān)系,也都是集中式通信,不同的是中介者模式是處理同級對象之間的交互,而觀察者模式是處理Observer和Subject之間的交互。中介者模式有些像婚戀中介,相親對象剛開始并不能直接交流,而是要通過中介去篩選匹配再決定誰和誰見面。
場景
例如購物車需求,存在商品選擇表單、顏色選擇表單、購買數(shù)量表單等等,都會觸發(fā)change事件,那么可以通過中介者來轉(zhuǎn)發(fā)處理這些事件,實(shí)現(xiàn)各個(gè)事件間的解耦,僅僅維護(hù)中介者對象即可。
var goods = { //手機(jī)庫存 '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 = { // 發(fā)送消息 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; }, // 發(fā)送消息 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
優(yōu)點(diǎn)
使各對象之間耦合松散,而且可以獨(dú)立地改變它們之間的交互
中介者和對象一對多的關(guān)系取代了對象之間的網(wǎng)狀多對多的關(guān)系
如果對象之間的復(fù)雜耦合度導(dǎo)致維護(hù)很困難,而且耦合度隨項(xiàng)目變化增速很快,就需要中介者重構(gòu)代碼
缺點(diǎn)
系統(tǒng)中會新增一個(gè)中介者對象,因?yàn)閷ο笾g交互的復(fù)雜性,轉(zhuǎn)移成了中介者對象的復(fù)雜性,使得中介者對象經(jīng)常是巨大的。中介 者對象自身往往就是一個(gè)難以維護(hù)的對象。
9. 訪問者模式(Visitor Pattern)
訪問者模式 是一種將算法與對象結(jié)構(gòu)分離的設(shè)計(jì)模式,通俗點(diǎn)講就是:訪問者模式讓我們能夠在不改變一個(gè)對象結(jié)構(gòu)的前提下能夠給該對象增加新的邏輯,新增的邏輯保存在一個(gè)獨(dú)立的訪問者對象中。訪問者模式常用于拓展一些第三方的庫和工具。
// 訪問者 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)
訪問者模式的實(shí)現(xiàn)有以下幾個(gè)要素:
Visitor Object:訪問者對象,擁有一個(gè)visit()方法
Receiving Object:接收對象,擁有一個(gè)accept() 方法
visit(receivingObj):用于Visitor接收一個(gè)Receiving Object
accept(visitor):用于Receving Object接收一個(gè)Visitor,并通過調(diào)用Visitor的 visit() 為其提供獲取Receiving Object數(shù)據(jù)的能力
簡單的代碼實(shí)現(xiàn)如下:
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); } }
驗(yàn)證一下:
const employee = new Employee('bruce', 1000); const visitor = new Visitor(); employee.accept(visitor); console.log(employee.getSalary());//輸出:2000
場景
對象結(jié)構(gòu)中對象對應(yīng)的類很少改變,但經(jīng)常需要在此對象結(jié)構(gòu)上定義新的操作
需要對一個(gè)對象結(jié)構(gòu)中的對象進(jìn)行很多不同的并且不相關(guān)的操作,而需要避免讓這些操作"污染"這些對象的類,也不希望在增加新操作時(shí)修改這些類。
優(yōu)點(diǎn)
符合單一職責(zé)原則
優(yōu)秀的擴(kuò)展性
靈活性
缺點(diǎn)
具體元素對訪問者公布細(xì)節(jié),違反了迪米特原則
違反了依賴倒置原則,依賴了具體類,沒有依賴抽象。
具體元素變更比較困難
關(guān)于66條JavaScript面試知識點(diǎn)和答案解析就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。