「offer来了」保姆级巩固你的js知识体系(4.0w字)
「面試專欄」前端面試之JavaScript篇
- 🧐序言
- 🥳思維導圖環節
- 😏一、JS規范
- 1、說幾條JavaScript的基本規范。
- 2、對原生JavaScript的了解。
- 3、說下對JS的了解吧。
- 4、JS原生拖拽節點
- 5、談談你對ES6的理解
- 6、知道ES6的class嘛?
- 7、說說你對AMD和Commonjs的理解
- 8、如何理解前端模塊化
- 9、面向對象編程思想
- 10、用過 TypeScript 嗎?它的作用是什么?
- 11、PWA使用過嗎?serviceWorker的使用原理是啥?
- 😲二、數據類型
- 1、問:0.1+0.2 === 0.3嗎?為什么?
- 2、js數據類型有哪些?具體存在哪里?判斷方式是什么?
- 3、什么是淺拷貝?什么是深拷貝?說明并分別寫出代碼。
- (1)淺拷貝
- (2)深拷貝
- 4、JS整數是怎么表示的?
- 5、Number的存儲空間是多大?如果后臺發送了一個超過最大數字怎么辦?
- 6、NAN是什么,用typeof會輸出什么?
- 7、Symbol有什么用處?
- 8、null,undefined的區別
- 9、JS隱式轉換,顯示轉換
- 10、介紹下js有哪些內置對象
- 11、js有哪些方法定義對象
- 12、如何判斷一個對象是不是空對象?
- 13、手寫題:獲取url參數getUrlParams(url)
- 14、數組能夠調用的函數有哪些?
- 15、函數中的arguments是數組嗎?類數組轉數組的方法了解一下?
- 16、手寫題:如何判斷數組類型?
- 17、手寫題:sort快速打亂數組
- 18、手寫題:數組去重操作
- 19、手寫題:數組扁平化
- 20、`new` 操作符具體干了什么呢?
- 21、手寫題:手寫一個new方法
- 22、js如何實現繼承?
- 23、JS中的垃圾回收機制
- 🤐三、作用域、原型鏈、閉包
- 1、作用域
- (1)什么是作用域?
- (2)什么是作用域鏈?
- 2、原型鏈
- (1)什么是原型?什么是原型鏈?
- (2)什么是原型鏈繼承?
- (3)手寫題:原型鏈之instance原理
- 3、閉包
- (1)閉包是什么?
- (2)js代碼的執行過程
- (3)一般如何產生閉包?
- (4)閉包產生的本質
- (5)閉包的特性
- (6)閉包的優缺點
- (7)解決方法
- (8)let閉包
- (9)閉包的應用場景
- (10)手寫題:函數柯里化
- (11)補充
- 4、變量對象
- (1)變量對象
- (2)活動對象
- (3)變量提升
- 😜四、事件
- 1、事件模型
- 2、事件是如何實現的?
- 3、怎么加事件監聽?
- 4、什么是事件委托?
- (1)定義
- (2)原理
- (3)好處
- (4)補充
- 5、說說事件循環 event loop
- (1)定義
- (2)常用的宏任務和微任務
- (3)setTimeout(fn,0)多久才執行,Event loop?
- (4)補充
- 🤪五、this問題
- 1、描述下this(談談對this對象的理解)
- 2、this綁定的四大規則
- (1)New綁定
- (2)顯式綁定
- (3)隱式綁定
- (4)默認綁定
- 3、如果一個構造函數,bind了一個對象,用這個構造函數創建出的實例會繼承這個對象的屬性嗎?為什么?
- 4、箭頭函數和普通函數有啥區別?箭頭函數能當構造函數嗎?
- (1)箭頭函數和普通函數定義
- (2)箭頭函數和普通函數的區別
- 5、了解this嘛,apply,call,bind具體指什么?
- (1)三者的區別
- (2)傳參方式
- (3)手寫apply、call、bind
- 😋六、Ajax問題
- 1、Ajax原理
- 2、Ajax解決瀏覽器緩存問題
- 3、js單線程
- 4、異步編程的實現方式
- (1)回調函數
- (2)事件監聽(采用時間驅動模式,取決于某個事件是否發生)
- (3)發布/訂閱(觀察者模式)
- (4)Promise 對象
- (5)Generator 函數
- (6)async 函數
- 5、js腳本加載問題,async、defer問題
- 6、關于window.onload 和 DOMContentLoaded
- 7、ajax、axios、fetch區別
- (1)ajax
- (2)axios
- (3)fetch
- 8、手寫題:手寫Ajax函數
- 9、手寫題:手寫Promise原理
- 10、手寫題:基于Promise手寫Promise.all
- 🥰七、手寫題補充
- 1、性能優化相關
- (1)手寫節流函數
- (2)手寫防抖函數
- (3)圖片懶加載
- 2、原生API手寫
- (1)forEach
- (2)map
- (3)filter
- (4)reduce
- 3、其余手寫題
- (1)JSONP的實現
- (2)Object.create
- (3)Object.assign
- (4)手寫發布訂閱
- 😉八、結束語
- 🐣彩蛋 One More Thing
- (:pdf內容獲取
- (:更新地址
- (:番外篇
🧐序言
大家都知道, js 在前端面試中的占比可以說是非常大了。基本上在每一場面試中,有 40% 以上的題都是 js 的題目。 js 不僅考察一個前端人的基礎能力,更重要的是前端可以說是以 js 為本,所以也很考察我們的代碼能力和邏輯思維。如果說在面試前端中 js 都不過關,那其實還是蠻危險的。
下面的這篇文章中,將講解我整個秋招備試過程的所有題目。其中,有些知識點是一個很大的范圍,但是放在面試系列中整理的話只能是概括性介紹,我將會以鏈接的方式,將我之前寫的文章和其他相關模塊的文章,放在題目后進行標注,方便大家更詳細的了解當下模塊的擴展知識點。
下面開始本文的講解~📚
🥳思維導圖環節
在真正開篇之前,先用一張思維導圖來了解全文的內容。詳情見下圖👇
思維導圖收入囊中了,就該開始來架起 js 的知識體系啦~
😏一、JS規范
1、說幾條JavaScript的基本規范。
- for-in 循環中的變量應該使用let關鍵字明確限定作用域,從而避免作用域污染。
- 比較布爾值/數值時,需用 === / !== 來比較;
- switch 語句必須帶有 default 分支;
- 不要使用全局函數;
- 使用對象字面量替代 new Array 這種形式,以下給出對象字面量的例子。
2、對原生JavaScript的了解。
數據類型、運算、對象、 Function 、繼承、閉包、作用域、原型鏈、事件、RegExp 、JSON 、Ajax 、DOM 、BOM 、內存泄漏、異步裝載、模板引擎、前端MVC 、路由、模塊化、Canvas 、ECMAScript 。
3、說下對JS的了解吧。
是基于原型的動態語言,主要特性有this、原型和原型鏈。
JS嚴格意義上來說分為:語言標準部分( ECMAScript )+ 宿主環境部分。
語言標準部分
- 2015年發布 ES6 ,引入諸多特性,使得能夠編寫大型項目成為可能,標準自 2015年 之后以年號作為代號,每年一更。
宿主環境部分
- 在瀏覽器宿主環境包括 DOM + BOM 等
- 在 Node ,宿主環境包括一些文件、數據庫、網絡、與操作系統的交互等
4、JS原生拖拽節點
- 給需要拖拽的節點綁定 mousedown ,mousemove ,mouseup 事件。
- mousedown 事件觸發后,開始拖拽。
- mousemove 時,需要通過 event.clientX 和 clientY 獲取拖拽位置,并實時更新位置。
- mouseup 時,拖拽結束。
- 需要注意瀏覽器邊界值,設置拖拽范圍。
5、談談你對ES6的理解
- 新增模板字符串(為 JavaScript 提供了簡單的字符串插值功能)。
- 箭頭函數。
- for-of(用來遍歷數據——例如數組中的值)。
- arguments 對象可以被不確定的參數和默認參數完美替代。
- ES6 將 promise 對象納入規范,提供了原生的 promise 對象。
- 增加了 let 和 const 命令,用來聲明變量。
- 還有就是引入 module 模塊的概念。
6、知道ES6的class嘛?
ES6 中的 class 是,為這個類的函數對象直接添加方法,而不是加在這個函數對象的原型對象上。
7、說說你對AMD和Commonjs的理解
- CommonJS 是服務器端模塊的規范,Node.js 采用了這個規范。
- CommonJS 規范加載模塊是同步的,也就是說,只有加載完成,才能執行后面的操作。AMD 規范則是非同步加載模塊,允許指定回調函數。
- AMD 推薦的風格通過返回一個對象作為模塊對象。CommonJS 的風格則是通過對 module.exports 或 exports 的屬性賦值來達到暴露模塊對象的目的。
8、如何理解前端模塊化
前端模塊化就是復雜的文件編程中一個個獨立的模塊,比如js文件等等,分成獨立的模塊有利于重用(復用性)和維護(版本迭代),這樣會引來模塊之間相互依賴的問題,所以有了commonJS規范,AMD,CMD規范等等,以及用于js打包(變異等處理)的工具webpack。
9、面向對象編程思想
- 基本思想是使用對象,類,繼承,封裝等基本概念來進行程序設計;
- 易維護;
- 易擴展;
- 開發工作的重用性、繼承性高,降低重復工作量;
- 縮短了開發周期。
10、用過 TypeScript 嗎?它的作用是什么?
TypeScript 為 JS 添加類型支持,以及提供最新版的 ES 語法的支持,有利于團隊協作和排錯,開發大型項目。
11、PWA使用過嗎?serviceWorker的使用原理是啥?
漸進式網絡應用(PWA)是谷歌在 2015年底 提出的概念。基本上算是web應用程序,但在外觀和感覺上與 原生app 類似。支持 PWA 的網站可以提供脫機工作、推送通知和設備硬件訪問等功能。
Service Worker 是瀏覽器在后臺獨立于網頁運行的腳本,它打開了通向不需要網頁或用戶交互的功能的大門。 現在,它們已包括如推送通知和后臺同步等功能。 將來, Service Worker 將會支持如定期同步或地理圍欄等其他功能。
注:漸進式網絡應用 Progressive Network Application
😲二、數據類型
1、問:0.1+0.2 === 0.3嗎?為什么?
在正常的數學邏輯思維中, 0.1+0.2=0.3 這個邏輯是正確的,但是在 JavaScript 中 0.1+0.2 !== 0.3 ,這是為什么呢?這個問題也會偶爾被用來當做面試題來考查面試者對 JavaScript 的數值的理解程度。
0.1 + 0.2 == 0.3 // false在 JS 中,二進制的浮點數 0.1 和 0.2 并不是精確的,所以它們相加的結果并非正好等于 0.3 ,而是一個比較接近 0.3 的數字 0.30000000000000004 ,所以條件判斷結果為 false 。
原因在于在 JS 當中,采用的是 IEEE 754 的雙精度標準,所以計算機內部在存儲數據編碼的時候,0.1在計算機內部不是精確的 0.1 ,而是一個有舍入誤差的 0.1 。當代碼被編譯或解析后, 0.1 已經被四舍五入成一個與之很接近的計算機內部數字,以至于計算還沒開始,一個很小的舍入錯誤就已經產生了。這也就是 0.1 + 0.2 不等于 0.3 的原因。
那如何避免這樣的問題?
最常用的方法就是將浮點數轉化成整數計算,因為整數都是可以精確表示的。
通常就是把計算數字提升10的N次方倍再除以 10 的 N 次方,一般都用 1000 就行了。
(0.1*1000 + 0.2*1000)/1000 == 0.3 //true2、js數據類型有哪些?具體存在哪里?判斷方式是什么?
(1)js數據類型
js 數據類型包括基本數據類型和引用數據類型。
(2)具體存放在哪里?
基本數據類型:
基本數據類型,是指 Numer 、 Boolean 、 String 、 null 、 undefined 、 Symbol (ES6新增的)、 BigInt(ES2020) 等值,它們在內存中都是存儲在棧中的,即直接訪問該變量就可以得到存儲在棧中的對應該變量的值。
若將一個變量的值賦值給另一個變量,則這兩個變量在內存中是獨立的,修改其中任意一個變量的值,不會影響另一個變量。這就是基本數據類型。
引用數據類型:
那引用數據類型呢,是指 Object 、 Array 、 Function 等,他們在內存中是存在于棧和堆當中的,即我們要訪問到引用類型的值時,需要先訪問到該變量在棧中的地址(指向堆中的值),然后再通過這個地址,訪問到存放在堆中的數據。這就是引用數據類型。
(3) 常用判斷方式:typeof、instanceof、===
1)typeof:
定義:返回數據類型的字符串表達(小寫)
用法:typeof + 變量
可以判斷:
-
undefined/ 數值 / 字符串 / 布爾值 / function (返回 ‘undefined’ / ‘number’ / ‘string’ / ‘boolean’ / ‘function’)
-
null與object 、object與array (null、array、object都會返回 ‘object’ )
2)instanceof:
定義:判斷對象的具體類型
用法:b instanceof A →表明 b 是否是 A 的實例對象
可以判斷:
專門用來判斷對象數據的類型: Object , Array 與 Function
判斷 String , Number , Boolean 這三種類型的數據時,直接賦值為 false ,調用構造函數創建的數據為 true
<script type="text/javascript">let str = new String("hello world") //console.log(str instanceof String); → truestr = "hello world" //console.log(str instanceof String); → falselet num = new Number(44) //console.log(num instanceof Number); → truenum = 44 //console.log(num instanceof Number); → falselet bool = new Boolean(true) //console.log(bool instanceof Boolean); → truebool = true //console.log(bool instanceof Boolean); → false</script> <script type="text/javascript">var items = []; var object = {}; function reflect(value) { return value;} console.log(items instanceof Array); // true console.log(items instanceof Object); // true console.log(object instanceof Object); // true console.log(object instanceof Array); // false console.log(reflect instanceof Function); // true console.log(reflect instanceof Object); // true3)===:
可以判斷: undefined , null
<script type="text/javascript">let str;console.log(typeof str, str === undefined); //'undefined', truelet str2 = null;console.log(typeof str2, str2 === null); // 'object', true</script>3、什么是淺拷貝?什么是深拷貝?說明并分別寫出代碼。
(1)淺拷貝
所謂淺拷貝,就是一個變量賦值給另一個變量,其中一個變量的值改變,則兩個變量的值都變了,即對于淺拷貝來說,是數據在拷貝后,新拷貝的對象內部仍然有一部分數據會隨著源對象的變化而變化。
// 分析 function shallowCopy(obj){let copyObj = {};for(let i in obj){copyObj[i] = obj[i];}return copyObj; }// 實例 let a = {name: '張三',age: 19,like: ['打籃球', '唱歌', '跳舞'] }let b = shallowCopy(a);a.name = '李四'; a.like[0] = '打打乒乓球'; console.log(a); console.log(b);(2)深拷貝
定義:深拷貝 就是,新拷貝的對象內部所有數據都是獨立存在的,不會隨著源對象的改變而改變。
深拷貝有兩種方式:遞歸拷貝和利用JSON函數進行深拷貝。
- 遞歸拷貝的實現原理是:對變量中的每個元素進行獲取,若遇到基本類型值,直接獲取;若遇到引用類型值,則繼續對該值內部的每個元素進行獲取。
- JSON深拷貝的實現原理是:將變量的值轉為字符串形式,然后再轉化為對象賦值給新的變量。
局限性:深拷貝的局限性在于,會忽略 undefined ,不能序列化函數,不能解決循環引用的對象。
遞歸拷貝方式實現代碼:
// 分析 function deepCopy(obj){// 判斷是否為引用數據類型if(typeof obj === 'object'){let result = obj.constructor === Array ? [] : {};// 對引用類型繼續進行遍歷,如果遍歷沒有結束的話for(let i in obj){result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];}return result;}// 為基本數據類型,直接賦值返回else{return obj;} }// 實例 - 利用遞歸函數做深拷貝 let c = {name:'張三',age:12,like:['打乒乓球','打羽毛球','打太極'] }let d = deepCopy(c);c.name = '李四'; c.like[0] = '打籃球'; console.log(c); console.log(d);JSON深拷貝實現代碼:
// 實例 - 利用json函數做深拷貝 let e = {name: '張三',age: 19,like:['打羽毛球', '唱歌', '跳舞'] }let f = JSON.parse(JSON.stringify(e));// 注意: JSON函數做深度拷貝時不能拷貝正則,Date,方法函數等e.name = '李四'; e.like[0] = '打乒乓球';// console.log(e); // console.log(f);這里可以在參考我之前寫過的一篇文章輔助理解👉棧在前端中的應用,順便再了解下深拷貝和淺拷貝!
4、JS整數是怎么表示的?
JS整數通過 Number 類型來表示,遵循 IEEE 754 標準,通過 64位 來表示一個數字,即 1+11+52 (符號位+指數位+小數部分有效位),最大安全數字是 253 - 1,對應 16位 十進制數。
注:1位十進制數對應4位二進制數
5、Number的存儲空間是多大?如果后臺發送了一個超過最大數字怎么辦?
Math.pow(2,53),53為有效數字;如果后臺發送一個超過最大數字,會發生截斷,等于 JS 能支持的最大安全數字 253 - 1。
6、NAN是什么,用typeof會輸出什么?
Not a Number,表示非數字。
typeof NaN === 'number'; //true7、Symbol有什么用處?
- 可以用來表示一個獨一無二的變量,防止命名沖突。
- 除此之外, Symbol 還可以用來模擬私有屬性。
- 詳細文章補充👇
- 原文:面試官:JavaScript 原始數據類型 Symbol 有什么用?
- 鏈接:https://www.cnblogs.com/lzkwin/p/12666300.html
8、null,undefined的區別
- undefined 表示不存在這個值。
- undefined 是一個表示“無”的原始值或者說表示“缺少值”,就是此處應該有一個值,但是還沒有定義。嘗試讀取時就會返回 undefined 。
- 例如變量被聲明了,但沒有賦值時,就等于 undefined 。
- null 表示一個對象被定義了,值為“空值”。
- null 是一個對象(空對象,沒有任何屬性和方法)。
- 例如作為函數的參數時,表示該函數的參數不是對象。
- 在驗證 null 時,一定要使用 === ,因為 == 無法區分 null 和 undefined 。
9、JS隱式轉換,顯示轉換
一般非基礎類型進行轉換時會調用valueOf,如果 valueOf 無法返回基本類型值,就會調用toString。
(1)字符串和數字
- “+”操作符,如果有一個為字符串,那么都轉化到字符串然后執行字符串拼接。
- “-”操作符,轉換為數字,相減(-a, a*1, a/1)都能進行隱式強制類型轉換。
(2)布爾值到數字
- 1 + true = 2;
- 1 + false = 1;
(3)轉換為布爾值
- for中第二個
- while
- if
- 三元表達式
- || (邏輯或)和 &&(邏輯與)左邊的操作個數
(4)符號
- 不能被轉換為數字
- 能被轉換為布爾值(都是true)
- 可以被轉換成字符串“Symbol(cool)”
(5)寬松相等和嚴格相等
寬松相等允許進行強制類型轉換,而嚴格相等不允許。
①字符串與數字
- 轉換為數字然后比較
②其他類型與布爾類型
- 先把布爾類型轉換為數字,然后繼續進行比較
③對象與非對象
- 執行對象的 ToPrimitive (對象)然后繼續進行比較
④假值列表
- undefined
- null
- false
- +0,-0,NaN
- “”
10、介紹下js有哪些內置對象
- Object 是 Javascript 中所有對象的父對象;
- 其他數據封裝類對象:Object 、Array 、Boolean 、Number 和 String;
- 其他對象:Function 、Arguments 、Math 、Date 、RegExp 、Error。
11、js有哪些方法定義對象
- 對象字面量:let obj = {} ;
- 構造函數:let obj = new Object() ;
- Object.create():let obj = Object.create(object.prototype) ;
12、如何判斷一個對象是不是空對象?
Object.keys(obj).length === 013、手寫題:獲取url參數getUrlParams(url)
//封裝函數getUrlParams, 將URL地址的參數解析為對象 function getUrlParams(url){let obj = {};if(url.indexOf('?') === -1){return obj;}let first_res = url.split('?')[1];let second_res = first_res.split('&');for(let i in second_res){third = second_res[i].split('=');obj[third[0]] = third[1];}return obj; }// 測試代碼let URL = 'https://www.sogou.com/web?ie=UTF-8&query=搜索內容&_em=3'; console.log(getUrlParams(URL));14、數組能夠調用的函數有哪些?
- push 向數組尾部添加元素
- pop 刪除并返回數組最后一個元素
- splice 添加/刪除元素
- slice 返回選定的元素
- shift 刪除第一個元素并返回
- unshift 向數組開頭添加一個或更多元素,并返回新長度
- sort 對數組元素進行排序
- find 返回通過測試的數組的第一個元素
- findIndex
- map/filter/reduce 等函數式編程方法
- 原型鏈上的方法:toString/valueOf
15、函數中的arguments是數組嗎?類數組轉數組的方法了解一下?
是類數組,是屬于鴨子類型的范疇,只是長得像數組。
- … 運算符
- Array.from
- Array.prototype.slice.apply(arguments)
16、手寫題:如何判斷數組類型?
// 方法一:instanceof方法 let arr = [1, 2, 3]; console.log(arr instanceof Array);// 方法二:constructor方法 let arr = [1, 2, 3]; console.log(arr.constructor === Array);// 方法三:isArray方法 let arr = [1, 2, 3]; console.log(Array.isArray(arr));// 方法四:Object.prototype方法 let arr = [1, 2, 3]; console.log(Object.prototype.toString.call(arr) === '[object Array]');// 方法五:Array.__proto__方法 let arr = [1, 2, 3]; console.log(arr.__proto__ === Array.prototype);// 方法六:Object.getPrototypeOf方法 let arr = [1, 2, 3]; console.log(Object.getPrototypeOf(arr) === Array.prototype);// 方法七:Array.prototype.isPrototypeOf方法 let arr = [1, 2, 3]; console.log(Array.prototype.isPrototypeOf(arr));17、手寫題:sort快速打亂數組
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; arr.sort(() => Math.random() - 0.5); //利用sort,返回結果為大于等于0時被交換位置,小于0時交換位置。18、手寫題:數組去重操作
/* 數組去重:讓數組所有元素都獨一無二,沒有重復元素 */// 創建一個含有重復元素的數組 let arr = [1, 1, 2, 3, 3, 6, 7, 2, 9, 9]// 第一種方法:利用 Set數據結構 + Array.from() 函數 function removeRepeat1(arr) {return Array.from(new Set(arr)) }// 第二種方法: 利用 Set數據結構 + ...擴展運算符 function removeRepeat2(arr) {return [...new Set(arr)] }// 第三種方法: 利用 indexOf 函數 function removeRepeat3(arr) {let new_arr = []for(let i in arr) {let item = arr[i]if(new_arr.indexOf(item) === -1) {new_arr.push(item)}}return new_arr }// 第四種方法: 利用 includes 函數 function removeRepeat4(arr) {let new_arr = []for(let i in arr) {let item = arr[i]if(!new_arr.includes(item)) {new_arr.push(item)}}return new_arr }// 第五種方法: 利用 filter 函數 function removeRepeat5(arr) {return arr.filter((value, index) => {return arr.indexOf(value) === index}) }// 第六種方法: 利用 Map 數據結構 function removeRepeat6(arr) {let map = new Map()let new_arr = []for(let i in arr) {let item = arr[i]if(!map.has(item)) {map.set(item, true)new_arr.push(item)}}return new_arr }// 測試方法 console.log(removeRepeat1(arr)); console.log(removeRepeat2(arr)); console.log(removeRepeat3(arr)); console.log(removeRepeat4(arr)); console.log(removeRepeat5(arr)); console.log(removeRepeat6(arr));19、手寫題:數組扁平化
/* 數組扁平化就是將多維數組轉成一維數組 */// 多維數組 let arr = [1, 2, [3, 4, [6, 7]]]// 第一種方法:利用 flat() 函數 function flatArr1(arr) {return arr.flat(Infinity) }// 第二種方法: 正則匹配 function flatArr2(arr) {return JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']') }// 第三種方法:利用 reduce() 遍歷所有的元素 function flatArr3(arr) {return arr.reduce((i, j) => {return i.concat(Array.isArray(j)? flatArr3(j) : j)}, []) }// 第四種方法:直接使用遞歸函數 function flatArr4(arr) {let new_arr = [] function innerArr(v) {for(let i in v) {let item = v[i]if(Array.isArray(item)) {innerArr(item)} else {new_arr.push(item)}}}innerArr(arr)return new_arr }// 方法測試 console.log(flatArr1(arr)); console.log(flatArr2(arr)); console.log(flatArr3(arr)); console.log(flatArr4(arr));20、new 操作符具體干了什么呢?
new 一個對象的過程是:
- 創建一個空對象;
- 對新對象進行 [prototype] 綁定(即 son. __ proto __ =father.prototype );
- 新對象和函數調用的 this 會綁定起來;
- 執行構造函數中的方法;
- 如果函數沒有返回值則自動返回這個新對象。
21、手寫題:手寫一個new方法
function father(name){this.name = name;this.sayname = function(){console.log(this.name);} }function myNew(ctx, ...args){ //...args為ES6展開符,也可以使用arguments// 先用Oject創建一個空的對象let obj = new Object();// 新對象會執行prototype連接obj.__proto__ = ctx.prototype;// 新對象和函數調用的this綁定起來let res = ctx.call(obj, ...args);// 判斷函數返回值如果是null或者undefined則返回obj,否則就返回resreturn res instanceof Object ? res : obj; }let son = myNew(father, 'Bob'); son.sayname();22、js如何實現繼承?
- 原型鏈繼承
- 盜用構造函數繼承
- 組合繼承
- 原型式繼承
- 繼承式繼承
- 寄生式組合繼承
- class的繼承
- 詳解文章補充👇
- 原文:一文梳理JavaScript中常見的七大繼承方案
- 鏈接:https://blog.csdn.net/weixin_44803753/article/details/119280627
- 碎碎念:對于js的繼承問題來說,要明確幾種繼承之間的關系,以及各自的優缺點,還有手寫每一種繼承。
23、JS中的垃圾回收機制
簡單來說,垃圾回收機制就是,清除無用變量,釋放更多內存,展現更好性能。
必要性:由于字符串、對象和數組沒有固定大小,所有只有當他們的大小已知時,才能對他們進行動態的存儲分配。
JavaScript 程序每次創建字符串、數組或對象時,解釋器都必須分配內存來存儲那個實體。只要像這樣動態地分配了內存,最終都要釋放這些內存以便他們能夠被再用,否則, JavaScript 的解釋器將會消耗完系統中所有可用的內存,造成系統崩潰。
這段話解釋了為什么系統需要垃圾回收, JS 不像 C/C++ ,它有自己的一套垃圾回收機制(GarbageCollection)。
JavaScript 的解釋器可以檢測到何時程序不再使用一個對象了,當他確定了一個對象是無用的時候,他就知道不再需要這個對象,可以把它所占用的內存釋放掉了。
例如:
var a="hello world"; var b="world"; var a=b; //這時,會釋放掉"hello world",釋放內存以便再引用垃圾回收的方法:標記清除法、引用計數法。
標記清除法
這是最常見的垃圾回收方式,當變量進入環境時,就標記這個變量為”進入環境“,從邏輯上講,永遠不能釋放進入環境的變量所占的內存,只要執行流程進入相應的環境,就可能用到他們。當離開環境時,就標記為“離開環境”。
垃圾回收器在運行的時候會給存儲在內存中的變量都加上標記(所有都加),然后去掉環境變量中的變量,以及被環境變量中的變量所引用的變量(條件性去除標記),刪除所有被標記的變量,刪除的變量無法在環境變量中被訪問所以會被刪除,最后垃圾回收器完成了內存的清除工作,并回收他們所占用的內存。
引用計數法
另一種不太常見的方法就是引用計數法,引用計數法的意思就是每個值沒有引用的次數。當聲明了一個變量,并用一個引用類型的值賦值給該變量,則這個值的引用次數為 1 ;相反的,如果包含了對這個值引用的變量又取得了另外一個值,則原先的引用值引用次數就減 1 ,當這個值的引用次數為 0 的時候,說明沒有辦法再訪問這個值了,因此就把所占的內存給回收進來,這樣垃圾收集器再次運行的時候,就會釋放引用次數為 0 的這些值。
用引用計數法會存在內存泄露,下面來看原因:
function problem() { var objA = new Object();var objB = new Object();objA.someOtherObject = objB;objB.anotherObject = objA; }在這個例子里面, objA 和 objB 通過各自的屬性相互引用,這樣的話,兩個對象的引用次數都為 2 ,在采用引用計數的策略中,由于函數執行之后,這兩個對象都離開了作用域,函數執行完成之后,因為計數不為 0 ,這樣的相互引用如果大量存在就會導致內存泄露。
特別是在 DOM 對象中,也容易存在這種問題:
var element=document.getElementById(’‘); var myObj=new Object(); myObj.element=element; element.someObject=myObj;這樣就不會有垃圾回收的過程。
-
詳解文章補充👇
-
原文:JavaScript的垃圾回收機制,清除無用變量,釋放多余內存,展現更好的性能
-
鏈接:https://blog.csdn.net/l_ppp/article/details/106858295
🤐三、作用域、原型鏈、閉包
1、作用域
(1)什么是作用域?
ES5 中只存在兩種作用域:全局作用域和函數作用域。在 Javascript 中,我們將作用域定義為一套規則,這套規則用來管理引擎如何在當前作用域以及嵌套子作用域中根據標識符名稱進行變量(變量名和函數名)查找。
作用域,就是當訪問一個變量時,編譯器在執行這段代碼時,會首先從當前的作用域中查找是否有這個標識符,如果沒有找到,就會去父作用域查找,如果父作用域還沒有找到則繼續向上查找,直到全局作用域為止。可理解為該上下文中聲明的變量和聲明的作用范圍,可分為塊級作用域和函數作用域。
(2)什么是作用域鏈?
- 作用域鏈可以看成是將變量對象按順序連接起來的一條鏈子。
- 每個執行環境中的作用域都是不同的。
- 當我們引用變量時,會順著當前執行環境的作用域鏈,從作用域鏈的開頭開始,依次往上尋找對應的變量,直到找到作用域鏈的尾部,報錯 undefined 。
- 作用域鏈保證了變量的有序訪問。
- 注意:作用域鏈只能向上訪問,到 window 對象即被終止。
2、原型鏈
(1)什么是原型?什么是原型鏈?
- 原型和原型鏈:在 Javascript 中,每個對象都會在其內部初始化一個屬性,這個屬性就是 prototype(原型)。當我們訪問一個對象的屬性時,如果這個對象內部不存在這個屬性,那么它就會去 prototype 里找這個屬性,這個 prototype 又會有自己的 prototype ,于是就這樣一直找下去,這樣逐級查找形似一個鏈條,且通過 [[prototype]] 屬性連接,這個連接的過程被稱為原型鏈。
- 關系:instance.constructor.prototype === instance.__ proto __ ;
(2)什么是原型鏈繼承?
原型鏈繼承,是類比類的繼承,即當有兩個構造函數 A 和 B ,將一個構造函數 A 的原型對象,通過其 [[prototype]] 屬性連接另外一個構造函數B的原型對象時,這個過程被稱為原型繼承。
(3)手寫題:原型鏈之instance原理
// 判斷A是否為B的實例 const instanceOf = (A, B) =>{// 定義一個指針P指向Alet p = A;// 當P存在時則繼續執行while(p){// 判斷P值是否等于B的prototype對象,是則說明A是B的實例if(p === B.prototype){return true;}// 不斷遍歷A的原型鏈,直到找到B的原型為止p = p.__proto__;}return false; }console.log(instanceOf([], Array));3、閉包
(1)閉包是什么?
閉包,是指函數內部再嵌套函數,且在嵌套的函數內有權訪問另外一個函數作用域中的變量。
(2)js代碼的執行過程
看完閉包的定義,我們再來了解 js 代碼的整個執行過程,具體如下:
JavaScript 代碼的整個執行過程,分為兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段作用域規則會確定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段創建。
(3)一般如何產生閉包?
- 函數作為返回值被傳遞
- 函數作為參數被返回
(4)閉包產生的本質
- 當前環境中存在指向父級作用域的引用
(5)閉包的特性
- 函數內再嵌套函數
- 內部函數可以引用外層的參數和變量
- 參數和變量不會被垃圾回收機制回收
(6)閉包的優缺點
- 優點:能夠實現封裝和緩存等。
- 缺點:①消耗內存;②使用不當會內存溢出。
(7)解決方法
- 在退出函數之前,將不使用的局部變量全部刪除。
(8)let閉包
let會產生臨時性死區,在當前的執行上下文中,會進行變量提升,但是未被初始化,所以在上下文執行階段時,執行代碼如果還沒有執行到變量賦值,就引用此變量會引發報錯,因為此變量未被初始化。
(9)閉包的應用場景
- 函數柯里化
- 模塊
(10)手寫題:函數柯里化
1)柯里化是什么
柯里化指的是,有這樣一個函數,它接收函數 A ,并且能返回一個新的函數,這個新的函數能夠處理函數 A 的剩余參數。
2)代碼實現
下面給出三種具體的實現方式,代碼如下:
/*** 函數柯里化:將一個接收多個參數的函數變為接收任意參數返回一個函數的形式,便于之后繼續調用,直到最后一次調用,才返回結果值 例子:有一個add函數,用于返回所有參數的和,add(1, 2, 3, 4, 5) 返回的是15現在要將其變為類似 add(1)(2)(3)(4)(5) 或者 add(1)(2, 3, 4)(5) 的形式,并且功能相同*/// 普通的add()函數 function add(){let sum = 0;let args = [...arguments];for(let i in args){sum += args[i];}return sum; }// 第一種add()函數柯里化方式 // 缺點:最后返回的結果是函數類型,但會被隱式轉化為字符串,調用toString()方法function add1(){// 創建數組,用于存放之后接收的所有參數let args = [...arguments];function getArgs(){args.push(...arguments);return getArgs;}getArgs.toString = function(){return args.reduce((a,b) => {return a + b;})}return getArgs; }// 第二種add()函數柯里化方式 // 缺點:需要在最后再自調用一次,即不傳參調用表示已沒有參數了 function add2(){let args = [...arguments];return function(){// 長度為0時直接把所有數進行相加if(arguments.length === 0){return args.reduce((a,b) => {return a + b;})}else{// 定義一個_args為了用來遍歷let _args = [...arguments];// 長度不為0時要進行遍歷for(let i = 0; i < _args.length; i++){args.push(_args[i]);}return arguments.callee;}} }// 第三種add()函數柯里化方式 // 缺點:在剛開始傳參之前,設定總共需要傳入參數的個數 function add3(length){// slice(1)表示從第二個元素開始取值let args = [...arguments].slice(1);return function(){args = args.concat([...arguments]);if(arguments.length < length){return add3.apply(this, [length - arguments.length].concat(args));}else{// 返回想要實現的目的return args.reduce((a,b) => a + b);}} }// 測試代碼 let res = add(1,2,3,4,5); let res1 = add1(1)(2)(3)(4)(5); let res2 = add2(1)(2,3,4)(5)(); let res3 = add3(5);console.log(res); console.log(res1); console.log(res2); console.log(res3(1)(2,3)(4)(5));(11)補充
詳解文章補充👇
- 原文:剖析作用域和閉包,淺析函數柯里化
- 鏈接:https://juejin.cn/post/6970469489938792455
4、變量對象
(1)變量對象
變量對象,是執行上下文中的一部分,可以抽象為一種數據作用域,也可以理解為就是一個簡單的對象,它存儲著該執行上下文中的所有變量和函數聲明(不包含函數表達式)。
(2)活動對象
活動對象(AO):當變量對象所處的上下文為 active EC 時,成為活動對象。
(3)變量提升
函數在運行的時候,會首先創建執行上下文,然后將執行上下文入棧,當此執行上下文處于棧頂時,開始運行執行上下文。
在創建執行上下文的過程中會做三件事:創建變量對象,創建作用域鏈,確定 this 指向,其中創建變量對象的過程中,首先會為 arguments 創建一個屬性,值為 arguments ,然后會掃描 function 函數聲明,創建一個同名屬性,值為函數的引用,接著會掃碼 var 變量聲明,創建一個同名屬性,值為 undefined ,這就是變量提升 。
以下給出具體實例:
js (b) //call b console.log(a) //undefined let a = 'Hello World'; function b(){ console.log('call b'); } b(); // call b second function b() { console.log('call b fist'); } function b() { console.log('call b second'); } var b = 'Hello world';😜四、事件
1、事件模型
W3C中定義事件的發生經歷三個階段:捕獲階段(capturing)、目標階段(targetin)、冒泡階段(bubbling)。
- 冒泡型事件:當你使用事件冒泡時,子級元素先觸發,父級元素后觸發。
- 捕獲型事件:當你使用事件捕獲時,父級元素先觸發,子級元素后觸發。
- DOM 事件流:同時支持兩種事件模型:捕獲型事件和冒泡型事件。
2、事件是如何實現的?
基于發布訂閱模式,就是在瀏覽器加載的時候會讀取事件相關的代碼,但是只有實際等到具體的事件觸發的時候才會執行。
比如點擊按鈕,這是個事件( Event ),而負責處理事件的代碼段通常被稱為事件處理程序(Event Handler),也就是「啟動對話框的顯示」這個動作。
在 Web 端,我們常見的就是 DOM 事件:
- DOM0 級事件,直接在 html 元素上綁定 on-event ,比如 onclick,取消的話,dom.onclick = null,同一個事件只能有一個處理程序,后面的會覆蓋前面的。
- DOM2 級事件,通過 addEventListener 注冊事件,通過 removeEventListener 來刪除事件,一個事件可以有多個事件處理程序,按順序執行,捕獲事件和冒泡事件。
- DOM3級事件,增加了事件類型,比如 UI 事件,焦點事件,鼠標事件。
3、怎么加事件監聽?
通過 onclick 和 addEventListener 來對事件進行監聽。
4、什么是事件委托?
(1)定義
- 事件代理(Event Delegation),又稱為事件委托,是 Javascript 中常用的綁定事件技巧。
- “事件代理”即是把原本需要綁定的事件委托給父元素,讓父元素擔當事件監聽的職務。
(2)原理
- 事件代理的原理是 DOM 元素的事件冒泡。
(3)好處
- 使用事件代理的好處是可以提高性能。
- 可以大量節省內存占用,減少事件注冊,比如說在 ul 上代理所有 li 的 click 事件。
- 可以實現當新增子對象時無需再次對其進行綁定。
(4)補充
詳解文章補充(事件)👇
- 原文:你真的理解事件綁定、事件冒泡和事件委托嗎?
- 鏈接:https://juejin.cn/post/6971940848439132196
5、說說事件循環 event loop
(1)定義
首先, js 是單線程的,主要的任務是處理用戶的交互,而用戶的交互無非就是響應 DOM 的增刪改,那如何處理事件響應呢?
瀏覽器的各種 Web API 會為異步的代碼提供了一個單獨的運行空間,當異步的代碼運行完畢以后,會將代碼中的回調送入到 Task Queue(任務隊列)中去,等到調用棧空時,再將隊列中的回調函數壓入調用棧中執行,等到棧空以及任務隊列也為空時,調用棧仍然會不斷檢測任務隊列中是否有代碼需要執行,這一過程就是完整的 Event loop 了。
同時需要注意的是,js 引擎在執行過程中有優先級之分, js 引擎在一次事件循環中, 會先執行 js 線程的主任務,然后會去查找是否有微任務 microtask(promise),如果有那就優先執行微任務,如果沒有,再去查找宏任務 macrotask(setTimeout、setInterval) 進行執行。
(2)常用的宏任務和微任務
1)常用的宏任務和微任務有:
| 宏任務 | script、setTimeout 、setInterval 、setImmediate、I/O、UI Rendering |
| 微任務 | process.nextTick()、Promise |
上訴的 setTimeout 和 setInterval 等都是任務源,真正進入任務隊列的是他們分發的任務。
2)優先級
- setTimeout = setInterval 一個隊列
- setTimeout > setImmediate
- process.nextTick > Promise
(3)setTimeout(fn,0)多久才執行,Event loop?
setTimeout 按照順序放到隊列里面,然后等待函數調用棧清空之后才開始執行,而這些操作進入隊列的順序,則由設定的延遲時間來決定。
(4)補充
詳解文章補充(事件循環)👇
- 原文1:詳解隊列在前端的應用,深剖JS中的事件循環Eventloop,再了解微任務和宏任務
- 鏈接1:https://juejin.cn/post/6968750855071727630
- 原文2:瀏覽器與Node環境下的Event Loop
- 鏈接2:https://juejin.cn/post/6886992599006380045
🤪五、this問題
1、描述下this(談談對this對象的理解)
- this ,函數執行的上下文,總是指向函數的直接調用者(而非間接調用者),可以通過 apply , call , bind 改變 this 的指向。
- 如果有 new 關鍵字,this 指向 new 出來的那個對象。
- 在事件中,this 指向觸發這個事件的對象,特殊的是,IE 中的 attachEvent 中的 this 總是指向全局對象 window 。
- 對于匿名函數或者直接調用的函數來說,this 指向全局上下文(瀏覽器為 window ,NodeJS 為 global),剩下的函數調用,那就是誰調用它, this 就指向誰。
- 對于 es6 的箭頭函數,箭頭函數的指向取決于該箭頭函數聲明的位置,在哪里聲明, this 就指向哪里。
2、this綁定的四大規則
this綁定四大規則遵循以下順序:
New 綁定 > 顯示綁定 > 隱式綁定 > 默認綁定
下面一一介紹四大規則。
(1)New綁定
- New 綁定: new 調用函數會創建一個全新的對象,并將這個對象綁定到函數調用的 this 。New 綁定時,如果是 new 一個硬綁定函數,那么會用 new 新建的對象替換這個硬綁定 this 。具體實現代碼如下:
(2)顯式綁定
- 顯示綁定:通過在函數上運行 call 和 apply ,來顯示綁定的 this 。具體實現代碼如下:
- 顯示綁定之硬綁定
(3)隱式綁定
- 隱式綁定:調用位置是否有上下文對象,或者是否被某個對象擁有或者包含,那么隱式綁定規則會把函數調用中的 this 綁定到這個上下文對象。而且,對象屬性鏈只有上一層或者說最后一層在調用位置中起作用。具體實現代碼如下:
(4)默認綁定
- 默認綁定:沒有其他修飾( bind 、 apply 、 call ),在非嚴格模式下定義指向全局對象,在嚴格模式下定義指向 undefined 。具體實現代碼如下:
3、如果一個構造函數,bind了一個對象,用這個構造函數創建出的實例會繼承這個對象的屬性嗎?為什么?
不會繼承,因為根據 this 綁定四大規則,new 綁定的優先級高于 bind 顯示綁定,通過 new 進行構造函數調用時,會創建一個新對象,這個新對象會代替 bind 的對象綁定,作為此函數的 this,并且在此函數沒有返回對象的情況下,返回這個新建的對象。
4、箭頭函數和普通函數有啥區別?箭頭函數能當構造函數嗎?
(1)箭頭函數和普通函數定義
普通函數通過 function 關鍵字定義, this 無法結合詞法作用域使用,在運行時綁定,只取決于函數的調用方式,在哪里被調用,調用位置。(取決于調用者,和是否獨立運行)。
箭頭函數使用被稱為胖箭頭的操作 => 來定義,箭頭函數不應用普通函數 this 綁定的四種規則,而是根據外層(函數或全局)的作用域來決定 this ,且箭頭函數的綁定無法被修改( new 也不行)。
(2)箭頭函數和普通函數的區別
- 箭頭函數常用于回調函數中,包括事件處理器或定時器。
- 箭頭函數和 var self = this ,都試圖取代傳統的 this 運行機制,將 this 的綁定拉回到詞法作用域。
- 沒有原型、沒有 this 、沒有 super,沒有 arguments ,沒有 new.target 。
- 不能通過 new 關鍵字調用。
- 一個函數內部有兩個方法:[[Call]] 和 [[Construct]],在通過 new 進行函數調用時,會執行 [[construct]] 方法,創建一個實例對象,然后再執行這個函數體,將函數的 this 綁定在這個實例對象上。
- 當直接調用時,執行 [[Call]] 方法,直接執行函數體。
- 箭頭函數沒有 [[Construct]] 方法,不能被用作構造函數調用,當使用 new 進行函數調用時會報錯。
5、了解this嘛,apply,call,bind具體指什么?
(1)三者的區別
- apply 、call 、bind 三者都是函數的方法,都可以改變函數的 this 指向。
- apply 和 call 都是改變函數 this 指向,并在傳入參數后立即調用執行該函數。
- bind 是在改變函數 this 指向后,并傳入參數后返回一個新的函數,不會立即調用執行。
(2)傳參方式
apply 傳入的參數是數組形式的,call 傳入的參數是按順序的逐個傳入并以逗號隔開, bind 傳入的參數既可以是數組形式,也可以是按順序逐個傳入。具體方式見下方:
apply: Array.prototype.apply(this, [args1, args2]) ES6 之前用來展開數組調用, foo.apply(null, []) , ES6 之后使用 ... 操作符;call: Array.prototype.call(this, args1, args2)。bind: Array.prototype.bind(this, args1, args2); Array.prototype.bind(this, [args1, args2])。(3)手寫apply、call、bind
apply:
// 實現apply函數,在函數原型上封裝myApply函數 , 實現和原生apply函數一樣的效果Function.prototype.myApply = function(context){// 存儲要轉移的目標對象_this = context ? Object(context) : window;// 在轉移this的對象上設定一個獨一無二的屬性,并將函數賦值給它let key = Symbol('key');_this[key] = this;// 將數組里存儲的參數拆分開,作為參數調用函數let res = arguments[1] ? _this[key](...arguments[1]) : _this[key]();// 刪除delete _this[key];// 返回函數返回值return res; }// 測試代碼 let obj = {'name': '張三' }function showName(first, second, third){console.log(first, second, third);console.log(this.name); }showName.myApply(obj, [7,8,9]);call:
// 實現call函數,在函數原型上封裝myCall函數 , 實現和原生call函數一樣的效果Function.prototype.myCall = function(context){// 存儲要轉移的目標對象let _this = context ? Object(context) : window;// 在轉移this的對象上設定一個獨一無二的屬性,并將函數賦值給它let key = Symbol('key');_this[key] = this;// 創建空數組,存儲多個傳入參數let args = [];// 將所有傳入的參數添加到新數組中for(let i =1; i < arguments.length; i++){args.push(arguments[i]);}// 將新數組拆開作為多個參數傳入,并調用函數let res = _this[key](...args);// 刪除delete _this[key];// 返回函數返回值return res; }let obj = {'name': '張三' }function showName(first, second, third){console.log(first, second, third);console.log(this.name); }showName.myCall(obj, 7, 8, 9);bind:
// 實現Bind函數,在函數原型上封裝myBind函數 , 實現和原生bind函數一樣的效果Function.prototype.myBind = function(context){// 存儲要轉移的目標對象let _this = context ? Object(context) : window;// 在轉移this的對象上設定一個獨一無二的屬性,并將函數賦值給它let key = Symbol('key');_this[key] = this;// 創建函數閉包return function(){// 將所有參數先拆分開,再添加到新數組中,以此來支持多參數傳入以及數組參數傳入的需求let args = [].concat(...arguments);// 調用函數let res = _this[key](...args);// 刪除delete _this[key];// 返回函數返回值return res;} }// 測試代碼 let obj = {'name' : '張三' }function showName(first, second, third){console.log(first, second, third);console.log(this.name); }showName.myBind(obj)([7,8,9]);😋六、Ajax問題
1、Ajax原理
- Ajax 的原理簡單來說是在用戶和服務器之間加了一個中間層(AJAX引擎),通過 XMLHTTPRequest 對象來向服務器發起異步請求,從服務器獲得數據,然后用 javascript 來操作 DOM 而更新頁面。
- 使用戶操作與服務器響應異步化。這其中最關鍵的一步就是從服務器獲得請求數據。
- Ajax 的過程只涉及 Javascript 、XMLHttpRequest 和 DOM ,其中 XMLHttpRequest 是 ajax 的核心機制。
2、Ajax解決瀏覽器緩存問題
- 在 ajax 發送請求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since","0") 。
- 在 ajax 發送請求前加上 anyAjaxObj.setRequestHeader("Cache-Control","no-cache") 。
- 在 URL 后面加上一個隨機數: "fresh=" + Math.random() 。
- 在 URL 后面加上時間搓:"nowtime=" + new Date().getTime() 。
3、js單線程
-
單線程:只有一個線程,只能做一件事情。
-
原因:避免 DOM 渲染的沖突。
- 瀏覽器需要渲染 DOM ;
- JS 可以修改 DOM 結構;
- JS 執行的時候,瀏覽器 DOM 渲染會暫停;
- 兩段 JS 也不能同時執行(都修改 DOM 就沖突了);
- Webworker 支持多線程,但是不能訪問 DOM。
-
解決方案:異步。
4、異步編程的實現方式
(1)回調函數
- 優點:簡單、容易理解
- 缺點:不利于維護,代碼耦合高
(2)事件監聽(采用時間驅動模式,取決于某個事件是否發生)
- 優點:容易理解,可以綁定多個事件,每個事件可以指定多個回調函數
- 缺點:事件驅動型,流程不夠清晰
(3)發布/訂閱(觀察者模式)
- 類似于事件監聽,但是可以通過“消息中心”,了解現在有多少發布者,多少訂閱者
(4)Promise 對象
- 優點:可以利用 then 方法,進行鏈式寫法;可以書寫錯誤時的回調函數;
- 缺點:編寫和理解,相對比較難
(5)Generator 函數
- 優點:函數體內外的數據交換、錯誤處理機制
- 缺點:流程管理不方便
(6)async 函數
- 優點:內置執行器、更好的語義、更廣的適用性、返回的是 Promise 、結構清晰。
- 缺點:錯誤處理機制
5、js腳本加載問題,async、defer問題
- 如果依賴其他腳本和 DOM 結果,使用 defer。
- 如果與 DOM 和其他腳本依賴不強時,使用 async。
- 總結:依賴性強用 defer ,依賴性不強用 async 。
6、關于window.onload 和 DOMContentLoaded
window.addEventListener('load', function(){//頁面的全部資源加載完才會執行,包括圖片、視頻等 });document.addEventListener('DOMContentLoaded', function(){//DOM 渲染完即可執行,此時圖片、視頻還可能沒加載完 -> 盡量選擇此方法 });-
關于 DOM 和 BOM 操作的補充👇
-
原文:提升對前端的認知,不得不了解Web API的DOM和BOM
-
鏈接:https://juejin.cn/post/6971567246174846989
7、ajax、axios、fetch區別
(1)ajax
- 本身是針對 MVC 的編程,不符合現在前端 MVVM 的浪潮。
- 基于原生的 XHR 開發, XHR 本身的架構不清晰,已經有了 fetch 的替代方案。
- JQuery 整個項目太大,單純使用 ajax 卻要引入整個 JQuery 非常的不合理(采取個性化打包的方案又不能享受 CDN 服務)。
(2)axios
- 從瀏覽器中創建 XMLHttpRequest。
- 從 node.js 發出 http 請求。
- 支持 Promise API 。
- 攔截請求和響應。
- 轉換請求和響應數據。
- 取消請求。
- 自動轉換 JSON 數據。
- 客戶端支持防止 CSRF/XSRF。
(3)fetch
- fetch 返回的 promise 不會被標記為 reject ,即使該 http 響應的狀態碼是 404 或 500 。僅當網絡故障或請求被阻止時,才會標記為 reject 。
- 只對網絡請求報錯,對 400 , 500 都當做成功的請求,需要封裝去處理。
- 這里對于 cookie 的處理比較特殊,不同瀏覽器對 credentials 的默認值不一樣,也就使得默認情況下 cookie 變的不可控。
- 本身無自帶 abort ,無法超時控制,可以使用 AbortController 解決取消請求問題。
- 沒有辦法原生監測請求的進度,而 XHR 可以。
8、手寫題:手寫Ajax函數
/*1. get()方法參數:url(請求的地址)、data(攜帶數據)、callback(成功回調函數)、dataType(返回數據類型)2. post()方法參數:url(請求的地址)、data(攜帶數據)、callback(成功回調函數)、dataType(返回數據類型)3. ajax()方法參數:obj(對象中包含了各種參數),其中有url、data、dataType、async、type */let $ = {createXHR: function() {if(window.XMLHttpRequest) {return new XMLHttpRequest()} else {return new ActiveXObject()} },get: function(url, data, callback, dataType) {let dataType = dataType.toLowerCase()if(data) {url += '?'Object.keys(data).forEach(key => url += `${key}=${data[key]}&`)url = url.slice(0, -1)}let xhr = this.createXHR()xhr.open('get', url)xhr.send()xhr.onreadystatechange = function() {if(xhr.readyState === 4) {if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {let res = dataType === 'json' ? JSON.parse(xhr.responseText) : xhr.responseTextcallback(res, xhr.status, xhr)}}}},post: function(url, data, callback, dataType) {let dataType = dataType.toLowerCase()let xhr = this.createXHR()let str = ''if(data) {Object.keys(data).forEach(key => str += `${key}=${data[key]}&`)str = str.slice(0, -1)}xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')xhr.send(str)xhr.onreadystatechange = function() {if(xhr.readyState === 4) {if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {let res = dataType === 'json' ? JSON.parse(xhr.responseText) : xhr.responseTextcallback(res, xhr.status, xhr)}}}},ajax: function(params) {// 初始化參數let type = params.type ? params.type.toLowerCase() : 'get'let isAsync = params.isAsync ? params.isAsync : 'true'let url = params.urllet data = params.data ? params.data : {}let dataType = params.dataType.toLowerCase()let xhr = this.createXHR()let str = ''// 拼接字符串Object.keys(data).forEach(key => str += `${key}=${data[key]}&`)str = str.slice(0, -1)if(type === 'get') url += `?${str}`;return new Promise((resolve, reject) => {// 創建請求xhr.open(type, url, isAsync)if(type === 'post') {xhr.setRequestHeader('Content-Type', 'application/x-www-form-rulencoded')xhr.send(str)} else {xhr.send()}xhr.onreadystatechange = function() {if(xhr.readyState === 4) {if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {let res = dataType === 'json' ? JSON.parse(xhr.responseText) : xhr.responseTextresolve(res) // 請求成功,返回數據} else {reject(xhr.status) // 請求失敗,返回狀態碼}}}}) } }9、手寫題:手寫Promise原理
class MyPromise{constructor(fn){this.resolvedCallbacks = [];this.rejectCallbacks = [];// pending 即在等待狀態this.state = 'PENDING';this.value = '';fn(this.resolve.bind(this), this.reject.bind(this));}resolve(value){if(this.state === 'PENDING'){this.state = 'RESOLVED';this.value = value;this.resolvedCallbacks.map(cb => cb(value));}}reject(value){if(this.state === 'PENDING'){this.state = 'REJECTED';this.value = value;this.rejectCallbacks.map(cb => cb(value));}}then(onFulfilled, onRejected){if(this.state === 'PENDING'){this.resolvedCallbacks.map(cb => cb(onFulfilled));this.rejectCallbacks.map(cb => cb(onRejected));}if(this.state === 'RESOLVED'){onFulfilled(this.value);}if(this.state === 'REJECTED'){onRejected(this.value);}} }10、手寫題:基于Promise手寫Promise.all
/*** promise的三種狀態:* 1.pending:等待狀態,比如正在網絡請求,或定時器沒有到時間;* 2.fulfill:滿足狀態,當我們主動回調了resolve時,就處于該狀態,并且會回調then函數* 3.reject:拒絕狀態,當我們主動回調了reject時,就處于該狀態,并且會回調catch函數*/ /*----------------------------------------------------------------- */ // 函數.then() // 函數then是Promise中的一個方法,它會在Promise處于fulfill狀態時調用觸發 // resolve和reject是默認傳入的函數參數 new Promise((resolve, reject) => {setTimeout(() => {// 在Promise中調用resolve函數,會使Promise變為fulfill狀態// resolve函數可以傳入一個參數,作為then函數的默認傳入參數resolve('成功');}, 1000); }) .then(data => {console.log(data); //結果輸出成功 });/*----------------------------------------------------------------- */ // 函數 .catch() // 函數catch是Promise的一個方法。它會在Promise處于reject狀態時調用觸發 new Promise((resolve, reject) => {setTimeout(() => {// 在Promise調用reject函數,會使Promise變為reject狀態// reject函數可以傳入一個參數,作為catch函數的默認傳入參數reject('失敗');}, 1000) }) .catch(err => {console.log(err); //結果輸出:失敗 })/*----------------------------------------------------------------- */ // 函數.finally() // 函數finally是Promise中的一個方法,它會在Promise的最后觸發,無論Promise處于什么狀態 new Promise((resolve, reject) => {setTimeout(() => {resolve('成功啦!')},1000) }) .then(data => {console.log(data); }) .finally(() => {console.log('Promise結束'); })/* 結果輸出:成功啦!Promise結束 *//*----------------------------------------------------------------- */ // 函數all() // 函數all是Promise中的一個方法,它用于將多個promise實例,包裝成一個新的promise實例 Promise.all([new Promise((resolve, reject) => {setTimeout(() => {resolve('我是第一個異步請求的數據');});}, 1000),new Promise((resolve, reject) => {setTimeout(() => {resolve('我是第二個異步請求的數據');}, 1000);}) ]) .then(results => {console.log(results); // ['我是第一個異步請求的數據', '我是第二個異步請求的數據'] })/*----------------------------------------------------------------- */ // 實際應用 let string1 = 'I am'; new Promise((resolve, reject) => {setTimeout(() => {let string2 = string1 + 'Monday';resolve(string2);}, 1000); }) .then(data => {return new Promise((resolve, reject) => {let string3 = data + 'in CSDN';resolve(string3);}) }) .then(data => {console.log(data); }) .finally(() => {console.log('Promise結束'); })/*輸出結果:I am Monday in CSDN!Promise結束 */詳解文章補充(Promise)👇
- 原文:保姆級一步步帶你實現promise的核心功能
- 鏈接:https://blog.csdn.net/weixin_44803753/article/details/119392804
🥰七、手寫題補充
1、性能優化相關
(1)手寫節流函數
節流:每隔一段時間執行一次,通常用在高頻率觸發的地方,降低頻率。—— 如:鼠標滑動、拖拽
通俗來講,節流是從頻繁觸發執行變為每隔一段時間才執行一次。
//封裝節流函數,實現節流 function throttle(func, delay=500) {let timer = null;let status = false;return function (...args) {if(status) return;status = true;timer = setTimeout(() => {func.apply(this, args)status = false}, delay);} }(2)手寫防抖函數
防抖:一段時間內連續觸發,不執行,直到超出限定時間執行最后一次。—— 如: input 、 scroll 滾動
通俗來講,防抖是從頻繁觸發執行變為最后一次才執行。
//封裝防抖函數,實現防抖 function denounce(func, delay=500){let timer = null;return function(...args){// 如果有值,清除定時器,之后繼續執行if(timer){clearTimeout(timer);}timer = setTimeout(() => {func.apply(this, args);},delay);} }-
詳解文章補充(防抖節流)👇
-
原文:關于前端性能優化問題,認識網頁加載過程和防抖節流
-
鏈接:https://juejin.cn/post/6973062729925918756
(3)圖片懶加載
定義:
懶加載突出一個“懶”字,懶就是拖延遲的意思,所以“懶加載”說白了就是延遲加載,比如我們加載一個頁面,這個頁面很長很長,長到我們的瀏覽器可視區域裝不下,那么懶加載就是優先加載可視區域的內容,其他部分等進入了可視區域在加載。
代碼實現:
let img = document.getElementsByTagName('img'); // 獲取img標簽相關的 let num = img.length; // 記錄有多少張圖片 let count = 0; // 計數器,從第一張圖片開始計數lazyload(); // 首次加載別忘了加載圖片function lazyload() {let viewHeight = document.documentElement.clientHeight; // clientHeight 獲取屏幕可視區域的高度let scrollHeight = document.documentElement.scrollTop || document.body.scrollTop; // 滾動條卷去的高度for (let i = 0; i < num; i++) {// 元素現在已經出現在視覺區域中if (img[i].offsetTop < scrollHeight + viewHeight) {// 當src不存在時,跳出本輪循環,繼續下一輪if (img[i].getAttribute('src') !== 'default.jpg') {continue;} else {// 當src屬性存在時,獲取src的值,并將其賦值給imgimg[i].src = img[i].getAttribute('data-src');count++;}}} }- 詳細文章補充👇
- 原文:原生js實現圖片懶加載(lazyLoad)
- 鏈接:https://zhuanlan.zhihu.com/p/55311726
2、原生API手寫
(1)forEach
用法:
forEach() 方法對數組的每個元素執行一次給定的函數。原生 API 具體解析如下:
arr.forEach(function(currentValue, currentIndex, arr) {}, thisArg)//currentValue 必需。當前元素 //currentIndex 可選。當前元素的索引 //arr 可選。當前元素所屬的數組對象。 //thisArg 可選參數。當執行回調函數時,用作 this 的值。代碼實現:
Array.prototype.myForEach = function (fn, thisArg) {if (typeof fn !== 'function') {throw new Error('參數必須為函數');}if (!Array.isArray(this)) {throw new Error('只能對數組使用forEach方法');}let arr = this;for (let i = 0; i < arr.length; i++) {fn.call(thisArg, arr[i], i, arr);} }// 測試 let arr = [1, 2, 3, 4, 5]; arr.myForEach((item, index) => {console.log(item, index); });// 測試 thisArg function Counter() {this.sum = 0;this.count = 0; }// 因為 thisArg 參數(this)傳給了forEach(),每次調用時,它都被傳給 callback 函數,作為它的 this 值 Counter.prototype.add = function (array) {array.myForEach(function (entry) {this.sum += entry;++this.count;}, this); }const obj = new Counter(); obj.add([2, 5, 9]);console.log(obj.count); // 3 === (1 + 1 + 1) console.log(obj.sum); // 16 === (2 + 5 + 9)(2)map
用法:
map 函數會依次處理數組中的每一個元素,并返回一個新的數組,且對原來的數組不會產生影響。
array.map(function(currentValue,index,arr){})代碼實現:
Array.prototype.myMap = function (arr, mapCallback) {// 檢查參數是否正確if (!Array.isArray(arr) || !Array.length || typeof mapCallback !== 'function') {return [];} else {let result = [];for (let i = 0; len = arr.length; i++) {result.push(mapCallback(arr[i], i, arr));}return result;} }// 測試 let arr = [1, 2, 3, 4, 5]; arr.map((item, index) => {console.log(item * 2); }); // 2, 4, 6, 8, 10(3)filter
用法:
filter() 方法返回執行結果為true的項組成的數組。
arr.filter(function(item, index, arr){}, context)代碼實現:
Array.prototype.myFilter = function (fn, context) {if (typeof fn !== 'function') {throw new Error(`${fn} is not a function`);}let arr = this;let temp = [];for (let i = 0; i < arr.length; i++) {let result = fn.call(context, arr[i], i, arr);// 判斷條件是否為真if (result) {temp.push(arr[i]);}}return temp; }// 測試 let arr = [1, 2, 3, 4, 5, 'A', 'B', 'C']; console.log(arr.myFilter((item) => typeof item === 'string')); // [ 'A', 'B', 'C' ](4)reduce
用法:
- 參數: 一個回調函數,一個初始化參數 (非必須)
- 回調函數參數有 4 個值( res : 代表累加值, cur : 目前值, index : 第幾個, arr :調用 reduce 的數組)
- 整體返回 res 累加值
代碼實現:
/*** * @param {fn} callback res→代表累加值,cur→目前值,index→第幾個,arr→調用reduce的數組* @param {*} initialValue 初始化參數(可選)*/Array.prototype.myReduce = function (cb, initValue) {if (!Array.isArray(this)) {throw new TypeError("not a array");}// 數組為空,并且有初始值,報錯if (this.length === 0 && arguments.length < 2) {throw new TypeError('Reduce of empty array with no initial value');}let arr = this;let res = null;// 判斷有沒有初始值if (arguments.length > 1) {res = initValue;} else {res = arr.splice(0, 1)[0]; //沒有就取第一個值}arr.forEach((item, index) => {res = cb(res, item, index, arr); // cb 每次執行完都會返回一個新的 res值,覆蓋之前的 res})return res; };// 測試結果 let arr = [1, 2, 3, 4]; let result = arr.myReduce((res, cur) => {return res + cur; }) console.log(result); // 103、其余手寫題
(1)JSONP的實現
JSONP 的原理:JSONP 的出現使得 script 標簽不受同源策略約束,用來進行跨域請求,優點是兼容性好,缺點就是只能用于 GET 請求。
const jsonp = ({ url, params, callbackName }) => {const generateUrl = () => {let dataSrc = '';for (let key in params) {if (params.hasOwnProperty(key)) {dataSrc += `${key}=${params[key]}&`;}}dataSrc += `callback=${callbackName}`;return `${url}?${dataSrc}`;}return new Promise((resolve, reject) => {const scriptElement = document.createElement('script')scriptElement.src = generateUrl()document.body.appendChild(scriptElement)window[callbackName] = data => {resolve(data)document.removeChild(scriptElement)}}) }(2)Object.create
用法:
Object.creat(object[,propertiesObject]) ,用于創建一個新對象,且這個新對象繼承 object 的屬性。第二個參數 propertyObject 也是對象,是一個可選參數,它旨在為新創建的對象指定屬性對象。該屬性對象可能包含以下值:
| configurable | 表示新創建的對象是否是可配置的,即對象的屬性是否可以被刪除或修改,默認false |
| enumerable | 對象屬性是否可枚舉的,即是否可以枚舉,默認false |
| writable | 對象是否可寫,是否或以為對象添加新屬性,默認false |
| get | 對象getter函數,默認undefined |
| set | 對象setter函數,默認undefined |
代碼實現:
/*** * @param {*} proto 新創建對象的原型對象* @param {*} propertyObject 要定義其可枚舉屬性或修改的屬性描述符的對象* @returns */ Object.create2 = function (proto, propertyObject = undefined) {if (typeof proto !== 'object' && typeof proto !== 'function') {throw new TypeError('Object prototype may only be an Object or null.')}// 創建一個空的構造函數 Ffunction F() { }// F 原型指向 protoF.prototype = proto// 創建 F 的實例const obj = new F()// propertiesObject有值則調用 Object.definePropertiesif (propertyObject != undefined) {Object.defineProperties(obj, propertyObject)}if (proto === null) {// 創建一個沒有原型對象的對象,Object.create(null)obj.__proto__ = null}// 返回 這個 objreturn obj }const person = {name: 'monday',printIntroduction: function() {console.log(`My name is ${this.name}, and my age is ${this.age}`);} };const me = Object.create2(person);me.name = 'Tuesday'; me.age = 18; me.printIntroduction();(3)Object.assign
用法:
Object.assign() 方法用于將所有可枚舉屬性的值從一個或多個源對象分配到目標對象。它將返回目標對象。
代碼實現:
Object.assign2 = function (target, ...source) {if (target == null) {throw new TypeError('Cannot convert undefined or null to object');}let res = Object(target);source.forEach(function (obj) {if (obj != null) {for (let key in obj) {if (obj.hasOwnProperty(key)) {res[key] = obj[key];}}}})return res; }const target = { a: 1, b: 2 }; const source = { b: 4, c: 5 };const returnedTarget = Object.assign2(target, source);console.log(target); // { a: 1, b: 4, c: 5 }console.log(returnedTarget); // { a: 1, b: 4, c: 5 }(4)手寫發布訂閱
代碼實現:
class Subject {constructor(name) {this.name = name; // 被觀察者的名字this.message = '今天是晴天'; // 存放一個值this.observers = []; // 存放所有觀察者}on(observer) {this.observers.push(observer);}triggle(data) {this.message = data;this.observers.forEach(item => item.update(data));} }class Observer {constructor(name) {this.name = name;}update(newDate) {console.log(`我是觀察者${this.name}: ${newDate}`);} }// 測試代碼 let subject = new Subject('message');let o1 = new Observer('小紅'); let o2 = new Observer('小明');subject.on(o1); // 我是觀察者小紅: 明天會下雨 subject.on(o2); // 我是觀察者小明: 明天會下雨subject.triggle('明天會下雨');😉八、結束語
以上收錄周一整個秋招備試過程中 JavaScript 的所有面試題,上面的面試題可能還不夠全,如有想要補充的內容也歡迎聯系 vx:MondayLaboratory ,希望能夠讓文章內容更加盡善盡美,造福更多備試的小伙伴~
最后,預祝各位看到這篇文章的小伙伴們,都能夠斬獲到自己心儀的 offer ~
🐣彩蛋 One More Thing
(:pdf內容獲取
👉 微信關注公眾號 星期一研究室 ,回復關鍵字 js面試pdf 即可獲取相關 pdf 內容~
👉 回復 面試大全pdf 可獲取全專欄內容!
(:更新地址
👉 offer來了面試專欄
(:番外篇
- 如果您覺得這篇文章有幫助到您的的話不妨點贊支持一下喲~~😉
- 以上就是本文的全部內容!我們下期見!👋👋👋
總結
以上是生活随笔為你收集整理的「offer来了」保姆级巩固你的js知识体系(4.0w字)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 没有域名怎么设置企业邮箱(没有域名怎么设
- 下一篇: 「offer来了」2种递进学习思维,24