刷《一年半經驗,百度、有贊、阿里面試總結》·手記
在掘金上看到了一位大佬發了一篇很詳細的面試記錄文章-《一年半經驗,百度、有贊、阿里面試總結》,為了查漏補缺,抽空就詳細做了下。(估計只有我這麼無聊了哈哈哈)
有給出的或者有些不完善的答案,也盡力給出/完善了(可能有錯,大家自行辨別)。有些很困難的題目(例如實現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]。。。。你這個演算法的時間複雜度是多少
實現思路:從“開始元素”起,每個元素都和開始元素進行交換;不斷縮小範圍,最後輸出這種排列。暴力法的時間複雜度是 ,遞迴實現的時間複雜度是
**如何去重?去重的全排列就是從第一個數字起每個數分別與它後面非重複出現的數字交換。**對於有重複元素的陣列,例如:[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。直接戳本篇原文的地址:刷《一年半經驗,百度、有贊、阿里面試總結》·手記