前言
以前我看到面試貼就直接刷掉的,從不會多看一眼,直到去年 9 月份我開始準備面試時,才發現很多面試經驗貼特別有用,看這些帖子(我不敢稱之為文章,怕被杠)的過程中對我的復習思維形成影響很大,所以我現在把之前自己好好整理的面試計劃分享出來,希望能幫到接下來要找工作的朋友,不喜勿噴哈~
一、簡歷
簡歷在找工作過程中是非常非常重要的,無論你是什么途徑去面試的,面試你的人一定會看你的簡歷。
1、重點
簡歷就像高考作文——閱卷時間非常短。
內容要簡潔。
直擊重點,表現出自己的優勢(只要是符合招人單位要求的都是優勢,不是別人不會的你會才叫優勢)。
2、簡歷包含的內容
個人信息。
專業技能。
工作經歷。
項目經歷。
社區貢獻。
2.1 基本信息
必備:姓名 電話 郵箱。
年齡(最好寫上,在這個行業年齡還是比較重要的),學歷(寫好是哪一屆)。
頭像無所謂(好看就放上唄)。
可以放 github 鏈接,前提是有內容。
2.2 專業技能
表現出自己的核心競爭力(只要是符合招人單位要求的都是優勢)。
內容不要太多,3、5 條即可。
太基礎的不要寫,例如會用 vscode、lodash。
2.3 工作經歷
如實寫。
寫明公司,職位,入職離職時間即可,多寫無益。
如果有空窗期,如實寫明即可。
2.4 項目經歷
寫 2-4 個具有說服力的項目(不要什么項目都寫,沒用)。
項目名稱,項目描述,技術棧,個人角色。
2.5 社區貢獻
有博客或者開源作品,會讓你更有競爭力。
切記:需要真的有內容,不可臨時抱佛腳。
3、注意事項
界面不能太花哨,簡潔明了即可。
注意用詞,“精通”“熟練”等慎用,可用“熟悉”。
不可造假,會被拉入黑名單。
4、面試前準備
看 JD,是否需要臨時準備一下。
打印紙質簡歷,帶著紙和筆(增加好印象)。
最好帶著自己電腦,現場可能手寫代碼(帶一個帆布包最適合,又優雅又方便)。
要有時間觀念,如果遲到或者推遲,要提前說。
衣著適當,不用正裝,也不要太隨意。
為何離職?—— 不要吐槽前東家,說自己的原因(想找一個更好的發展平臺等)。
能加班嗎?—— 能!除非你特別自信,能找到其他機會。
不要挑戰面試官,即便他錯了(面試一定要保證愉快)。
遇到不會的問題,要表現出自己積極的一面(不好意思哈,確實是我的知識盲區,可以跟我說下 xxx 嗎,我回去研究一下)。
二、HTML+CSS 面試題
HTML 和 CSS 面試題答不出來基本可以回去了。
1、HTML 面試題
以下是針對 HTML 相關的面試題,一般來說這地方不會出太多題,面試官也不愿意花太多時間在這上面。
1.1 如何理解 HTML 語義化?
讓人更容易讀懂(增加代碼可讀性)。
讓搜索引擎更容易讀懂,有助于爬蟲抓取更多的有效信息,爬蟲依賴于標簽來確定上下文和各個關鍵字的權重(SEO)。
在沒有 CSS 樣式下,頁面也能呈現出很好地內容結構、代碼結構。
1.2 script 標簽中 defer 和 async 的區別?
script :會阻礙 HTML 解析,只有下載好并執行完腳本才會繼續解析 HTML。
async script :解析 HTML 過程中進行腳本的異步下載,下載成功立馬執行,有可能會阻斷 HTML 的解析。
defer script:完全不會阻礙 HTML 的解析,解析完成之后再按照順序執行腳本。
下圖清晰地展示了三種 script 的過程:
1.3 從瀏覽器地址欄輸入 url 到請求返回發生了什么
輸入 URL 后解析出協議、主機、端口、路徑等信息,并構造一個 HTTP 請求。
強緩存。
協商緩存。
DNS 域名解析。
TCP 連接。
總是要問:為什么需要三次握手,兩次不行嗎?其實這是由 TCP 的自身特點可靠傳輸決定的。客戶端和服務端要進行可靠傳輸,那么就需要確認雙方的接收和發送能力。第一次握手可以確認客服端的發送能力,第二次握手,確認了服務端的發送能力和接收能力,所以第三次握手才可以確認客戶端的接收能力。不然容易出現丟包的現象。
http 請求。
服務器處理請求并返回 HTTP 報文。
瀏覽器渲染頁面。
斷開 TCP 連接。
2、CSS 面試題
以下是針對 CSS 相關的面試題,這些題答不出來會給人非常不好的技術印象。
2.1 盒模型介紹
CSS3 中的盒模型有以下兩種:標準盒模型、IE(替代)盒模型。
兩種盒子模型都是由 content + padding + border + margin 構成,其大小都是由 content + padding + border 決定的,但是盒子內容寬/高度(即 width/height)的計算范圍根據盒模型的不同會有所不同:
標準盒模型:只包含 content 。
IE(替代)盒模型:content + padding + border 。
可以通過 box-sizing 來改變元素的盒模型:
box-sizing: content-box :標準盒模型(默認值)。
box-sizing: border-box :IE(替代)盒模型。
2.2 css 選擇器和優先級
首先我們要知道有哪些選擇器:
常規來說,大家都知道樣式的優先級一般為 !important > style > id > class ,但是涉及多類選擇器作用于同一個元素時候怎么判斷優先級呢?相信我,你在改一些第三方庫(比如 antd ??)樣式時,理解這個會幫助很大!
上述文章中核心內容: 優先級是由 A 、B、C、D 的值來決定的,其中它們的值計算規則如下:
如果存在內聯樣式,那么 A = 1,否則 A = 0 ;
B 的值等于 ID選擇器(#id) 出現的次數;
C 的值等于 類選擇器(.class) 和 屬性選擇器(a[]) 和 偽類(:first-child) 出現的總次數;
D 的值等于 標簽選擇器(h1,a,div) 和 偽元素(::before,::after) 出現的總次數。
從左至右比較,如果是樣式優先級相等,取后面出現的樣式。
2.3 重排(reflow)和重繪(repaint)的理解
簡單地總結下兩者的概念:
重排:無論通過什么方式影響了元素的幾何信息(元素在視口內的位置和尺寸大小),瀏覽器需要重新計算元素在視口內的幾何屬性,這個過程叫做重排。
重繪:通過構造渲染樹和重排(回流)階段,我們知道了哪些節點是可見的,以及可見節點的樣式和具體的幾何信息(元素在視口內的位置和尺寸大小),接下來就可以將渲染樹的每個節點都轉換為屏幕上的實際像素,這個階段就叫做重繪。
如何減少重排和重繪?
最小化重繪和重排,比如樣式集中改變,使用添加新樣式類名 .class 或 cssText 。
批量操作 DOM,比如讀取某元素 offsetWidth 屬性存到一個臨時變量,再去使用,而不是頻繁使用這個計算屬性;又比如利用 document.createDocumentFragment() 來添加要被添加的節點,處理完之后再插入到實際 DOM 中。
使用?absolute?或?fixed?使元素脫離文檔流,這在制作復雜的動畫時對性能的影響比較明顯。
開啟 GPU 加速,利用 css 屬性 transform 、will-change 等,比如改變元素位置,我們使用 translate 會比使用絕對定位改變其 left 、top 等來的高效,因為它不會觸發重排或重繪,transform 使瀏覽器為元素創建?個 GPU 圖層,這使得動畫元素在一個獨立的層中進行渲染。當元素的內容沒有發生改變,就沒有必要進行重繪。
2.4 對 BFC 的理解
BFC 即塊級格式上下文,根據盒模型可知,每個元素都被定義為一個矩形盒子,然而盒子的布局會受到尺寸,定位,盒子的子元素或兄弟元素,視口的尺寸等因素決定,所以這里有一個瀏覽器計算的過程,計算的規則就是由一個叫做視覺格式化模型的東西所定義的,BFC 就是來自這個概念,它是 CSS 視覺渲染的一部分,用于決定塊級盒的布局及浮動相互影響范圍的一個區域。
BFC 具有一些特性:
塊級元素會在垂直方向一個接一個的排列,和文檔流的排列方式一致。
在 BFC 中上下相鄰的兩個容器的 margin 會重疊,創建新的 BFC 可以避免外邊距重疊。
計算 BFC 的高度時,需要計算浮動元素的高度。
BFC 區域不會與浮動的容器發生重疊。
BFC 是獨立的容器,容器內部元素不會影響外部元素。
每個元素的左 margin 值和容器的左 border 相接觸。
利用這些特性,我們可以解決以下問題:
利用 4 和 6 ,我們可以實現三欄(或兩欄)自適應布局。
利用 2 ,我們可以避免 margin 重疊問題。
利用 3 ,我們可以避免高度塌陷。
創建 BFC 的方式:
絕對定位元素(position 為 absolute 或 fixed )。
行內塊元素,即 display 為 inline-block 。
overflow 的值不為 visible 。
2.5 實現兩欄布局(左側固定 + 右側自適應布局)
現在有以下 DOM 結構:
<div class="outer">
<div class="left">左側</div>
<div class="right">右側</div>
</div>
復制代碼
利用浮動,左邊元素寬度固定 ,設置向左浮動。將右邊元素的 margin-left 設為固定寬度 。注意,因為右邊元素的 width 默認為 auto ,所以會自動撐滿父元素。
.outer {
height: 100px;
}
.left {
float: left;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
margin-left: 200px;
height: 100%;
background: lightseagreen;
}
復制代碼
同樣利用浮動,左邊元素寬度固定 ,設置向左浮動。右側元素設置 overflow: hidden; 這樣右邊就觸發了 BFC ,BFC 的區域不會與浮動元素發生重疊,所以兩側就不會發生重疊。
.outer {
height: 100px;
}
.left {
float: left;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
overflow: auto;
height: 100%;
background: lightseagreen;
}
復制代碼
利用 flex 布局,左邊元素固定寬度,右邊的元素設置 flex: 1 。
.outer {
display: flex;
height: 100px;
}
.left {
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
flex: 1;
height: 100%;
background: lightseagreen;
}
復制代碼
利用絕對定位,父級元素設為相對定位。左邊元素 absolute 定位,寬度固定。右邊元素的 margin-left 的值設為左邊元素的寬度值。
.outer {
position: relative;
height: 100px;
}
.left {
position: absolute;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
margin-left: 200px;
height: 100%;
background: lightseagreen;
}
復制代碼
利用絕對定位,父級元素設為相對定位。左邊元素寬度固定,右邊元素 absolute 定位, left 為寬度大小,其余方向定位為 0 。
.outer {
position: relative;
height: 100px;
}
.left {
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
position: absolute;
left: 200px;
top: 0;
right: 0;
bottom: 0;
height: 100%;
background: lightseagreen;
}
復制代碼
2.6 實現圣杯布局和雙飛翼布局(經典三分欄布局)
圣杯布局和雙飛翼布局的目的:
三欄布局,中間一欄最先加載和渲染(內容最重要,這就是為什么還需要了解這種布局的原因)。
兩側內容固定,中間內容隨著寬度自適應。
一般用于 PC 網頁。
圣杯布局和雙飛翼布局的技術總結:
使用 float 布局。
兩側使用 margin 負值,以便和中間內容橫向重疊。
防止中間內容被兩側覆蓋,圣杯布局用 padding ,雙飛翼布局用 margin 。
圣杯布局: HTML 結構:
<div id="container" class="clearfix">
<p class="center">我是中間</p>
<p class="left">我是左邊</p>
<p class="right">我是右邊</p>
</div>
復制代碼
CSS 樣式:
container {
padding-left: 200px;
padding-right: 150px;
overflow: auto;
}
container p {
float: left;
}
.center {
width: 100%;
background-color: lightcoral;
}
.left {
width: 200px;
position: relative;
left: -200px;
margin-left: -100%;
background-color: lightcyan;
}
.right {
width: 150px;
margin-right: -150px;
background-color: lightgreen;
}
.clearfix:after {
content: "";
display: table;
clear: both;
}
復制代碼
雙飛翼布局: HTML 結構:
<div id="main" class="float">
<div id="main-wrap">main</div>
</div>
<div id="left" class="float">left</div>
<div id="right" class="float">right</div>
復制代碼
CSS 樣式:
.float {
float: left;
}
main {
width: 100%;
height: 200px;
background-color: lightpink;
}
main-wrap {
margin: 0 190px 0 190px;
}
left {
width: 190px;
height: 200px;
background-color: lightsalmon;
margin-left: -100%;
}
right {
width: 190px;
height: 200px;
background-color: lightskyblue;
margin-left: -190px;
}
復制代碼
tips:上述代碼中 margin-left: -100% 相對的是父元素的 content 寬度,即不包含 paddig 、 border 的寬度。
其實以上問題需要掌握 margin 負值問題 即可很好理解。
2.7 水平垂直居中多種實現方式
利用絕對定位,設置 left: 50% 和 top: 50% 現將子元素左上角移到父元素中心位置,然后再通過 translate 來調整子元素的中心點到父元素的中心。該方法可以不定寬高。
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
復制代碼
利用絕對定位,子元素所有方向都為 0 ,將 margin 設置為 auto ,由于寬高固定,對應方向實現平分,該方法必須盒子有寬高。
.father {
position: relative;
}
.son {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
margin: auto;
height: 100px;
width: 100px;
}
復制代碼
利用絕對定位,設置 left: 50% 和 top: 50% 現將子元素左上角移到父元素中心位置,然后再通過 margin-left 和 margin-top 以子元素自己的一半寬高進行負值賦值。該方法必須定寬高。
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
width: 200px;
height: 200px;
margin-left: -100px;
margin-top: -100px;
}
復制代碼
利用 flex ,最經典最方便的一種了,不用解釋,定不定寬高無所謂的。
.father {
display: flex;
justify-content: center;
align-items: center;
}
復制代碼
其實還有很多方法,比如 display: grid 或 display: table-cell 來做。
2.8 flex 布局
這里有個小問題,很多時候我們會用到 flex: 1 ,它具體包含了以下的意思:
flex-grow: 1 :該屬性默認為 0 ,如果存在剩余空間,元素也不放大。設置為 1 代表會放大。
flex-shrink: 1 :該屬性默認為 1 ,如果空間不足,元素縮小。
flex-basis: 0% :該屬性定義在分配多余空間之前,元素占據的主軸空間。瀏覽器就是根據這個屬性來計算是否有多余空間的。默認值為 auto ,即項目本身大小。設置為 0% 之后,因為有 flex-grow 和 flex-shrink 的設置會自動放大或縮小。在做兩欄布局時,如果右邊的自適應元素 flex-basis 設為 auto 的話,其本身大小將會是 0 。
2.9 line-height 如何繼承?
父元素的 line-height 寫了具體數值,比如 30px,則子元素 line-height 繼承該值。
父元素的 line-height 寫了比例,比如 1.5 或 2,則子元素 line-height 也是繼承該比例。
父元素的 line-height 寫了百分比,比如 200%,則子元素 line-height 繼承的是父元素 font-size * 200% 計算出來的值。
三、js 基礎
js 的考察其實來回就那些東西,不過就我自己而已學習的時候理解是真的理解了,但是忘也確實會忘(大家都說理解了一定不會忘,但是要答全的話還是需要理解+背)。
1、數據類型
以下是比較重要的幾個 js 變量要掌握的點。
1.1 基本的數據類型介紹,及值類型和引用類型的理解
在 JS 中共有 8 種基礎的數據類型,分別為: Undefined 、 Null 、 Boolean 、 Number 、 String 、 Object 、 Symbol 、 BigInt 。
其中 Symbol 和 BigInt 是 ES6 新增的數據類型,可能會被單獨問:
Symbol 代表獨一無二的值,最大的用法是用來定義對象的唯一屬性名。
BigInt 可以表示任意大小的整數。
值類型的賦值變動過程如下:
let a = 100;
let b = a;
a = 200;
console.log(b); // 200
復制代碼
值類型是直接存儲在棧(stack)中的簡單數據段,占據空間小、大小固定,屬于被頻繁使用數據,所以放入棧中存儲;
引用類型的賦值變動過程如下:
let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30
復制代碼
引用類型存儲在堆(heap)中的對象,占據空間大、大小不固定。如果存儲在棧中,將會影響程序運行的性能;
1.2 數據類型的判斷
typeof:能判斷所有值類型,函數。不可對 null、對象、數組進行精確判斷,因為都返回 object 。
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function
// 不能判別
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
復制代碼
instanceof:能判斷對象類型,不能判斷基本數據類型,其內部運行機制是判斷在其原型鏈中能否找到該類型的原型。比如考慮以下代碼:
class People {}
class Student extends People {}
const vortesnail = new Student();
console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true
復制代碼
其實現就是順著原型鏈去找,如果能找到對應的 Xxxxx.prototype 即為 true 。比如這里的 vortesnail 作為實例,順著原型鏈能找到 Student.prototype 及 People.prototype ,所以都為 true 。
Object.prototype.toString.call():所有原始數據類型都是能判斷的,還有 Error 對象,Date 對象等。
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"
復制代碼
在面試中有一個經常被問的問題就是:如何判斷變量是否為數組?
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
復制代碼
1.3 手寫深拷貝
這個題一定要會啊!筆者面試過程中瘋狂被問到!
/**
深拷貝
@param {Object} obj 要拷貝的對象
@param {Map} map 用于存儲循環引用對象的地址
*/
function deepClone(obj = {}, map = new Map()) {
if (typeof obj !== "object") {
returnobj;
}
if (map.get(obj)) {
returnmap.get(obj);
}
let result = {};
// 初始化返回結果
if (
objinstanceofArray||//加 || 的原因是為了防止Array的 prototype 被重寫,Array.isArray 也是如此Object.prototype.toString(obj) ==="[object Array]"
) {
result=[];
}
// 防止循環引用
map.set(obj, result);
for (const key in obj) {
// 保證 key 不是原型屬性if(obj.hasOwnProperty(key)) {// 遞歸調用result[key]= deepClone(obj[key],map);}
}
// 返回結果
return result;
}
復制代碼
1.4 根據 0.1+0.2 ! == 0.3,講講 IEEE 754 ,如何讓其相等?
原因總結:
進制轉換 :js 在做數字計算的時候,0.1 和 0.2 都會被轉成二進制后無限循環 ,但是 js 采用的 IEEE 754 二進制浮點運算,最大可以存儲 53 位有效數字,于是大于 53 位后面的會全部截掉,將導致精度丟失。
對階運算 :由于指數位數不相同,運算時需要對階運算,階小的尾數要根據階差來右移(0舍1入),尾數位移時可能會發生數丟失的情況,影響精度。
解決辦法:
轉為整數(大數)運算。
function add(a, b) {
const maxLen = Math.max(
a.toString().split(".")[1].length,b.toString().split(".")[1].length
);
const base = 10 ** maxLen;
const bigA = BigInt(base * a);
const bigB = BigInt(base * b);
const bigRes = (bigA + bigB) / BigInt(base); // 如果是 (1n + 2n) / 10n 是等于 0n的。。。
return Number(bigRes);
}
復制代碼
這里代碼是有問題的,因為最后計算 bigRes 的大數相除(即 /)是會把小數部分截掉的,所以我很疑惑為什么網絡上很多文章都說可以通過先轉為整數運算再除回去,為了防止轉為的整數超出 js 表示范圍,還可以運用到 ES6 新增的大數類型,我真的很疑惑,希望有好心人能解答下。
使用 Number.EPSILON 誤差范圍。
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
復制代碼
Number.EPSILON 的實質是一個可以接受的最小誤差范圍,一般來說為 Math.pow(2, -52) 。
轉成字符串,對字符串做加法運算。
// 字符串數字相加
var addStrings = function (num1, num2) {
let i = num1.length - 1;
let j = num2.length - 1;
const res = [];
let carry = 0;
while (i >= 0 || j >= 0) {
const n1 = i >=0?Number(num1[i]):0;const n2 = j >=0?Number(num2[j]):0;const sum = n1 + n2 + carry;res.unshift(sum %10);carry =Math.floor(sum / 10);i--;j--;
}
if (carry) {
res.unshift(carry);
}
return res.join("");
};
function isEqual(a, b, sum) {
const [intStr1, deciStr1] = a.toString().split(".");
const [intStr2, deciStr2] = b.toString().split(".");
const inteSum = addStrings(intStr1, intStr2); // 獲取整數相加部分
const deciSum = addStrings(deciStr1, deciStr2); // 獲取小數相加部分
return inteSum + "." + deciSum === String(sum);
}
console.log(isEqual(0.1, 0.2, 0.3)); // true
復制代碼
2、 原型和原型鏈
可以說這部分每家面試官都會問了。。首先理解的話,其實一張圖即可,一段代碼即可。
function Foo() {}
let f1 = new Foo();
let f2 = new Foo();
復制代碼
千萬別畏懼下面這張圖,特別有用,一定要搞懂,熟到提筆就能默畫出來。
總結:
原型:每一個 JavaScript 對象(null 除外)在創建的時候就會與之關聯另一個對象,這個對象就是我們所說的原型,每一個對象都會從原型"繼承"屬性,其實就是 prototype 對象。
原型鏈:由相互關聯的原型組成的鏈狀結構就是原型鏈。
先說出總結的話,再舉例子說明如何順著原型鏈找到某個屬性。
3、 作用域與作用域鏈
作用域:規定了如何查找變量,也就是確定當前執行代碼對變量的訪問權限。換句話說,作用域決定了代碼區塊中變量和其他資源的可見性。(全局作用域、函數作用域、塊級作用域)
作用域鏈:從當前作用域開始一層層往上找某個變量,如果找到全局作用域還沒找到,就放棄尋找 。這種層級關系就是作用域鏈。(由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈,學習下面的內容之后再考慮這句話)
需要注意的是,js 采用的是靜態作用域,所以函數的作用域在函數定義時就確定了。
總結:當 JavaScript 代碼執行一段可執行代碼時,會創建對應的執行上下文。對于每個執行上下文,都有三個重要屬性:
變量對象(Variable object,VO);
作用域鏈(Scope chain);
this。(關于 this 指向問題,在上面推薦的深入系列也有講從 ES 規范講的,但是實在是難懂
4、 閉包
根據 MDN 中文的定義,閉包的定義如下:
在 JavaScript 中,每當創建一個函數,閉包就會在函數創建的同時被創建出來。可以在一個內層函數中訪問到其外層函數的作用域。
也可以這樣說:
閉包是指那些能夠訪問自由變量的函數。 自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。 閉包 = 函數 + 函數能夠訪問的自由變量。
在回答時,我們這樣答:
在某個內部函數的執行上下文創建時,會將父級函數的活動對象加到內部函數的 [[scope]] 中,形成作用域鏈,所以即使父級函數的執行上下文銷毀(即執行上下文棧彈出父級函數的執行上下文),但是因為其活動對象還是實際存儲在內存中可被內部函數訪問到的,從而實現了閉包。
閉包應用: 函數作為參數被傳遞:
function print(fn) {
const a = 200;
fn();
}
const a = 100;
function fn() {
console.log(a);
}
print(fn); // 100
復制代碼
函數作為返回值被返回:
function create() {
const a = 100;
return function () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100
復制代碼
閉包:自由變量的查找,是在函數定義的地方,向上級作用域查找。不是在執行的地方。
應用實例:比如緩存工具,隱藏數據,只提供 API 。
function createCache() {
const data = {}; // 閉包中被隱藏的數據,不被外界訪問
return {
set: function (key,val) {data[key] =val;},get: function (key) {returndata[key];},
};
}
const c = createCache();
c.set("a", 100);
console.log(c.get("a")); // 100
復制代碼
6、 call、apply、bind 實現
這部分實現還是要知道的,就算工作中不會自己手寫,但是說不準面試官就是要問,知道點原理也好,可以擴寬我們寫代碼的思路。
call
call() 方法在使用一個指定的 this 值和若干個指定的參數值的前提下調用某個函數或方法。
舉個例子:
var obj = {
value: "vortesnail",
};
function fn() {
console.log(this.value);
}
fn.call(obj); // vortesnail
復制代碼
通過 call 方法我們做到了以下兩點:
call 改變了 this 的指向,指向到 obj 。
fn 函數執行了。
那么如果我們自己寫 call 方法的話,可以怎么做呢?我們先考慮改造 obj 。
var obj = {
value: "vortesnail",
fn: function () {
console.log(this.value);
},
};
obj.fn(); // vortesnail
復制代碼
這時候 this 就指向了 obj ,但是這樣做我們手動給 obj 增加了一個 fn 屬性,這顯然是不行的,不用擔心,我們執行完再使用對象屬性的刪除方法(delete)不就行了?
obj.fn = fn;
obj.fn();
delete obj.fn;
復制代碼
根據這個思路,我們就可以寫出來了:
Function.prototype.myCall = function (context) {
// 判斷調用對象
if (typeof this !== "function") {
thrownewError("Type error");
}
// 首先獲取參數
let args = [...arguments].slice(1);
let result = null;
// 判斷 context 是否傳入,如果沒有傳就設置為 window
context = context || window;
// 將被調用的方法設置為 context 的屬性
// this 即為我們要調用的方法
context.fn = this;
// 執行要被調用的方法
result = context.fn(...args);
// 刪除手動增加的屬性方法
delete context.fn;
// 將執行結果返回
return result;
};
復制代碼
apply
我們會了 call 的實現之后,apply 就變得很簡單了,他們沒有任何區別,除了傳參方式。
Function.prototype.myApply = function (context) {
if (typeof this !== "function") {
thrownewError("Type error");
}
let result = null;
context = context || window;
// 與上面代碼相比,我們使用 Symbol 來保證屬性唯一
// 也就是保證不會重寫用戶自己原來定義在 context 中的同名屬性
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 執行要被調用的方法
if (arguments[1]) {
result=context[fnSymbol](...arguments[1]);
} else {
result=context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
復制代碼
bind
bind 返回的是一個函數
Function.prototype.myBind = function (context) {
// 判斷調用對象是否為函數
if (typeof this !== "function") {
thrownewError("Type error");
}
// 獲取參數
const args = [...arguments].slice(1),
const fn = this;
return function Fn() {
returnfn.apply(thisinstanceof Fn ?this: context,// 當前的這個 arguments 是指 Fn 的參數args.concat(...arguments));
};
};
復制代碼
7、 new 實現
首先創一個新的空對象。
根據原型鏈,設置空對象的?proto?為構造函數的 prototype 。
構造函數的 this 指向這個對象,執行構造函數的代碼(為這個新對象添加屬性)。
判斷函數的返回值類型,如果是引用類型,就返回這個引用類型的對象。
function myNew(context) {
const obj = new Object();
obj.__proto__ = context.prototype;
const res = context.apply(obj, [...arguments].slice(1));
return typeof res === "object" ? res : obj;
}
復制代碼
8、 異步
這部分著重要理解 Promise、async awiat、event loop 等。
8.1 event loop、宏任務和微任務
簡單的例子:
console.log("Hi");
setTimeout(function cb() {
console.log("cb"); // cb 即 callback
}, 5000);
console.log("Bye");
復制代碼
它的執行過程是這樣的:
Web APIs 會創建對應的線程,比如 setTimeout 會創建定時器線程,ajax 請求會創建 http 線程。。。這是由 js 的運行環境決定的,比如瀏覽器。
看完上面的視頻之后,至少大家畫 Event Loop 的圖講解不是啥問題了,但是涉及到宏任務和微任務
注意:1.Call Stack 調用棧空閑 -> 2.嘗試 DOM 渲染 -> 觸發 Event loop。
每次 Call Stack 清空(即每次輪詢結束),即同步任務執行完。
都是 DOM 重新渲染的機會,DOM 結構有改變則重新渲染。
然后再去觸發下一次 Event loop。
宏任務:setTimeout,setInterval,Ajax,DOM 事件。 微任務:Promise async/await。
兩者區別:
宏任務:DOM 渲染后觸發,如 setTimeout 、setInterval 、DOM 事件 、script 。
微任務:DOM 渲染前觸發,如 Promise.then 、MutationObserver 、Node 環境下的 process.nextTick 。
從 event loop 解釋,為何微任務執行更早?
微任務是 ES6 語法規定的(被壓入 micro task queue)。
宏任務是由瀏覽器規定的(通過 Web APIs 壓入 Callback queue)。
宏任務執行時間一般比較長。
每一次宏任務開始之前一定是伴隨著一次 event loop 結束的,而微任務是在一次 event loop 結束前執行的。
8.2 Promise
關于這一塊兒沒什么好說的,最好是實現一遍 Promise A+ 規范,多少有點印象,當然面試官也不會叫你默寫一個完整的出來,但是你起碼要知道實現原理。
實現一個 Promise.all:
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
// 參數可以不是數組,但必須具有 Iterator 接口
if (typeof promises[Symbol.iterator] !== "function") {
? reject("Type error");
}
if (promises.length === 0) {
? resolve([]);
} else {
? const res = [];
? let count = 0;
? const len = promises.length;
? for (let i = 0; i < len; i++) {
? ? //考慮到 promises[i] 可能是 thenable 對象也可能是普通值
? ? Promise.resolve(promises[i])
? ? ? .then((data) => {
? ? ? ? res[i] = data;
? ? ? ? if (++count === len) {
? ? ? ? ? resolve(res);
? ? ? ? }
? ? ? })
? ? ? .catch((err) => {
? ? ? ? reject(err);
? ? ? });
? }
}
});
};
復制代碼
8.3 async/await 和 Promise 的關系
async/await 是消滅異步回調的終極武器。
但和 Promise 并不互斥,反而,兩者相輔相成。
執行 async 函數,返回的一定是 Promise 對象。
await 相當于 Promise 的 then。
tru...catch 可捕獲異常,代替了 Promise 的 catch。
9、 瀏覽器的垃圾回收機制
總結一下:
有兩種垃圾回收策略:
標記清除:標記階段即為所有活動對象做上標記,清除階段則把沒有標記(也就是非活動對象)銷毀。
引用計數:它把對象是否不再需要簡化定義為對象有沒有其他對象引用到它。如果沒有引用指向該對象(引用計數為 0),對象將被垃圾回收機制回收。
標記清除的缺點:
內存碎片化,空閑內存塊是不連續的,容易出現很多空閑內存塊,還可能會出現分配所需內存過大的對象時找不到合適的塊。
分配速度慢,因為即便是使用 First-fit 策略,其操作仍是一個 O(n) 的操作,最壞情況是每次都要遍歷到最后,同時因為碎片化,大對象的分配效率會更慢。
解決以上的缺點可以使用?標記整理(Mark-Compact)算法?,標記結束后,標記整理算法會將活著的對象(即不需要清理的對象)向內存的一端移動,最后清理掉邊界的內存(如下圖)
引用計數的缺點:
需要一個計數器,所占內存空間大,因為我們也不知道被引用數量的上限。
解決不了循環引用導致的無法回收問題。
V8 的垃圾回收機制也是基于標記清除算法,不過對其做了一些優化。
針對新生區采用并行回收。
針對老生區采用增量標記與惰性回收。
10、 實現一個 EventMitter 類
EventMitter 就是發布訂閱模式的典型應用:
export class EventEmitter {
private _events: Record<string, Array<Function>>;
constructor() {
this._events = Object.create(null);
}
emit(evt: string, ...args: any[]) {
if (!this._events[evt]) return false;
const fns = [...this._events[evt]];
fns.forEach((fn) => {
? fn.apply(this, args);
});
return true;
}
on(evt: string, fn: Function) {
if (typeof fn !== "function") {
? throw new TypeError("The evet-triggered callback must be a function");
}
if (!this._events[evt]) {
? this._events[evt] = [fn];
} else {
? this._events[evt].push(fn);
}
}
once(evt: string, fn: Function) {
const execFn = () => {
? fn.apply(this);
? this.off(evt, execFn);
};
this.on(evt, execFn);
}
off(evt: string, fn?: Function) {
if (!this._events[evt]) return;
if (!fn) {
? this._events[evt] && (this._events[evt].length = 0);
}
let cb;
const cbLen = this._events[evt].length;
for (let i = 0; i < cbLen; i++) {
? cb = this._events[evt][i];
? if (cb === fn) {
? ? this._events[evt].splice(i, 1);
? ? break;
? }
}
}
removeAllListeners(evt?: string) {
if (evt) {
? this._events[evt] && (this._events[evt].length = 0);
} else {
? this._events = Object.create(null);
}
}
}
復制代碼
四、web 存儲
要掌握 cookie,localStorage 和 sessionStorage。
1、cookie
本身用于瀏覽器和 server 通訊。
被“借用”到本地存儲來的。
可用 document.cookie = '...' 來修改。
其缺點:
存儲大小限制為 4KB。
http 請求時需要發送到服務端,增加請求數量。
只能用 document.cookie = '...' 來修改,太過簡陋。
2、localStorage 和 sessionStorage
HTML5 專門為存儲來設計的,最大可存 5M。
API 簡單易用, setItem getItem。
不會隨著 http 請求被發送到服務端。
它們的區別:
localStorage 數據會永久存儲,除非代碼刪除或手動刪除。
sessionStorage 數據只存在于當前會話,瀏覽器關閉則清空。
一般用 localStorage 會多一些。
五、Http
前端工程師做出網頁,需要通過網絡請求向后端獲取數據,因此 http 協議是前端面試的必考內容。
1、http 狀態碼
1.1 狀態碼分類
1xx - 服務器收到請求。
2xx - 請求成功,如 200。
3xx - 重定向,如 302。
4xx - 客戶端錯誤,如 404。
5xx - 服務端錯誤,如 500。
1.2 常見狀態碼
200 - 成功。
301 - 永久重定向(配合 location,瀏覽器自動處理)。
302 - 臨時重定向(配合 location,瀏覽器自動處理)。
304 - 資源未被修改。
403 - 沒權限。
404 - 資源未找到。
500 - 服務器錯誤。
504 - 網關超時。
1.3 關于協議和規范
狀態碼都是約定出來的。
要求大家都跟著執行。
不要違反規范,例如 IE 瀏覽器。
2、http 緩存
關于緩存的介紹。
http 緩存策略(強制緩存 + 協商緩存)。
刷新操作方式,對緩存的影響。
4.1 關于緩存
什么是緩存? 把一些不需要重新獲取的內容再重新獲取一次
為什么需要緩存? 網絡請求相比于 CPU 的計算和頁面渲染是非常非常慢的。
哪些資源可以被緩存? 靜態資源,比如 js css img。
4.2 強制緩存
Cache-Control:
在 Response Headers 中。
控制強制緩存的邏輯。
例如 Cache-Control: max-age=3153600(單位是秒)
Cache-Control 有哪些值:
max-age:緩存最大過期時間。
no-cache:可以在客戶端存儲資源,每次都必須去服務端做新鮮度校驗,來決定從服務端獲取新的資源(200)還是使用客戶端緩存(304)。
no-store:永遠都不要在客戶端存儲資源,永遠都去原始服務器去獲取資源。
4.3 協商緩存(對比緩存)
服務端緩存策略。
服務端判斷客戶端資源,是否和服務端資源一樣。
一致則返回 304,否則返回 200 和最新的資源。
資源標識:
在 Response Headers 中,有兩種。
Last-Modified:資源的最后修改時間。
Etag:資源的唯一標識(一個字符串,類似于人類的指紋)。
Last-Modified:
服務端拿到 if-Modified-Since 之后拿這個時間去和服務端資源最后修改時間做比較,如果一致則返回 304 ,不一致(也就是資源已經更新了)就返回 200 和新的資源及新的 Last-Modified。
Etag:
其實 Etag 和 Last-Modified 一樣的,只不過 Etag 是服務端對資源按照一定方式(比如 contenthash)計算出來的唯一標識,就像人類指紋一樣,傳給客戶端之后,客戶端再傳過來時候,服務端會將其與現在的資源計算出來的唯一標識做比較,一致則返回 304,不一致就返回 200 和新的資源及新的 Etag。
兩者比較:
優先使用 Etag。
Last-Modified 只能精確到秒級。
如果資源被重復生成,而內容不變,則 Etag 更精確。
4.4 綜述
4.4 三種刷新操作對 http 緩存的影響
正常操作:地址欄輸入 url,跳轉鏈接,前進后退等。
手動刷新:f5,點擊刷新按鈕,右鍵菜單刷新。
強制刷新:ctrl + f5,shift+command+r。
正常操作:強制緩存有效,協商緩存有效。 手動刷新:強制緩存失效,協商緩存有效。 強制刷新:強制緩存失效,協商緩存失效。
面試
比如會被經常問到的: GET 和 POST 的區別。
從緩存的角度,GET 請求會被瀏覽器主動緩存下來,留下歷史記錄,而 POST 默認不會。
從編碼的角度,GET 只能進行 URL 編碼,只能接收 ASCII 字符,而 POST 沒有限制。
從參數的角度,GET 一般放在 URL 中,因此不安全,POST 放在請求體中,更適合傳輸敏感信息。
從冪等性的角度,GET 是冪等的,而 POST 不是。(冪等表示執行相同的操作,結果也是相同的)
從 TCP 的角度,GET 請求會把請求報文一次性發出去,而 POST 會分為兩個 TCP 數據包,首先發 header 部分,如果服務器響應 100(continue), 然后發 body 部分。(火狐瀏覽器除外,它的 POST 請求只發一個 TCP 包)
HTTP/2 有哪些改進?(很大可能問原理)
頭部壓縮。
多路復用。
服務器推送。
六、React
1、 React 事件機制,React 16 和 React 17 事件機制的不同
為什么要自定義事件機制?
抹平瀏覽器差異,實現更好的跨平臺。
避免垃圾回收,React 引入事件池,在事件池中獲取或釋放事件對象,避免頻繁地去創建和銷毀。
方便事件統一管理和事務機制。
2、class component
不排除現在還會有面試官問關于 class component 的問題。
2.1 生命周期
初始化階段。
發生在 constructor 中的內容,在 constructor 中進行 state 、props 的初始化,在這個階段修改 state,不會執行更新階段的生命周期,可以直接對 state 賦值。
掛載階段。
componentWillMount
發生在 render 函數之前,還沒有掛載 Dom
render
componentDidMount
發生在 render 函數之后,已經掛載 Dom
復制代碼
更新階段。
更新階段分為由 state 更新引起和 props 更新引起。
props 更新時:
componentWillReceiveProps(nextProps,nextState)
這個生命周期主要為我們提供對 props 發生改變的監聽,如果你需要在 props 發生改變后,相應改變組件的一些 state。在這個方法中改變 state 不會二次渲染,而是直接合并 state。
shouldComponentUpdate(nextProps,nextState)
這個生命周期需要返回一個 Boolean 類型的值,判斷是否需要更新渲染組件,優化 react 應用的主要手段之一,當返回 false 就不會再向下執行生命周期了,在這個階段不可以 setState(),會導致循環調用。
componentWillUpdate(nextProps,nextState)
這個生命周期主要是給我們一個時機能夠處理一些在 Dom 發生更新之前的事情,如獲得 Dom 更新前某些元素的坐標、大小等,在這個階段不可以 setState(),會導致循環調用。
一直到這里 this.props 和 this.state 都還未發生更新
render
componentDidUpdate(prevProps, prevState)
在此時已經完成渲染,Dom 已經發生變化,state 已經發生更新,prevProps、prevState 均為上一個狀態的值。
state 更新時(具體同上)
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
復制代碼
卸載階段。
componentWillUnmount
在組件卸載及銷毀之前直接調用。在此方法中執行必要的清理操作,例如,清除 timer,取消網絡請求或清除在 componentDidMount 中創建的訂閱等。componentWillUnmount 中不應調用 setState,因為該組件將永遠不會重新渲染。組件實例卸載后,將永遠不會再掛載它。
在 React 16 中官方已經建議刪除以下三個方法,非要使用必須加前綴:UNSAVE_ 。
componentWillMount;
componentWillReceiveProps;
componentWillUpdate;
復制代碼
取代這兩三個生命周期的以下兩個新的。
static getDerivedStateFromProps(nextProps,nextState)
在組件實例化、接收到新的 props 、組件狀態更新時會被調用
getSnapshotBeforeUpdate(prevProps,prevState)
在這個階段我們可以拿到上一個狀態 Dom 元素的坐標、大小的等相關信息。用于替代舊的生命周期中的 componentWillUpdate。
該函數的返回值將會作為 componentDidUpdate 的第三個參數出現。
復制代碼
需要注意的是,一般都會問為什么要廢棄三個生命周期,原因是什么。
2.2 setState 同步還是異步
setState 本身代碼的執行肯定是同步的,這里的異步是指是多個 state 會合成到一起進行批量更新。 同步還是異步取決于它被調用的環境。
如果 setState 在 React 能夠控制的范圍被調用,它就是異步的。比如合成事件處理函數,生命周期函數, 此時會進行批量更新,也就是將狀態合并后再進行 DOM 更新。
如果 setState 在原生 JavaScript 控制的范圍被調用,它就是同步的。比如原生事件處理函數,定時器回調函數,Ajax 回調函數中,此時 setState 被調用后會立即更新 DOM 。
3、對函數式編程的理解
總結一下: 函數式編程有兩個核心概念。
數據不可變(無副作用): 它要求你所有的數據都是不可變的,這意味著如果你想修改一個對象,那你應該創建一個新的對象用來修改,而不是修改已有的對象。
無狀態: 主要是強調對于一個函數,不管你何時運行,它都應該像第一次運行一樣,給定相同的輸入,給出相同的輸出,完全不依賴外部狀態的變化。
純函數帶來的意義。
便于測試和優化:這個意義在實際項目開發中意義非常大,由于純函數對于相同的輸入永遠會返回相同的結果,因此我們可以輕松斷言函數的執行結果,同時也可以保證函數的優化不會影響其他代碼的執行。
可緩存性:因為相同的輸入總是可以返回相同的輸出,因此,我們可以提前緩存函數的執行結果。
更少的 Bug:使用純函數意味著你的函數中不存在指向不明的 this,不存在對全局變量的引用,不存在對參數的修改,這些共享狀態往往是絕大多數 bug 的源頭。
4、react hooks
現在應該大多數面試官會問 hooks 相關的啦。
4.1 為什么不能在條件語句中寫 hook
hook 在每次渲染時的查找是根據一個“全局”的下標對鏈表進行查找的,如果放在條件語句中使用,有一定幾率會造成拿到的狀態出現錯亂。
4.2 HOC 和 hook 的區別
hoc 能復用邏輯和視圖,hook 只能復用邏輯。
4.3 useEffect 和 useLayoutEffect 區別
對于 React 的函數組件來說,其更新過程大致分為以下步驟:
因為某個事件 state 發生變化。
React 內部更新 state 變量。
React 處理更新組件中 return 出來的 DOM 節點(進行一系列 dom diff 、調度等流程)。
將更新過后的 DOM 數據繪制到瀏覽器中。
用戶看到新的頁面。
useEffect 在第 4 步之后執行,且是異步的,保證了不會阻塞瀏覽器進程。 useLayoutEffect 在第 3 步至第 4 步之間執行,且是同步代碼,所以會阻塞后面代碼的執行。
4.4 useEffect 依賴為空數組與 componentDidMount 區別
在 render 執行之后,componentDidMount 會執行,如果在這個生命周期中再一次 setState ,會導致再次 render ,返回了新的值,瀏覽器只會渲染第二次 render 返回的值,這樣可以避免閃屏。
但是 useEffect 是在真實的 DOM 渲染之后才會去執行,這會造成兩次 render ,有可能會閃屏。
實際上 useLayoutEffect 會更接近 componentDidMount 的表現,它們都同步執行且會阻礙真實的 DOM 渲染的。
4.5 React.memo() 和 React.useMemo() 的區別
memo 是一個高階組件,默認情況下會對 props 進行淺比較,如果相等不會重新渲染。多數情況下我們比較的都是引用類型,淺比較就會失效,所以我們可以傳入第二個參數手動控制。
useMemo 返回的是一個緩存值,只有依賴發生變化時才會去重新執行作為第一個參數的函數,需要記住的是,useMemo 是在 render 階段執行的,所以不要在這個函數內部執行與渲染無關的操作,諸如副作用這類的操作屬于 useEffect 的適用范疇。
4.6 React.useCallback() 和 React.useMemo() 的區別
useCallback 可緩存函數,其實就是避免每次重新渲染后都去重新執行一個新的函數。
useMemo 可緩存值。
有很多時候,我們在 useEffect 中使用某個定義的外部函數,是要添加到 deps 數組中的,如果不用 useCallback 緩存,這個函數在每次重新渲染時都是一個完全新的函數,也就是引用地址發生了變化,這就會導致 useEffect 總會無意義的執行。
4.7 React.forwardRef 是什么及其作用
一般在父組件要拿到子組件的某個實際的 DOM 元素時會用到。
6、react hooks 與 class 組件對比
react hooks 與 class 組件對比 函數式組件與類組件有何不同
7、介紹 React dom diff 算法
讓虛擬 DOM 和 DOM-diff 不再成為你的絆腳石。
8、對 React Fiber 的理解
然后我們再閱讀下其它作者對于 React Fiber 的理解,再轉化為我們自己的思考總結
9、React 性能優化手段
使用 React.memo 來緩存組件。
使用 React.useMemo 緩存大量的計算。
避免使用匿名函數。
利用 React.lazy 和 React.Suspense 延遲加載不是立即需要的組件。
盡量使用 CSS 而不是強制加載和卸載組件。
使用 React.Fragment 避免添加額外的 DOM。
九、性能優化
代碼層面:
防抖和節流(resize,scroll,input)。
減少回流(重排)和重繪。
事件委托。
css 放 ,js 腳本放 最底部。
減少 DOM 操作。
按需加載,比如 React 中使用 React.lazy 和 React.Suspense ,通常需要與 webpack 中的 splitChunks 配合。
構建方面:
壓縮代碼文件,在 webpack 中使用 terser-webpack-plugin 壓縮 Javascript 代碼;使用 css-minimizer-webpack-plugin 壓縮 CSS 代碼;使用 html-webpack-plugin 壓縮 html 代碼。
開啟 gzip 壓縮,webpack 中使用 compression-webpack-plugin ,node 作為服務器也要開啟,使用 compression。
常用的第三方庫使用 CDN 服務,在 webpack 中我們要配置 externals,將比如 React, Vue 這種包不打倒最終生成的文件中。而是采用 CDN 服務。
其它:
使用 http2。因為解析速度快,頭部壓縮,多路復用,服務器推送靜態資源。
使用服務端渲染。
圖片壓縮。
使用 http 緩存,比如服務端的響應中添加 Cache-Control / Expires 。
十、常見手寫
以下的內容是上面沒有提到的手寫,比如 new 、Promise.all 這種上面內容中已經提到了如何寫。
1、防抖
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
? let callNow = !timeout;
? timeout = setTimeout(function () {
? ? timeout = null;
? }, wait);
? if (callNow) func.apply(context, args);
} else {
? timeout = setTimeout(function () {
? ? func.apply(context, args);
? }, wait);
}
};
}
復制代碼
2、節流
// 使用時間戳
function throttle(func, wait) {
let preTime = 0;
return function () {
let nowTime = +new Date();
let context = this;
let args = arguments;
if (nowTime - preTime > wait) {
? func.apply(context, args);
? preTime = nowTime;
}
};
}
// 定時器實現
function throttle(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (!timeout) {
? timeout = setTimeout(function () {
? ? timeout = null;
? ? func.apply(context, args);
? }, wait);
}
};
}
復制代碼
3、快速排序
function sortArray(nums) {
quickSort(0, nums.length - 1, nums);
return nums;
}
function quickSort(start, end, arr) {
if (start < end) {
const mid = sort(start, end, arr);
quickSort(start, mid - 1, arr);
quickSort(mid + 1, end, arr);
}
}
function sort(start, end, arr) {
const base = arr[start];
let left = start;
let right = end;
while (left !== right) {
while (arr[right] >= base && right > left) {
? right--;
}
arr[left] = arr[right];
while (arr[left] <= base && right > left) {
? left++;
}
arr[right] = arr[left];
}
arr[left] = base;
return left;
}
復制代碼
4、instanceof
這個手寫一定要懂原型及原型鏈。
function myInstanceof(target, origin) {
if (typeof target !== "object" || target === null) return false;
if (typeof origin !== "function")
thrownewTypeError("origin must be function");
let proto = Object.getPrototypeOf(target); // 相當于 proto = target.__proto__;
while (proto) {
if (proto===origin.prototype) return true;proto=Object.getPrototypeOf(proto);
}
return false;
}
復制代碼
5、數組扁平化
重點,不要覺得用不到就不管,這道題就是考察你對 js 語法的熟練程度以及手寫代碼的基本能力。
function flat(arr, depth = 1) {
if (depth > 0) {
// 以下代碼還可以簡化,不過為了可讀性,還是....
return arr.reduce((pre, cur) => {
? return pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur);
}, []);
}
return arr.slice();
}
復制代碼
6、手寫 reduce
先不考慮第二個參數初始值:
Array.prototype.reduce = function (cb) {
const arr = this; //this就是調用reduce方法的數組
let total = arr[0]; // 默認為數組的第一項
for (let i = 1; i < arr.length; i++) {
total= cb(total, arr[i], i, arr);
}
return total;
};
復制代碼
考慮上初始值:
Array.prototype.reduce = function (cb, initialValue) {
const arr = this;
let total = initialValue || arr[0];
// 有初始值的話從0遍歷,否則從1遍歷
for (let i = initialValue ? 0 : 1; i < arr.length; i++) {
total= cb(total, arr[i], i, arr);
}
return total;
};
復制代碼
7、帶并發的異步調度器 Scheduler
JS 實現一個帶并發限制的異度調度器 Scheduler,保證同時運行的任務最多有兩個。完善下面代碼中的 Scheduler 類,使得以下程序能正確輸出。
class Scheduler {
add(promiseMaker) {}
}
const timeout = (time) =>
new Promise((resolve) => {
setTimeout(resolve,time);
});
const scheduler = new Scheduler();
const addTask = (time, order) => {
scheduler.add(() => timeout(time).then(() => console.log(order)));
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
// output:2 3 1 4
// 一開始,1,2兩個任務進入隊列。
// 500ms 時,2完成,輸出2,任務3入隊。
// 800ms 時,3完成,輸出3,任務4入隊。
// 1000ms 時,1完成,輸出1。
復制代碼
根據題目,我們只需要操作 Scheduler 類就行:
class Scheduler {
constructor() {
this.waitTasks = []; // 待執行的任務隊列
this.excutingTasks = []; // 正在執行的任務隊列
this.maxExcutingNum = 2; // 允許同時運行的任務數量
}
add(promiseMaker) {
if (this.excutingTasks.length < this.maxExcutingNum) {
? this.run(promiseMaker);
} else {
? this.waitTasks.push(promiseMaker);
}
}
run(promiseMaker) {
const len = this.excutingTasks.push(promiseMaker);
const index = len - 1;
promiseMaker().then(() => {
? this.excutingTasks.splice(index, 1);
? if (this.waitTasks.length > 0) {
? ? this.run(this.waitTasks.shift());
? }
});
}
}
復制代碼
8、去重
利用 ES6 set 關鍵字:
function unique(arr) {
return [...new Set(arr)];
}
復制代碼
利用 ES5 filter 方法:
function unique(arr) {
return arr.filter((item, index, array) => {
return array.indexOf(item)===index;
});
}
復制代碼
十一、其它
requestAnimationFrame
如何排查內存泄漏問題,面試官可能會問為什么頁面越來越卡頓,直至卡死,怎么定位到產生這種現象的源代碼(開發環境)?
vite 大火,我復習的時候是去年 9 月份,還沒那么火,可能現在的你需要學一學了~
vue3 也一樣,如果你是 React 技術棧(就像我之前一樣)當我沒說。