百度某部門一面原題
大廠技術 高級前端 Node進階
點擊上方 程序員成長指北,關注公眾號
回復1,加入高級Node交流群
一、前言
二、原題
1. 如何用chatgpt提升前端開發(fā)效率
ChatGPT可以幫助回答與前端開發(fā)相關的問題。當你在編寫代碼的時候,當一時忘記了某個API怎么用,就可以向ChatGPT提問,并獲得解答和指導,甚至還會給出一些更加深入且性能更好的應用。這可以幫助更快地解決問題和理解前端開發(fā)中的概念。
ChatGPT可以幫助你生成常見的前端代碼片段和示例。你可以描述你想要實現的功能或解決的問題,然后向ChatGPT請求相關代碼片段。這樣,您可以更快地獲得一些基礎代碼,從而加快開發(fā)速度。
ChatGPT可以幫助你生成前端代碼的文檔。你可以描述一個函數、組件或類,并向ChatGPT請求生成相關的文檔注釋。這可以幫助您更輕松地為你的代碼添加文檔,提高代碼的可讀性和可維護性。
在開發(fā)過程中,您可能會遇到問題或錯誤。您可以向ChatGPT描述您遇到的問題,或者直接把代碼交給它,并請求幫助進行排查和調試。ChatGPT可以提供一些建議和指導,幫助您更快地找到問題的根本原因并解決它們。
ChatGPT可以為你提供關于前端開發(fā)的學習資源和最新信息。你可以向ChatGPT詢問關于前端開發(fā)的最佳實踐、最新的框架或庫、前端設計原則等方面的問題。這可以幫助我們不斷學習和更新自己的前端開發(fā)知識,從而提高效率。
2. [1, 2, 3, 4, 5, 6, 7, 8, 9] => [[1, 2, 3],[4, 5, 6],[7, 8, 9]],把一個一維數組變成三個三個的二維數組
在JavaScript中,可以使用數組的slice方法和一個循環(huán)來將一個一維數組轉換為一個二維數組。下面是一個示例代碼:
function convertTo2DArray(arr, chunkSize) {
var result = [];
for (var i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}
var inputArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var outputArray = convertTo2DArray(inputArray, 3);
console.log(outputArray);
slice 不會修改原數組,只會返回一個淺復制了原數組中的元素的一個新數組,不信的話自己可以編譯一下。
convertTo2DArray函數接受兩個參數:arr表示輸入的一維數組,chunkSize表示每個子數組的大小。它使用slice方法來從輸入數組中提取每個子數組,并使用循環(huán)來遍歷整個數組并構建輸出二維數組。最后,它返回生成的二維數組。
3. 輸出結果,為什么?
const obj3 = {a: 1};
const obj4 = {b: 2};
console.log(obj3 == obj4); // false
console.log(obj3 === obj4); // false
結果:
false,false
原因:
在這段代碼中,obj3和obj4分別是兩個獨立的對象,它們開辟的堆內存地址是完全不一樣。==運算符用于比較兩個操作數是否相等,而===運算符用于比較兩個操作數是否嚴格相等。
==運算符比較兩個對象時,它們將會進行類型轉換后再進行比較。由于obj3和obj4是不同的對象,即使它們的屬性值相同,它們的引用也不同,因此在進行類型轉換后,它們會被視為不相等的對象。因此,console.log(obj3 == obj4);的輸出結果將會是false。
===運算符比較兩個對象時,不會進行類型轉換,而是直接比較兩個操作數的值和類型是否完全相同。由于obj3和obj4是不同的對象,且類型也不同,即使它們的屬性值相同,它們也不會被視為嚴格相等的對象。因此,console.log(obj3 === obj4);的輸出結果同樣會是false。
==運算符還是===運算符,obj3和obj4都不會被視為相等或嚴格相等的對象,因為它們是不同的對象。
4. this有關 輸出結果,為什么?
const obj1 = {
fn: () => {
return this
}
}
const obj2 = {
fn: function(){
return this
}
}
console.log(obj1.fn());
console.log(obj2.fn());
輸出結果:
window || undefinedobj2
原因是:
在箭頭函數 fn 中的 this 關鍵字指向的是定義該函數的上下文,而不是調用該函數的對象。因此,當 obj1.fn() 被調用時,由于箭頭函數沒有它自己的this,當你調用fn()函數時,this指向會向上尋找,因此箭頭函數中的 this 指向的是全局對象(在瀏覽器環(huán)境下通常是 window 對象),因此返回的是 undefined。
fn 中的 this 關鍵字指向的是調用該函數的對象。在 obj2.fn() 中,函數 fn 是作為 obj2 的方法被調用的,所以其中的 this 指向的是 obj2 對象本身,因此返回的是 obj2。
objl.fn()的輸出結果是window(不是嚴格模式下)‖undefined(嚴格模式下)
obj2.fn()的結果是obj2(不管是不是)
5. Promise有關輸出結果,為什么?
console.log('1');
function promiseFn() {
return new Promise((resolve, reject) => {
setTimeout(()=> {
console.log('2');
})
resolve('3');
console.log('4')
})
}
promiseFn().then(res => {
console.log(res);
});
輸出結果:1 4 3 2
原因是:
首先,代碼從上往下執(zhí)行,把
console.log('1')放入同步任務再調用promiseFn(),因為
new Promise是同步任務,所以放入同步任務,繼續(xù)執(zhí)行遇到setTimout這個宏任務,放入宏任務隊列中
遇到resolve('3'),把res返回
之后再執(zhí)行.then(),因為promise.then是微任務,所以放入微任務隊列
代碼是先執(zhí)行同步任務,再執(zhí)行微任務,之后再是宏任務
所以輸出結果為1 4 3 2
6. 實現斐波那契的第N個值(從0開始),要求時間復雜度為O(n)
dp[i] = dp[i-1] + dp[i-2]
function fibonacci(n) {
if (n <= 1) return n;
let fib = [0, 1]; // 保存斐波那契數列的結果
for (let i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2]; // 計算第i個斐波那契數
}
return fib[n];
}
斐波那契數列(Fibonacci Sequence):
dp[i] = dp[i-1] + dp[i-2],其中dp[i]表示第i個斐波那契數。
爬樓梯問題(Climbing Stairs):
dp[i] = dp[i-1] + dp[i-2],其中dp[i]表示爬到第i級樓梯的方法數。
背包問題(Knapsack Problem):
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中dp[i][j]表示在前i個物品中選擇總重量不超過j的最大價值,weight[i]表示第i個物品的重量,value[i]表示第i個物品的價值。
最長遞增子序列(Longest Increasing Subsequence):
dp[i] = max(dp[j] + 1, dp[i]),其中dp[i]表示以第i個元素結尾的最長遞增子序列的長度,j為0到i-1的索引,且nums[i] > nums[j]。
最大子數組和(Maximum Subarray Sum):
dp[i] = max(nums[i], nums[i] + dp[i-1]),其中dp[i]表示以第i個元素結尾的最大子數組和。
最長公共子序列(Longest Common Subsequence):
如果
str1[i]等于str2[j],則dp[i][j] = dp[i-1][j-1] + 1;否則,
dp[i][j] = max(dp[i-1][j], dp[i][j-1]),其中dp[i][j]表示str1的前i個字符和str2的前j個字符的最長公共子序列的長度。
編輯距離(Edit Distance):
如果
word1[i]等于word2[j],則dp[i][j] = dp[i-1][j-1];否則,
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,其中dp[i][j]表示將word1的前i個字符轉換為word2的前j個字符所需的最少操作次數。
打家劫舍(House Robber):
dp[i] = max(dp[i-1], dp[i-2] + nums[i]),其中dp[i]表示前i個房屋能夠獲得的最大金額,nums[i]表示第i個房屋中的金額。
最大正方形(Maximal Square):
如果
matrix[i][j]等于 1,則dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1;否則,
dp[i][j] = 0,其中dp[i][j]表示以matrix[i][j]為右下角的最大正方形的邊長。
7. 手寫EventBus
EventBus 時,你可以創(chuàng)建一個全局的事件總線對象,并在該對象上定義事件的訂閱和發(fā)布方法。
class EventBus {
constructor() {
this.events = {}; // 存儲事件及其對應的回調函數列表
}
// 訂閱事件
subscribe(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,創(chuàng)建一個空的回調函數列表
this.events[eventName].push(callback); // 將回調函數添加到事件的回調函數列表中
}
// 發(fā)布事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 執(zhí)行回調函數,并傳遞數據作為參數
});
}
}
// 取消訂閱事件
unsubscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 過濾掉要取消的回調函數
}
}
}
EventBus 類,你可以執(zhí)行以下操作:
// 創(chuàng)建全局事件總線對象
const eventBus = new EventBus();
const callback1 = data => {
console.log('Callback 1:', data);
};
const callback2 = data => {
console.log('Callback 2:', data);
};
// 訂閱事件
eventBus.subscribe('event1', callback1);
eventBus.subscribe('event1', callback2);
// 發(fā)布事件
eventBus.publish('event1', 'Hello, world!');
// 輸出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// 取消訂閱事件
eventBus.unsubscribe('event1', callback1);
// 發(fā)布事件
eventBus.publish('event1', 'Goodbye!');
// 輸出:
// Callback 2: Goodbye!
subscribe、publish 和 unsubscribe 方法。subscribe 方法用于訂閱事件,publish 方法用于發(fā)布事件并觸發(fā)相關的回調函數,unsubscribe 方法用于取消訂閱事件。我們使用全局的 eventBus 對象來執(zhí)行訂閱和發(fā)布操作。
EventBus 實現允許你在不同的組件或模塊之間發(fā)布和訂閱事件,以實現跨組件的事件通信和數據傳遞。你可以根據需要對 EventBus 類進行擴展,添加更多的功能,如命名空間、一次訂閱多個事件等。
當問到EventBus時,得預防面試官問到EvnetEmitter,不過當我在網上查找相關的資料時,發(fā)現很多人似乎都搞混了這兩個概念,雖然我在這里的手寫原理似乎也差不多,但在實際使用中,兩者可能在細節(jié)上有所不同。因此,在具體場景中,你仍然需要根據需求和所選用的實現來查看相關文檔或源碼,以了解它們的具體實現和用法。
下面是一個簡單的 EventEmitter 類實現的基本示例:
class EventEmitter {
constructor() {
this.events = {}; // 用于存儲事件及其對應的回調函數列表
}
// 訂閱事件
on(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,創(chuàng)建一個空的回調函數列表
this.events[eventName].push(callback); // 將回調函數添加到事件的回調函數列表中
}
// 發(fā)布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 執(zhí)行回調函數,并傳遞數據作為參數
});
}
}
// 取消訂閱事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 過濾掉要取消的回調函數
}
}
// 添加一次性的事件監(jiān)聽器
once(eventName, callback) {
const onceCallback = data => {
callback(data); // 執(zhí)行回調函數
this.off(eventName, onceCallback); // 在執(zhí)行后取消訂閱該事件
};
this.on(eventName, onceCallback);
}
}
使用上述 EventEmitter 類,你可以執(zhí)行以下操作:
const emitter = new EventEmitter();
const callback1 = data => {
console.log('Callback 1:', data);
};
const callback2 = data => {
console.log('Callback 2:', data);
};
// 添加一次性事件監(jiān)聽器
const onceCallback = data => {
console.log('Once Callback:', data);
};
// 訂閱事件
emitter.on('event1', callback1);
emitter.on('event1', callback2);
emitter.once('event1', onceCallback);
// 發(fā)布事件
emitter.emit('event1', 'Hello, world!');
// 輸出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// Once Callback: Hello, world!
// 取消訂閱事件
emitter.off('event1', callback1);
// 發(fā)布事件
emitter.emit('event1', 'Goodbye!');
// 輸出:
// Callback 2: Goodbye!
在上述示例中,EventEmitter 類具有 on、emit 、 off和once 方法。on 方法用于訂閱事件,emit 方法用于發(fā)布事件并觸發(fā)相關的回調函數,off 方法用于取消訂閱事件,once方法用于添加一次性的事件監(jiān)聽器。你可以根據需求對 EventEmitter 類進行擴展,添加更多的功能,比如一次訂閱多個事件、取消所有事件訂閱等。
eventBus,eventEmitter的區(qū)別
EventBus 和 EventEmitter 都是用于實現事件發(fā)布-訂閱模式的工具,但它們在實現和使用上有一些區(qū)別。
實現方式:
EventBus:EventBus是一個全局的事件總線,通常是作為一個單例對象存在,用于在不同組件或模塊之間傳遞事件和數據。在 Vue.js 中,Vue 實例可以充當EventBus的角色。EventEmitter:EventEmitter是一個基于類的模塊,通常是作為一個實例對象存在,用于在單個組件或模塊內部實現事件的發(fā)布和訂閱。
2.使用范圍:
EventBus:EventBus的作用范圍更廣泛,可以跨越不同組件、模塊或文件進行事件的發(fā)布和訂閱。它可以實現多個組件之間的通信和數據傳遞。EventEmitter:EventEmitter主要用于單個組件或模塊內部,用于實現內部事件的處理和通信。
3.依賴關系:
EventBus:EventBus通常需要一個中央管理的實例,因此需要在應用程序的某個地方進行創(chuàng)建和管理。在 Vue.js 中,Vue 實例可以用作全局的EventBus。EventEmitter:EventEmitter可以在需要的地方創(chuàng)建實例對象,并將其用于內部事件的發(fā)布和訂閱。
4.命名空間:
EventBus:EventBus可以使用不同的事件名稱來進行事件的區(qū)分和分類,可以使用命名空間來標識不同類型的事件。EventEmitter:EventEmitter通常使用字符串作為事件的名稱,沒有直接支持命名空間的概念。
總結起來,EventBus 主要用于實現跨組件或模塊的事件通信和數據傳遞,適用于大型應用程序;而 EventEmitter 主要用于組件或模塊內部的事件處理和通信,適用于小型應用程序或組件級別的事件管理。選擇使用哪種工具取決于你的具體需求和應用場景。
8. (場景題)在瀏覽器中一天只能彈出一個彈窗,如何實現,說一下你的思路?
當頁面加載時,檢查本地存儲中是否已存在彈窗狀態(tài)的標記。
如果標記不存在或者標記表示上一次彈窗是在前一天,則顯示彈窗并更新本地存儲中的標記為當前日期。
如果標記存在且表示上一次彈窗是在當天,則不顯示彈窗。
// 檢查彈窗狀態(tài)的函數
function checkPopupStatus() {
// 獲取當前日期
const currentDate = new Date().toDateString();
// 從本地存儲中獲取彈窗狀態(tài)標記
const popupStatus = localStorage.getItem('popupStatus');
// 如果標記不存在或者標記表示上一次彈窗是在前一天
if (!popupStatus || popupStatus !== currentDate) {
// 顯示彈窗
displayPopup();
// 更新本地存儲中的標記為當前日期
localStorage.setItem('popupStatus', currentDate);
}
}
// 顯示彈窗的函數
function displayPopup() {
// 在這里編寫顯示彈窗的邏輯,可以是通過修改 DOM 元素顯示彈窗,或者調用自定義的彈窗組件等
console.log('彈出彈窗');
}
// 在頁面加載時調用檢查彈窗狀態(tài)的函數
checkPopupStatus();
checkPopupStatus 函數會在頁面加載時被調用。它首先獲取當前日期,并從本地存儲中獲取彈窗狀態(tài)的標記。如果標記不存在或者表示上一次彈窗是在前一天,就會調用 displayPopup 函數顯示彈窗,并更新本地存儲中的標記為當前日期。
通過這種方式,就可以確保在同一天只能彈出一個彈窗,而在后續(xù)的頁面加載中不會重復彈窗。
9. 項目中的性能優(yōu)化?
VueLazy的懶加載。
2、減少HTTP請求數量:由于頻繁的請求會對后端服務器造成極大的負擔,所以應該減少不必要的請求,比如在我的項目中的搜索界面,對于搜索按鈕增加了防抖功能
3、使用緩存:使用瀏覽器緩存可以減少資源請求,從而提高頁面加載速度。項目中我會把用戶的一些需要持久化的信息存入本地存儲。
4、異步請求使用Promise.all:異步請求可以在后臺加載資源,從而避免阻塞頁面加載。在請求數據時,我會使用Promise.all一次性并行的請求類似的數據,而不需要一個一個的請求,較少了請求時間。
5、圖片優(yōu)化:使用適當的圖片格式和大小可以減少頁面的資源請求和加載時間,項目中我會把圖片轉化成base64的格式和webp格式,這樣可以使圖片大小更小
6、使用CDN加速:使用CDN可以提高資源的訪問速度,從而加快頁面加載速度。我項目中的一些第三方資源有時需要請求,因此我會使用CDN內容分發(fā)網絡來提高訪問速度。
7、骨架屏(Skeleton Screen):它可以提升用戶感知的加載速度和用戶體驗。雖然骨架屏本身并不直接影響代碼性能,但它可以改善用戶對應用程序的感知,提供更好的用戶體驗。
10. 項目中遇到的難點,如何解決
1. 數據狀態(tài)管理
前端登錄狀態(tài)管理
-
我在一個練手的項目中做前端登錄功能的時候, 碰到了購物車需要登錄判斷的功能,比如用 isLogin來判斷有沒有登錄,當時由于沒有深入了解vuex,所以我一開始想著把這個isLogin通過組件與組件的傳值方法,把這個值傳給相應的組件,然后在需要登錄組件中進行判斷,但后來發(fā)現這個方法太麻煩了 后來通過學習了解,使用了
vuex這個全局狀態(tài)管理的方法, 通過使用createStore這個vuex中的API創(chuàng)建了一個全局的登錄狀態(tài),再通過actions mutations實現登錄判斷和登錄狀態(tài)共享
我項目中一開始首頁、詳情頁等其他頁面越來越多的狀態(tài)放在同一個
store上,雖然感覺有點亂,但實現了數據流和組件開發(fā)的分離,使得我更能夠專注于數據的管理但隨著數據的增多,感覺實在太亂了,然后得知
vuex中可以使用modules來進行分模塊,相應的頁面放入相應的模塊狀態(tài)中,之后再用actions,mutations,state,getters這四件套, 更好的模塊化管理數據,能夠知道哪些狀態(tài)是全局共享的(登錄), 哪些狀態(tài)是模塊共享的然后在新的項目中,也就是現在簡歷上的項目里,嘗試使用
pinia來管理,因為我發(fā)現它更簡單(沒有mutations),模塊化更好,讓我對組件狀態(tài)管理的更加得心應手,學習起來也更加的方便。
node的錯誤處理
-
一開始用 node寫后端的時候,一堆錯誤,比如路由沒配置,數據庫報錯。使得后面的代碼都無法運行,寫著寫著就感覺寫不下去,經常一個錯誤就需要反復的在腦海中想最后依靠那一絲的靈光一閃才解決 -
之后我就在 app.js這個后端入口文件的最后,添加一個統(tǒng)一的錯誤處理的中間件,向前端返回狀態(tài)碼和相應的信息后,直接使用next()向后繼續(xù)執(zhí)行,這樣雖然服務器報了錯,但仍然可以執(zhí)行后續(xù)的代碼。
跨域問題
在我寫完前端項目的時候,想要提升一下自己,就轉去學習了Koa,在搭建了大致的服務器,寫了一個簡單的接口并運行服務器后,我想當然的就在前端直接請求后端的端口,結果報了一個跨域的錯誤,由于當時初學后端,不怎么了解跨域,所以找了很多的解答并逐個在項目中進行嘗試,比如跨域中的
script,postMessage,html本身的Websocket但發(fā)現最實用的還是在服務器中配置
Access-Control-Allow-Origin來控制跨域請求的url地址,以及其他一些Access-Control-Allow頭來控制跨域請求方法等,然后跨域請求url的白名單我放入了.env這個全局環(huán)境變量中。
axios響應攔截
在后端返回數據的時候,我返回數據有一個狀態(tài)碼以及添加到
data這個需要返回的數據(代碼如下),這導致我在獲取接口里的數據時需要多.data(引用一層data),當時我沒意識到,結果一直獲取不到數據。之后輸出獲取的數據才發(fā)現在數據外面包了一層,雖然這個時候解決了服務器那邊數據返回的問題,但后面每次獲取數據時都需要在往里再獲取,非常的麻煩。最后在學習了并在項目中使用axios進行請求和響應后,就在響應的時候設置一個攔截器,對響應進行一番處理之后就可以直接拿到后端接口返回的值,而不會導致接口返回的值不會有太多的嵌套了。
11. 如何學習前端的,學了幾年?
我大致說說我回答的,僅作參考
我從大二開始就對前端很感興趣,當時正好學校也分了Web前端的方向,于是就跟著學校的課程開始學習基本的html,css,js三劍客,但之后感覺到老師教的很慢,就自己到B站上學習了,之后由于參加過一次藍橋杯,就看到了藍橋云課上有相關的基于html,css,js比較基礎項目,接著我還學習了一些行內大牛寫的一些博客文章,比如阮一峰,張鑫旭,廖雪峰等這些老師。之后又學習了vue并且在GitHub上學習相關的設計理念,根據GitHub上項目中不懂的東西又逐漸學習了各種UI組件庫和數據請求方式,最后又學習了Nodejs中的Koa,用Vue和Koa仿寫了一個全棧型項目,目前正在學習一些typescript的基本用法并嘗試著運用到項目中,并在學習Vue的一些底層源碼。
關于本文
作者:吃膩的奶油
https://juejin.cn/post/7240751116701728805
三、最后
這套題目,更加看重的是代碼底層的實現和算法基礎。
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一下
