2019年6月中旬,實在厭倦了以前平平淡淡的工做和絕不起眼的薪資,不顧親人的反對,毅然決然地決定隻身前往沿海城市,想着找到一份更加具備挑戰性的工做,來不折不扣地從新打磨本身,同時去追求更好的薪資待遇。固然在此以前,本身天天下班後都會利用業餘時間抓緊複習鞏固刷題等等,大概從3月份開始的吧,持續了3個多月。然後從6月中旬面試一直到6月底,中間大概兩個星期,其實個人學歷和背景並不突出,可是我我的感受多是由於本身簡歷作的稍微還行(後面我可能會單獨出一篇文章,來聊聊我作簡歷時的一點點心得),讓大廠的HR可以多看幾眼,中間面過的公司包括喜馬拉雅、攜程、嗶哩嗶哩、流利說、蜻蜓FM、愛回收等,陸陸續續拿到4,5個Offer吧,現在已經轉正,因此在這裏記錄下以前的部分面試題,和你們一塊兒分享交流。javascript
這家公司其實我也沒有太瞭解過,是我前同事推薦的,說裏面的薪資待遇不錯,而後我當時也有空閒時間,因此就去試試了,雖然公司名氣沒有上面提到的公司大,可是他的面試題我以爲仍是挺有份量的。
async function async1() { console.log(1); const result = await async2(); console.log(3); } async function async2() { console.log(2); } Promise.resolve().then(() => { console.log(4); }); setTimeout(() => { console.log(5); }); async1(); console.log(6);
個人回答是[1,2,6,4,3,5]。這道題目主要考對JS宏任務和微任務的理解程度,JS的事件循環中每一個宏任務稱爲一個Tick(標記),在每一個標記的末尾會追加一個微任務隊列,一個宏任務執行完後會執行全部的微任務,直到隊列清空。上題中我以爲稍微複雜點的在於async1函數,async1函數自己會返回一個Promise,同時await後面緊跟着async2函數返回的Promise,console.log(3)
實際上是在async2函數返回的Promise的then語句中執行的,then語句自己也會返回一個Promise而後追加到微任務隊列中,因此在微任務隊列中console.log(3)
在console.log(4)
後面,不太清楚的同窗能夠網上查下資料或者關注個人公衆號「前端之境」,咱們能夠一塊兒交流學習。css
幸運的是在面試前恰好查閱了下這部分的資料,因此回答過程當中還算駕輕就熟,主要是須要遵循Promise/A+規範:
(1) 一個promise必須具有三種狀態(pending|fulfilled(resolved)|rejected),當處於pending狀態時,能夠轉移到fulfilled(resolved)狀態或rejected狀態,處於fulfilled(resolved)狀態或rejected狀態時,狀態再也不可變;
(2) 一個promise必須有then方法,then方法必須接受兩個參數:前端
// onFulfilled在狀態由pending -> fulfilled(resolved) 時執行,參數爲resolve()中傳遞的值 // onRejected在狀態由pending -> rejected 時執行,參數爲reject()中傳遞的值 promise.then(onFulfilled,onRejected)
(3) then方法必須返回一個promise:vue
promise2 = promise1.then(onFulfilled, onRejected);
實現代碼直接貼出來吧:java
參考自: 實現一個完美符合Promise/A+規範的Promise
function myPromise(constructor){ let self=this; self.status="pending" //定義狀態改變前的初始狀態 self.value=undefined;//定義狀態爲resolved的時候的狀態 self.reason=undefined;//定義狀態爲rejected的時候的狀態 self.onFullfilledArray=[]; self.onRejectedArray=[]; function resolve(value){ if(self.status==="pending"){ self.value=value; self.status="resolved"; self.onFullfilledArray.forEach(function(f){ f(self.value); //若是狀態從pending變爲resolved, //那麼就遍歷執行裏面的異步方法 }); } } function reject(reason){ if(self.status==="pending"){ self.reason=reason; self.status="rejected"; self.onRejectedArray.forEach(function(f){ f(self.reason); //若是狀態從pending變爲rejected, //那麼就遍歷執行裏面的異步方法 }) } } //捕獲構造異常 try{ constructor(resolve,reject); }catch(e){ reject(e); } } myPromise.prototype.then=function(onFullfilled,onRejected){ let self=this; let promise2; switch(self.status){ case "pending": promise2 = new myPromise(function(resolve,reject){ self.onFullfilledArray.push(function(){ setTimeout(function(){ try{ let temple=onFullfilled(self.value); resolvePromise(temple) }catch(e){ reject(e) //error catch } }) }); self.onRejectedArray.push(function(){ setTimeout(function(){ try{ let temple=onRejected(self.reason); resolvePromise(temple) }catch(e){ reject(e)// error catch } }) }); }) case "resolved": promise2=new myPromise(function(resolve,reject){ setTimeout(function(){ try{ let temple=onFullfilled(self.value); //將上次一then裏面的方法傳遞進下一個Promise狀態 resolvePromise(temple); }catch(e){ reject(e);//error catch } }) }) break; case "rejected": promise2=new myPromise(function(resolve,reject){ setTimeout(function(){ try{ let temple=onRejected(self.reason); //將then裏面的方法傳遞到下一個Promise的狀態裏 resolvePromise(temple); }catch(e){ reject(e); } }) }) break; default: } return promise2; } function resolvePromise(promise,x,resolve,reject){ if(promise===x){ throw new TypeError("type error") } let isUsed; if(x!==null&&(typeof x==="object"||typeof x==="function")){ try{ let then=x.then; if(typeof then==="function"){ //是一個promise的狀況 then.call(x,function(y){ if(isUsed)return; isUsed=true; resolvePromise(promise,y,resolve,reject); },function(e){ if(isUsed)return; isUsed=true; reject(e); }) }else{ //僅僅是一個函數或者是對象 resolve(x) } }catch(e){ if(isUsed)return; isUsed=true; reject(e); } }else{ //返回的基本類型,直接resolve resolve(x) } }
let a = {a: 10}; let b = {b: 10}; let obj = { a: 10 }; obj[b] = 20; console.log(obj[a]);
個人回答是:20。這道題目主要考對JS數據類型的熟練度以及對ES6中屬性名錶達式的理解。在上題中obj[b] = 20
的賦值操做後,obj
其實已經變成了{a: 10, [object Object]: 20}
,這是由於若是屬性名錶達式是一個對象的話,那麼默認狀況下會自動將對象轉爲字符串[object Object]
,最後一步獲取obj[a]
時,a自己也是一個對象,因此會被轉換爲獲取obj['[object Object]']
也就是上一步賦值的20。webpack
這個其實網上已經有大把大把的實現方案了,我也就大概給出瞭如下幾種:git
let originalArray = [1,2,3,4,5,3,2,4,1]; // 方式1 const result = Array.from(new Set(originalArray)); console.log(result); // -> [1, 2, 3, 4, 5] // 方式2 const result = []; const map = new Map(); for (let v of originalArray) { if (!map.has(v)) { map.set(v, true); result.push(v); } } console.log(result); // -> [1, 2, 3, 4, 5] // 方式3 const result = []; for (let v of originalArray) { if (!result.includes(v)) { result.push(v); } } console.log(result); // -> [1, 2, 3, 4, 5] // 方式4 for (let i = 0; i < originalArray.length; i++) { for (let j = i + 1; j < originalArray.length; j++) { if (originalArray[i] === originalArray[j]) { originalArray.splice(j, 1); j--; } } } console.log(originalArray); // -> [1, 2, 3, 4, 5] // 方式5 const obj = {}; const result = originalArray.filter(item => obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)); console.log(result); // -> [1, 2, 3, 4, 5]
這個題目不僅一家公司問到了,開始的時候一臉懵逼,內心想着每一個對象的內存地址自己就不同,去重的意義何在,非要去重的話,那隻能經過JSON.stringify
序列化成字符串(這個方法有必定的缺陷)後進行對比,或者遞歸的方式進行鍵-值對比,可是對於大型嵌套對象來講仍是比較耗時的,因此仍是沒有答好,後來面試官跟我說是根據每一個對象的某一個具體屬性來進行去重,由於考慮到服務端返回的數據中可能存在id重複的狀況,須要前端進行過濾,以下:github
const responseList = [ { id: 1, a: 1 }, { id: 2, a: 2 }, { id: 3, a: 3 }, { id: 1, a: 4 }, ]; const result = responseList.reduce((acc, cur) => { const ids = acc.map(item => item.id); return ids.includes(cur.id) ? acc : [...acc, cur]; }, []); console.log(result); // -> [ { id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3} ]
當時是前一天進行了一次電面,而後次日現場面,兩個面試官輪流問,大概持續了一個半小時吧,問的問題仍是比較多的,有些問題時間久了仍是不太記得了,多多見諒!
淺拷貝是指建立一個對象,這個對象有着原始對象屬性值的一份精確拷貝。若是屬性是基本類型,那麼拷貝的就是基本類型的值,若是屬性是引用類型,那麼拷貝的就是內存地址,因此若是其中一個對象修改了某些屬性,那麼另外一個對象就會受到影響。
深拷貝是指從內存中完整地拷貝一個對象出來,並在堆內存中爲其分配一個新的內存區域來存放,而且修改該對象的屬性不會影響到原來的對象。web
淺拷貝:(1) Object.assign的方式 (2) 經過對象擴展運算符 (3) 經過數組的slice方法 (4) 經過數組的concat方法。
深拷貝:(1) 經過JSON.stringify來序列化對象 (2) 手動實現遞歸的方式。面試
先簡單說了下實現輪播的思路,多張圖片從左至右依次排列,點擊左右側按鈕切換圖片的時候,讓圖片的父級容器的left偏移值增長或減小單張圖片的寬度大小,同時配合CSS3 transition過渡或者手寫一個動畫函數,這樣能夠實現一個比較平滑的動畫效果。對於無縫輪播,我當時的思路是再拷貝一個圖片的父級容器出來,例如原來一個<ul><li></li><li></li></ul>
對應兩張圖片,如今變爲兩個ul
對應4張圖片,同時ul
的父容器監聽自身的scrollLeft
,若是值已經大於等於一個ul
的寬度,則當即將自身的scrollLeft
值重置爲0,這樣就又能夠從起點開始輪播,實現無縫的效果。
var a = 10; var obj = { a: 20, say: function () { console.log(this.a); } }; obj.say();
這個是被我簡化後的版本,具體題目記不太清了,反正就是考的this的指向問題,上題中答案爲20。而後面試官繼續追問,如何才能打印出10,給出以下方式:
// 方式1 var a = 10; var obj = { a: 20, say: () => { // 此處改成箭頭函數 console.log(this.a); } }; obj.say(); // -> 10 // 方式2 var a = 10; var obj = { a: 20, say: function () { console.log(this.a); } }; obj.say.call(this); // 此處顯示綁定this爲全局window對象 // 方式3 var a = 10; var obj = { a: 20, say: function () { console.log(this.a); } }; var say = obj.say; // 此處先建立一個臨時變量存放函數定義,而後單獨調用 say();
建立:beforeCreate,created;
載入:beforeMount,mounted;
更新:beforeUpdate,updated;
銷燬:beforeDestroy,destroyed;
當時的思路是頭部(Header)通常分爲左、中、右三個部分,分爲三個區域來設計,中間爲主標題,每一個頁面的標題確定不一樣,因此能夠經過vue props的方式作成可配置對外進行暴露,左側大部分頁面可能都是回退按鈕,可是樣式和內容不盡相同,右側通常都是具備功能性的操做按鈕,因此左右兩側能夠經過vue slot插槽的方式對外暴露以實現多樣化,同時也能夠提供default slot默認插槽來統一頁面風格。
這個是flex佈局的內容,其實就是一個邊距的區別,按水平佈局來講,space-between
在左右兩側沒有邊距,而space-around
在左右兩側會留下邊距,垂直佈局同理,以下圖所示:
這個其實方案仍是比較多的,能夠從DOM層面,CSS樣式層面和JS邏輯層面分別入手,大概給出如下幾種:
(1) 減小DOM的訪問次數,能夠將DOM緩存到變量中;
(2) 減小重繪和迴流,任何會致使重繪和迴流的操做都應減小執行,可將屢次操做合併爲一次;
(3) 儘可能採用事件委託的方式進行事件綁定,避免大量綁定致使內存佔用過多;
(4) css層級儘可能扁平化,避免過多的層級嵌套,儘可能使用特定的選擇器來區分;
(5) 動畫儘可能使用CSS3動畫屬性來實現,開啓GPU硬件加速;
(6) 圖片在加載前提早指定寬高或者脫離文檔流,可避免加載後的從新計算致使的頁面迴流;
(7) css文件在<head>
標籤中引入,js文件在<body>
標籤中引入,優化關鍵渲染路徑;
(8) 加速或者減小HTTP請求,使用CDN加載靜態資源,合理使用瀏覽器強緩存和協商緩存,小圖片可使用Base64來代替,合理使用瀏覽器的預取指令prefetch和預加載指令preload;
(9) 壓縮混淆代碼,刪除無用代碼,代碼拆分來減小文件體積;
(10) 小圖片使用雪碧圖,圖片選擇合適的質量、尺寸和格式,避免流量浪費。
衝突主要是出如今多人在修改同一個文件的同一部份內容時,對方當你以前push
,而後你後push
的時候git檢測到兩次提交內容不匹配,提示你Conflict
,而後你pull
下來的代碼會在衝突的地方使用=====
隔開,此時你須要找到對應的開發人員商量代碼的取捨,切不可隨意修改並強制提交,解決衝突後再次push
便可。
當時是兩輪技術面,一次電面,一次現場面,電面有部分題目仍是答得很模糊,現場面自我感受還能夠吧。
代碼以下:
Function.prototype.bind = function(context, ...args1) { if (typeof this !== 'function') { throw new Error('not a function'); } let fn = this; let resFn = function(...args2) { return fn.apply(this instanceof resFn ? this : context, args1.concat(args2)); }; const DumpFunction = function DumpFunction() {}; DumpFunction.prototype = this.prototype; resFn.prototype = new DumpFunction(); return resFn; }
在React中咱們通常有兩種方式來建立組件,類定義或者函數定義;在類定義中咱們可使用許多React的特性,好比state或者各類生命週期鉤子,可是在函數定義中卻沒法使用。因此在React 16.8版本中新推出了React Hooks的功能,經過React Hooks咱們就能夠在函數定義中來使用類定義當中才能使用的特性。固然React Hooks的出現自己也是爲了組件複用,以及相比於類定義當中的生命週期鉤子,React Hooks中提供的useEffect
將多個生命週期鉤子進行結合,使得原先在類定義中分散的邏輯變得更加集中,方便維護和管理。
useEffect
能夠當作是componentDidMount
,componentDidUpdate
和componentWillUnmount
三者的結合。useEffect(callback, [source])
接收兩個參數,調用方式以下:
useEffect(() => { console.log('mounted'); return () => { console.log('willUnmount'); } }, [source]);
生命週期函數的調用主要是經過第二個參數[source]
來進行控制,有以下幾種狀況:
(1) [source]
參數不傳時,則每次都會優先調用上次保存的函數中返回的那個函數,而後再調用外部那個函數;
(2) [source]
參數傳[]
時,則外部的函數只會在初始化時調用一次,返回的那個函數也只會最終在組件卸載時調用一次;
(3) [source]
參數有值時,則只會監聽到數組中的值發生變化後才優先調用返回的那個函數,再調用外部的函數。
高階組件(Higher Order Componennt)自己其實不是組件,而是一個函數,這個函數接收一個元組件做爲參數,而後返回一個新的加強組件,高階組件的出現自己也是爲了邏輯複用,舉個例子:
function withLoginAuth(WrappedComponent) { return class extends React.Component { constructor(props) { super(props); this.state = { isLogin: false }; } async componentDidMount() { const isLogin = await getLoginStatus(); this.setState({ isLogin }); } render() { if (this.state.isLogin) { return <WrappedComponent {...this.props} />; } return (<div>您還未登陸...</div>); } } }
parseInt('2017-07-01') // -> 2017 parseInt('2017abcdef') // -> 2017 parseInt('abcdef2017') // -> NaN
(1) 增長shouldComponentUpdate
鉤子對新舊props
進行比較,若是值相同則阻止更新,避免沒必要要的渲染,或者使用PureReactComponent
替代Component
,其內部已經封裝了shouldComponentUpdate
的淺比較邏輯;
(2) 對於列表或其餘結構相同的節點,爲其中的每一項增長惟一key
屬性,以方便React的diff
算法中對該節點的複用,減小節點的建立和刪除操做;
(3) render
函數中減小相似onClick={() => {doSomething()}}
的寫法,每次調用render
函數時均會建立一個新的函數,即便內容沒有發生任何變化,也會致使節點不必的重渲染,建議將函數保存在組件的成員對象中,這樣只會建立一次;
(4) 組件的props
若是須要通過一系列運算後才能拿到最終結果,則能夠考慮使用reselect
庫對結果進行緩存,若是props
值未發生變化,則結果直接從緩存中拿,避免高昂的運算代價;
(5) webpack-bundle-analyzer
分析當前頁面的依賴包,是否存在不合理性,若是存在,找到優化點並進行優化。
這題沒答好,兩個字形容:稀爛!一碰到算法題就容易緊張蒙圈,來個正解吧。
建立一個最小堆結構,初始值爲10000個數的前10個,堆頂爲10個數裏的最小數。而後遍歷剩下的9990個數,若是數字小於堆頂的數,則把堆頂的數刪除,將遍歷的數插入堆中。堆結構會自動進行調整,因此能夠保證堆頂的數必定是10個數裏最小的。遍歷完畢後,堆裏的10個數就是這10000個數裏面最大的10個。
當時是提早有一次電面,而後過了幾天纔去現場面,現場兩輪技術面,公司很注重底層原理,因此答得不是很好。
代碼以下:
// 防抖函數 function debounce(fn, wait, immediate) { let timer = null; return function (...args) { let context = this; if (immediate && !timer) { fn.apply(context, args); } if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, wait); } } class SearchInput extends React.Component { constructor(props) { super(props); this.state = { value: '' }; this.handleChange = this.handleChange.bind(this); this.callAjax = debounce(this.callAjax, 500, true); } handleChange(e) { this.setState({ value: e.target.value }); this.callAjax(); } callAjax() { // 此處根據輸入值調用服務端接口 console.log(this.state.value); } render() { return (<input type="text" value={this.state.value} onChange={this.handleChange} />); } }
代碼以下:
function request(url, body, successCallback, errorCallback, maxCount = 3) { return fetch(url, body) .then(response => successCallback(response) .catch(err => { if (maxCount <= 0) return errorCallback('請求超時'); return request(url, body, successCallback, errorCallback, --maxCount); }); } // 調用 request('https://some/path', { method: 'GET', headers: {} }, (response) => { console.log(response.json()); }, (err) => console.error(err));
==
表示抽象相等,兩邊值類型不一樣的時候,會先作隱式類型轉換,再對值進行比較; ===
表示嚴格相等,不會作類型轉換,兩邊的類型不一樣必定不相等。
(1) GET請求在瀏覽器回退和刷新時是無害的,而POST請求會告知用戶數據會被從新提交;
(2) GET請求能夠收藏爲書籤,POST請求不能夠收藏爲書籤;
(3) GET請求能夠被緩存,POST請求不能夠被緩存,除非在響應頭中包含合適的Cache-Control/Expires字段,可是不建議緩存POST請求,其不知足冪等性,每次調用都會對服務器資源形成影響;
(4) GET請求通常不具備請求體,所以只能進行url編碼,而POST請求支持多種編碼方式。
(5) GET請求的參數能夠被保留在瀏覽器的歷史中,POST請求不會被保留;
(6) GET請求由於是向URL添加數據,不一樣的瀏覽器廠商,代理服務器,web服務器均可能會有本身的長度限制,而POST請求無長度限制;
(7) GET請求只容許ASCII字符,POST請求無限制,支持二進制數據;
(8) GET請求的安全性較差,數據被暴露在瀏覽器的URL中,因此不能用來傳遞敏感信息,POST請求的安全性較好,數據不會暴露在URL中;
(9) GET請求具備冪等性(屢次請求不會對資源形成影響),POST請求不冪等;
(10) GET請求通常不具備請求體,請求中通常不包含100-continue 協議,因此只會發一次請求,而POST請求在發送數據到服務端以前容許雙方"握手",客戶端先發送Expect:100-continue消息,詢問服務端是否願意接收數據,接收到服務端正確的100-continue應答後纔會將請求體發送給服務端,服務端再響應200返回數據。
瀏覽器的緩存機制可分爲強緩存和協商緩存,服務端能夠在響應頭中增長Cache-Control/Expires來爲當前資源設置緩存有效期(Cache-Control的max-age的優先級高於Expires),瀏覽器再次發送請求時,會先判斷緩存是否過時,若是未過時則命中強緩存,直接使用瀏覽器的本地緩存資源,若是已過時則使用協商緩存,協商緩存大體有如下兩種方案:
(1) 惟一標識:Etag(服務端響應攜帶) & If-None-Match(客戶端請求攜帶);
(2) 最後修改時間: Last-Modified(服務端響應攜帶) & If-Modified-Since (客戶端請求攜帶) ,其優先級低於Etag。
服務端判斷值是否一致,若是一致,則直接返回304通知瀏覽器使用本地緩存,若是不一致則返回新的資源。
現場兩輪技術面,問了不少考驗基礎知識的題目,總體來講回答的還算比較滿意吧。
transition
過渡動畫:
(1) transition-property
:屬性名稱
(2) transition-duration
: 間隔時間
(3) transition-timing-function
: 動畫曲線
(4) transition-delay
: 延遲
animation
關鍵幀動畫:
(1) animation-name
:動畫名稱
(2) animation-duration
: 間隔時間
(3) animation-timing-function
: 動畫曲線
(4) animation-delay
: 延遲
(5) animation-iteration-count
:動畫次數
(6) animation-direction
: 方向
(7) animation-fill-mode
: 禁止模式
指的是頁面在渲染時,DOM元素所採用的佈局模型,一個元素佔用的空間大小由幾個部分組成,內容(content)、內邊距(padding),邊框(border)和外邊距(margin)。能夠經過box-sizing
來進行設置,其中IE盒模型的content包含了padding和border,這是區別於W3C標準盒模型的地方。
!important > 行內樣式 > id選擇器 > class選擇器 > 標籤選擇器 > * > 繼承 > 默認
forEach
遍歷數組,參數爲一個回調函數,回調函數接收三個參數,當前元素,元素索引,整個數組; map
與forEach
相似,遍歷數組,但其回調函數的返回值會組成一個新數組,新數組的索引結構和原數組一致,原數組不變; filter
會返回原數組的一個子集,回調函數用於邏輯判斷,返回true
則將當前元素添加到返回數組中,不然排除當前元素,原數組不變。
代碼以下:
const curry = (fn, ...args1) => (...args2) => ( arg => arg.length === fn.length ? fn(...arg) : curry(fn, ...arg) )([...args1, ...args2]); // 調用 const foo = (a, b, c) => a * b * c; curry(foo)(2, 3, 4); // -> 24 curry(foo, 2)(3, 4); // -> 24 curry(foo, 2, 3)(4); // -> 24 curry(foo, 2, 3, 4)(); // -> 24
(1) BroadCast Channel
(2) Service Worker
(3) LocalStorage + window.onstorage監聽
(4) Shared Worker + 定時器輪詢(setInterval)
(5) IndexedDB + 定時器輪詢(setInterval)
(6) cookie + 定時器輪詢(setInterval)
(7) window.open + window.postMessage
(8) Websocket
代碼以下:
function getType(obj) { if (obj === null) return String(obj); return typeof obj === 'object' ? Object.prototype.toString.call(obj).replace('[object ', '').replace(']', '').toLowerCase() : typeof obj; } // 調用 getType(null); // -> null getType(undefined); // -> undefined getType({}); // -> object getType([]); // -> array getType(123); // -> number getType(true); // -> boolean getType('123'); // -> string getType(/123/); // -> regexp getType(new Date()); // -> date
有些面試題實在是想不起來了,上面的題目其實大部分仍是比較基礎的,問到的頻率也比較高,這裏只是作一個簡單的分享,但願對你們多多少少有點幫助,也但願能和你們一塊兒交流學習,若是有疑惑歡迎留言討論。
今天先分享到這裏,筆者剛新開公衆號,若是你們有興趣和筆者一塊兒學習,相互討論技術,能夠關注我們的公衆號,一塊兒見證公衆號的成長。
文章已同步更新至Github博客,若覺文章尚可,歡迎前往star!
你的一個點贊,值得讓我付出更多的努力!
逆境中成長,只有不斷地學習,才能成爲更好的本身,與君共勉!