在掘金上看到了一位大佬發了一篇很詳細的面試記錄文章 "《一年半經驗,百度、有贊、阿裡面試總結》" ,為了查漏補缺,抽空就詳細做了下。( 估計只有我這麼無聊了哈哈哈 ) 有給出的或者有些不完善的答案,也儘力給出/完善了(可能有錯,大家自行辨別)。有些很困難的題目(例如實現 ),附帶相關鏈接(懶癌患者福 ...
在掘金上看到了一位大佬發了一篇很詳細的面試記錄文章-《一年半經驗,百度、有贊、阿裡面試總結》,為了查漏補缺,抽空就詳細做了下。(估計只有我這麼無聊了哈哈哈)
有給出的或者有些不完善的答案,也儘力給出/完善了(可能有錯,大家自行辨別)。有些很困難的題目(例如實現Promise
),附帶相關鏈接(懶癌患者福利)。
總的來說,將這些題目分成了“Javascript”、“CSS”、“瀏覽器/協議”、“演算法”和“Web工程化”5個部分進行回答和代碼實現。
最後,歡迎來我的博客和我扯犢子:godbmw.com。直接戳本篇原文的地址:刷《一年半經驗,百度、有贊、阿裡面試總結》·手記
1. Javascript相關
1.1 迴文字元串
題目:實現一個函數,判斷是不是迴文字元串
原文的思路是將字元串轉化成數組=>反轉數組=>拼接成字元串。這種做法充分利用了js的BIF,但性能有所損耗:
function run(input) {
if (typeof input !== 'string') return false;
return input.split('').reverse().join('') === input;
}
其實正常思路也很簡單就能實現,性能更高,但是沒有利用js的特性:
// 迴文字元串
const palindrome = (str) => {
// 類型判斷
if(typeof str !== 'string') {
return false;
}
let len = str.length;
for(let i = 0; i < len / 2; ++i){
if(str[i] !== str[len - i - 1]){
return false;
}
}
return true;
}
1.2 實現Storage
題目:實現Storage,使得該對象為單例,並對localStorage進行封裝設置值setItem(key,value)和getItem(key)
題目重點是單例模式,需要註意的是藉助localStorage
,不是讓自己手動實現!
const Storage = () => {}
Storage.prototype.getInstance = (() => {
let instance = null
return () => {
if(!instance){
instance = new Storage()
}
return instance
}
})()
Storage.prototype.setItem = (key, value) => {
return localStorage.setItem(key, value)
}
Storage.prototype.getItem = (key) => {
return localStorage.getItem(key)
}
1.3 JS事件流
題目:說說事件流吧
事件流分為冒泡和捕獲。
事件冒泡:子元素的觸發事件會一直向父節點傳遞,一直到根結點停止。此過程中,可以在每個節點捕捉到相關事件。可以通過stopPropagation
方法終止冒泡。
事件捕獲:和“事件冒泡”相反,從根節點開始執行,一直向子節點傳遞,直到目標節點。印象中只有少數瀏覽器的老舊版本才是這種事件流,可以忽略。
1.4 實現函數繼承
題目:現在有一個函數A和函數B,請你實現B繼承A。並且說明他們優缺點。
方法一:綁定構造函數
優點:可以實現多繼承
缺點:不能繼承父類原型方法/屬性
function Animal(){
this.species = "動物";
}
function Cat(){
Animal.apply(this, arguments); // 父對象的構造函數綁定到子節點上
}
var cat = new Cat()
console.log(cat.species) // 輸出:動物
方法二:原型鏈繼承
優點:能夠繼承父類原型和實例方法/屬性,並且可以捕獲父類的原型鏈改動
缺點:無法實現多繼承,會浪費一些記憶體(Cat.prototype.constructor = Cat
)。除此之外,需要註意應該將Cat.prototype.constructor
重新指向本身。
js中交換原型鏈,均需要修複prototype.constructor
指向問題。
function Animal(){
this.species = "動物";
}
Animal.prototype.func = function(){
console.log("heel")
}
function Cat(){}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
var cat = new Cat()
console.log(cat.func, cat.species)
方法3:結合上面2種方法
function Animal(){
this.species = "動物";
}
Animal.prototype.func = function(){
console.log("heel")
}
function Cat(){
Animal.apply(this, arguments)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat;
var cat = new Cat()
console.log(cat.func, cat.species)
1.5 ES5對象 vs ES6對象
題目:es6 class 的new實例和es5的new實例有什麼區別?
在ES6
中(和ES5
相比),class
的new
實例有以下特點:
class
的構造參數必須是new
來調用,不可以將其作為普通函數執行es6
的class
不存在變數提升- 最重要的是:
es6
內部方法不可以枚舉。es5的prototype
上的方法可以枚舉。
為此我做了以下測試代碼進行驗證:
console.log(ES5Class()) // es5:可以直接作為函數運行
// console.log(new ES6Class()) // 會報錯:不存在變數提升
function ES5Class(){
console.log("hello")
}
ES5Class.prototype.func = function(){ console.log("Hello world") }
class ES6Class{
constructor(){}
func(){
console.log("Hello world")
}
}
let es5 = new ES5Class()
let es6 = new ES6Class()
console.log("ES5 :")
for(let _ in es5){
console.log(_)
}
// es6:不可枚舉
console.log("ES6 :")
for(let _ in es6){
console.log(_)
}
這篇《JavaScript創建對象—從es5到es6》對這個問題的深入解釋很好,推薦觀看!
1.6 實現MVVM
題目:請簡單實現雙向數據綁定mvvm
vuejs是利用Object.defineProperty
來實現的MVVM,採用的是訂閱發佈模式。每個data
中都有set和get屬性,這種點對點的效率,比Angular
實現MVVM的方式的效率更高。
<body>
<input type="text">
<script>
const input = document.querySelector('input')
const obj = {}
Object.defineProperty(obj, 'data', {
enumerable: false, // 不可枚舉
configurable: false, // 不可刪除
set(value){
input.value = value
_value = value
// console.log(input.value)
},
get(){
return _value
}
})
obj.data = '123'
input.onchange = e => {
obj.data = e.target.value
}
</script>
</body>
1.7 實現Promise
這是一位大佬實現的Promise
版本:過了Promie/A+
標準的測試!!!網上能搜到的基本都是從這篇文章變形而來或者直接照搬!!!原文地址,直接戳:剖析Promise內部結構,一步一步實現一個完整的、能通過所有Test case的Promise類
下麵附上一種近乎完美的實現:可能無法和其他Promise
庫的實現無縫對接。但是,上面的原文實現了全部的,歡迎Mark!
function MyPromise(executor){
var that = this
this.status = 'pending' // 當前狀態
this.data = undefined
this.onResolvedCallback = [] // Promise resolve時的回調函數集,因為在Promise結束之前有可能有多個回調添加到它上面
this.onRejectedCallback = [] // Promise reject時的回調函數集,因為在Promise結束之前有可能有多個回調添加到它上面
// 更改狀態 => 綁定數據 => 執行回調函數集
function resolve(value){
if(that.status === 'pending'){
that.status = 'resolved'
that.data = value
for(var i = 0; i < that.onResolvedCallback.length; ++i){
that.onResolvedCallback[i](value)
}
}
}
function reject(reason){
if(that.status === 'pending'){
that.status = 'rejected'
that.data = reason
for(var i = 0; i < that.onResolvedCallback.length; ++i){
that.onRejectedCallback[i](reason)
}
}
}
try{
executor(resolve, reject) // resolve, reject兩個函數可以在外部傳入的函數(executor)中調用
} catch(e) { // 考慮到執行過程可能有錯
reject(e)
}
}
// 標準是沒有catch方法的,實現了then,就實現了catch
// then/catch 均要返回一個新的Promise實例
MyPromise.prototype.then = function(onResolved, onRejected){
var that = this
var promise2
// 值穿透
onResolved = typeof onResolved === 'function' ? onResolved : function(v){ return v }
onRejected = typeof onRejected === 'function' ? onRejected : function(r){ return r }
if(that.status === 'resolved'){
return promise2 = new MyPromise(function(resolve, reject){
try{
var x = onResolved(that.data)
if(x instanceof MyPromise){ // 如果onResolved的返回值是一個Promise對象,直接取它的結果做為promise2的結果
x.then(resolve, reject)
}
resolve(x) // 否則,以它的返回值做為promise2的結果
} catch(e) {
reject(e) // 如果出錯,以捕獲到的錯誤做為promise2的結果
}
})
}
if(that.status === 'rejected'){
return promise2 = new MyPromise(function(resolve, reject){
try{
var x = onRejected(that.data)
if(x instanceof MyPromise){
x.then(resolve, reject)
}
} catch(e) {
reject(e)
}
})
}
if(that.status === 'pending'){
return promise2 = new MyPromise(function(resolve, reject){
self.onResolvedCallback.push(function(reason){
try{
var x = onResolved(that.data)
if(x instanceof MyPromise){
x.then(resolve, reject)
}
} catch(e) {
reject(e)
}
})
self.onRejectedCallback.push(function(value){
try{
var x = onRejected(that.data)
if(x instanceof MyPromise){
x.then(resolve, reject)
}
} catch(e) {
reject(e)
}
})
})
}
}
MyPromise.prototype.catch = function(onRejected){
return this.then(null, onRejected)
}
// 以下是簡單的測試樣例:
new MyPromise(resolve => resolve(8)).then(value => {
console.log(value)
})
1.8 Event Loop
題目:說一下JS的EventLoop
其實阮一峰老師這篇《JavaScript 運行機制詳解:再談Event Loop》已經講的很清晰了(手動贊)!
這裡簡單總結下:
- JS是單線程的,其上面的所有任務都是在兩個地方執行:執行棧和任務隊列。前者是存放同步任務;後者是非同步任務有結果後,就在其中放入一個事件。
- 當執行棧的任務都執行完了(棧空),js會讀取任務隊列,並將可以執行的任務從任務隊列丟到執行棧中執行。
- 這個過程是迴圈進行,所以稱作
Loop
。
2. CSS相關
2.1 水平垂直居中
題目: 兩種以上方式實現已知或者未知寬度的垂直水平居中
第一種方法就是利用CSS3
的translate
進行偏移定位,註意:兩個參數的百分比都是針對元素本身計算的。
.wrap {
position: relative;
width: 100vw;
height: 100vh;
}
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
第二種方法是利用CSS3
的flex
佈局,父元素diplay屬性設置為flex
,並且定義元素在兩條軸線的佈局方式均為center
.wrap {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
}
.wrap .box {
width: 100px;
height: 100px;
}
第三種方法是利用margin負值來進行元素偏移,優點是瀏覽器相容好,缺點是不夠靈活(要自行計算margin的值):
.wrap {
position: relative;
width: 100vw;
height: 100vh;
}
.box {
position: absolute;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
margin: -50px 0 0 -50px;
}
2.2 “點擊”改變樣式
題目:實現效果,點擊容器內的圖標,圖標邊框變成border 1px solid red,點擊空白處重置。
利用event.target
可以判斷是否是指定元素本身(判斷“空白處”),除此之外,註意禁止冒泡(題目指明瞭“容器內”)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#app {
min-width: 100vw;
min-height: 100vh;
}
#app .icon{
display: inline-block;
cursor: pointer;
}
</style>
</head>
<body>
<div id="app">
<span class="icon">123456</span>
</div>
<script>
const app = document.querySelector("#app")
const icon = document.querySelector(".icon")
app.addEventListener("click", e => {
if(e.target === icon){
return;
}
// 非空白處才去除 border
icon.style.border = "none";
})
icon.addEventListener("click", e => {
// 禁止冒泡
e.stopPropagation()
// 更改樣式
icon.style.border = "1px solid red";
})
</script>
</body>
</html>
3. 瀏覽器/協議相關
3.1 緩存機制
題目:說一下瀏覽器的緩存機制。
瀏覽器緩存分為強緩存和協商緩存。緩存的作用是提高客戶端速度、節省網路流量、降低伺服器壓力。
強緩存:瀏覽器請求資源,如果header中的Cache-Control
和Expires
沒有過期,直接從緩存(本地)讀取資源,不需要再向伺服器請求資源。
協商緩存:瀏覽器請求的資源如果是過期的,那麼會向伺服器發送請求,header中帶有Etag
欄位。伺服器再進行判斷,如果ETag匹配,則返回給客戶端300系列狀態碼,客戶端繼續使用本地緩存;否則,客戶端會重新獲取數據資源。
關於過程中詳細的欄位,可以參考這篇《http協商緩存VS強緩存》
3.2 從URL到頁面生成
題目:輸入URL到看到頁面發生的全過程,越詳細越好
- DNS解析
- 建立TCP連接(3次握手)
- 發送HTTP請求,從伺服器下載相關內容
- 瀏覽器構建DOM樹和CSS樹,然後生成渲染樹。這個一個漸進式過程,引擎會力求最快將內容呈現給用戶。
- 在第四步的過程中,
<script>
的位置和載入方式會影響響應速度。 - 搞定了,關閉TCP連接(4次握手)
3.3 TCP握手
題目:解釋TCP建立的時候的3次握手和關閉時候的4次握手
看這題的時候,我也是突然懵(手動捂臉)。推薦翻一下電腦網路的相關書籍,對於FIN
、ACK
等欄位的講解很贊!
3.4 CSS和JS位置
題目:CSS和JS的位置會影響頁面效率,為什麼?
先說CSS。CSS的位置不會影響載入速度,但是CSS一般放在<head>
標簽中。前面有說DOM樹和CSS樹共同生成渲染樹,CSS位置太靠後的話,在CSS載入之前,可能會出現閃屏、樣式混亂、白屏等情況。
再說JS。JS是阻塞載入,預設的<script>
標簽會載入並且立即執行腳本,如果腳本很複雜或者網路不好,會出現很久的白屏。所以,JS標簽一般放到<body>
標簽最後。
現在,也可以為<script>
標簽設置async
或者defer
屬性。前者是js腳本的載入和執行將與後續文檔的載入和渲染同步執行。後者是js腳本的載入將與後續文檔的載入和渲染同步執行,當所有元素解析完,再執行js腳本。
4. 演算法相關
4.1 數組全排列
題目:現在有一個數組[1,2,3,4],請實現演算法,得到這個數組的全排列的數組,如[2,1,3,4],[2,1,4,3]。。。。你這個演算法的時間複雜度是多少
實現思路:從“開始元素”起,每個元素都和開始元素進行交換;不斷縮小範圍,最後輸出這種排列。暴力法的時間複雜度是 \(O(N_N)\),遞歸實現的時間複雜度是 \(O(N!)\)
如何去重?去重的全排列就是從第一個數字起每個數分別與它後面非重覆出現的數字交換。對於有重覆元素的數組,例如:[1, 2, 2]
,應該剔除重覆的情況。每次只需要檢查arr[start, i)
中是不是有和arr[i]
相同的元素,有的話,說明之前已經輸出過了,不需要考慮。
代碼實現:
const swap = (arr, i, j) => {
let tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
}
const permutation = arr => {
const _permutation = (arr, start) => {
if(start === arr.length){
console.log(arr)
return
}
for(let i = start; i < arr.length; ++i){
// 全排列:去重操作
if(arr.slice(start, i).indexOf(arr[i]) !== -1){
continue
}
swap(arr, i, start) // 和開始元素進行交換
_permutation(arr, start + 1)
swap(arr, i, start) // 恢複數組
}
return
}
return _permutation(arr, 0)
}
permutation([1, 2, 2])
console.log("**********")
permutation([1, 2, 3, 4])
4.2 背包問題
題目:我現在有一個背包,容量為m,然後有n個貨物,重量分別為w1,w2,w3...wn,每個貨物的價值是v1,v2,v3...vn,w和v沒有任何關係,請求背包能裝下的最大價值。
這個還在學習中,背包問題博大精深。。。
4.3 圖的連通分量
題目:我現在有一個canvas,上面隨機布著一些黑塊,請實現方法,計算canvas上有多少個黑塊。
這一題可以轉化成圖的聯通分量問題。通過getImageData
獲得像素數組,從頭到尾遍歷一遍,就可以判斷每個像素是否是黑色。同時,準備一個width * height
大小的二維數組,這個數組的每個元素是1/0
。如果是黑色,二維數組對應元素就置1;否則置0。
然後問題就被轉換成了圖的連通分量問題。可以通過深度優先遍歷或者並查集來實現。之前我用C++實現了,這裡不再冗贅:
5. Web工程化
5.1 Dialog組件思路
題目:現在要你完成一個Dialog組件,說說你設計的思路?它應該有什麼功能?
- 可以指定寬度、高度和位置
- 需要一個遮蓋層,遮住底層內容
- 由頭部、尾部和正文構成
- 需要監聽事件和自定義事件,非單向數據流:例如點擊組件右上角,修改父組件的
visible
屬性,關閉組件。
關於工程化組件封裝,可以去試試ElementUI。這個是ElementUI的Dialog組件:Element-Dialog
5.2 React的Diff演算法和虛擬DOM
題目: react 的虛擬dom是怎麼實現的
原答案寫的挺好滴,這裡直接貼了。
首先說說為什麼要使用Virturl DOM,因為操作真實DOM的耗費的性能代價太高,所以react內部使用js實現了一套dom結構。
在每次操作在和真實dom之前,使用實現好的diff演算法,對虛擬dom進行比較,遞歸找出有變化的dom節點,然後對其進行更新操作。
為了實現虛擬DOM,我們需要把每一種節點類型抽象成對象,每一種節點類型有自己的屬性,也就是prop,每次進行diff的時候,react會先比較該節點類型:
假如節點類型不一樣,那麼react會直接刪除該節點,然後直接創建新的節點插入到其中;
假如節點類型一樣,那麼會比較prop是否有更新,假如有prop不一樣,那麼react會判定該節點有更新,那麼重渲染該節點,然後在對其子節點進行比較,一層一層往下,直到沒有子節點。
參考鏈接:React源碼之Diff演算法
最後,歡迎來我的博客和我扯犢子:godbmw.com。直接戳本篇原文的地址:刷《一年半經驗,百度、有贊、阿裡面試總結》·手記