作者:吃腻的奶油 https://juejin.cn/post/7240751116701728805
这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。
因为我嘴贱,平时习惯了使用chatgpt,然后自我介绍说了一句,由于之前面得公司都没问过,导致我没怎么往这方面准备,以至于答得时候牛头不对马嘴,所以说不愧是大厂啊。
ChatGPT
可以帮助回答与前端开发相关的问题。当你在编写代码的时候,当一时忘记了某个API怎么用,就可以向ChatGPT
提问,并获得解答和指导,甚至还会给出一些更加深入且性能更好的应用。这可以帮助更快地解决问题和理解前端开发中的概念。ChatGPT
可以帮助你生成常见的前端代码片段和示例。你可以描述你想要实现的功能或解决的问题,然后向ChatGPT
请求相关代码片段。这样,您可以更快地获得一些基础代码,从而加快开发速度。ChatGPT
可以帮助你生成前端代码的文档。你可以描述一个函数、组件或类,并向ChatGPT
请求生成相关的文档注释。这可以帮助您更轻松地为你的代码添加文档,提高代码的可读性和可维护性。ChatGPT
描述您遇到的问题,或者直接把代码交给它,并请求帮助进行排查和调试。ChatGPT
可以提供一些建议和指导,帮助您更快地找到问题的根本原因并解决它们。ChatGPT
可以为你提供关于前端开发的学习资源和最新信息。你可以向ChatGPT
询问关于前端开发的最佳实践、最新的框架或库、前端设计原则等方面的问题。这可以帮助我们不断学习和更新自己的前端开发知识,从而提高效率。在JavaScript中,可以使用数组的slice
方法和一个循环来将一个一维数组转换为一个二维数组。下面是一个示例代码:
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);
输出结果将是:
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
slice
不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组,不信的话自己可以编译一下。
这段代码中的convertTo2DArray
函数接受两个参数:arr
表示输入的一维数组,chunkSize
表示每个子数组的大小。它使用slice
方法来从输入数组中提取每个子数组,并使用循环来遍历整个数组并构建输出二维数组。最后,它返回生成的二维数组。
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
都不会被视为相等或严格相等的对象,因为它们是不同的对象。
const obj1 = {
fn: () => {
return this
}
}
const obj2 = {
fn: function(){
return this
}
}
console.log(obj1.fn());
console.log(obj2.fn());
输出结果:
window || undefined
obj2
原因是:
在箭头函数 fn
中的 this
关键字指向的是定义该函数的上下文,而不是调用该函数的对象。因此,当 obj1.fn()
被调用时,由于箭头函数没有它自己的this,当你调用fn()函数时,this指向会向上寻找,因此箭头函数中的 this
指向的是全局对象(在浏览器环境下通常是 window
对象),因此返回的是 undefined
。
而在普通函数 fn
中的 this
关键字指向的是调用该函数的对象。在 obj2.fn()
中,函数 fn
是作为 obj2
的方法被调用的,所以其中的 this
指向的是 obj2
对象本身,因此返回的是 obj2
。
需要注意的是,在严格模式下,普通函数中的 this
也会变为 undefined
,因此即使是 obj2.fn()
也会返回 undefined
。但在示例中没有明确指定使用严格模式,所以默认情况下运行在非严格模式下。
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
原因是:
console.log('1')
放入同步任务new Promise
是同步任务,所以放入同步任务,继续执行首先,说到斐波那契第一个想到的肯定是如下的算法,但这可是百度啊,如果只是这种程度的话如何能和同样面相同岗位的人竞争呢,所以我们得想到如下算法有什么缺点,然后如何优化
function fib(n) {
if (n == 0 || n === 1) return 1;
return fib(n - 1) + fib(n - 2);
};
console.log(fib(3)); // 5
console.log(fib(5)); // 8
单纯的使用递归看似没什么问题,也能运算出结果,但是里面有个致命的问题,首先,时间复杂度就不对,递归思想的复杂度为 O(2^n) ,它不为O(n),然后还有会重复计算,比如计算n=3时,会计算fib(1) + fib(2)
,再次计算fib(4)时,会先算fib(3) = fib(1) + fib(2)
,然后再计算fib(4) = fib(1) + fib(2) + fib(3)
,在这里,fib(1)和fib(2)重复计算了两次,对于性能损耗极大。此时的你如果对动态规划敏感的话,就会从中想到动态规划其中最关键的特征——重叠子问题
因此,使用动态规划来规避重复计算问题,算是比较容易想到较优的一种解法,并且向面试官展现了你算法能力中有动态规划的思想,对于在面试中的你加分是极大的。
以下是动态规划思路的算法,状态转移方程为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];
}
当然,你可能会说,在面试中怎么可能一下子就能想到动态规划,所以在面试前你需要背一背相关的状态转移方程,当你对算法问题分析到一定程度时,就能够记忆起这些状态转移方程,提高你写算法的速度。
在面试中,动态规划的常用状态转移方程可以根据问题的具体情况有所不同。以下是几个常见的动态规划问题和它们对应的状态转移方程示例:
dp[i] = dp[i-1] + dp[i-2]
,其中 dp[i]
表示第 i
个斐波那契数。dp[i] = dp[i-1] + dp[i-2]
,其中 dp[i]
表示爬到第 i
级楼梯的方法数。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
个物品的价值。dp[i] = max(dp[j] + 1, dp[i])
,其中 dp[i]
表示以第 i
个元素结尾的最长递增子序列的长度,j
为 0
到 i-1
的索引,且 nums[i] > nums[j]
。dp[i] = max(nums[i], nums[i] + dp[i-1])
,其中 dp[i]
表示以第 i
个元素结尾的最大子数组和。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
个字符的最长公共子序列的长度。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
个字符所需的最少操作次数。dp[i] = max(dp[i-1], dp[i-2] + nums[i])
,其中 dp[i]
表示前 i
个房屋能够获得的最大金额,nums[i]
表示第 i
个房屋中的金额。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]
为右下角的最大正方形的边长。当需要手动实现一个简单的 EventBus
时,你可以创建一个全局的事件总线对象,并在该对象上定义事件的订阅和发布方法。
class EventBus {
constructor() {
this.events = {}; // 存储事件及其对应的回调函数列表
}
// 订阅事件
subscribe(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}
// 发布事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}
// 取消订阅事件
unsubscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}
}
使用上述 EventBus
类,你可以执行以下操作:
// 创建全局事件总线对象
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);
// 发布事件
eventBus.publish('event1', 'Hello, world!');
// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// 取消订阅事件
eventBus.unsubscribe('event1', callback1);
// 发布事件
eventBus.publish('event1', 'Goodbye!');
// 输出:
// Callback 2: Goodbye!
在上述示例中,我们创建了一个 EventBus 类,该类具有 subscribe
、publish
和 unsubscribe
方法。subscribe
方法用于订阅事件,publish
方法用于发布事件并触发相关的回调函数,unsubscribe
方法用于取消订阅事件。我们使用全局的 eventBus
对象来执行订阅和发布操作。
这个简单的 EventBus
实现允许你在不同的组件或模块之间发布和订阅事件,以实现跨组件的事件通信和数据传递。你可以根据需要对 EventBus
类进行扩展,添加更多的功能,如命名空间、一次订阅多个事件等。
当问到EventBus时,得预防面试官问到EvnetEmitter,不过当我在网上查找相关的资料时,发现很多人似乎都搞混了这两个概念,虽然我在这里的手写原理似乎也差不多,但在实际使用中,两者可能在细节上有所不同。因此,在具体场景中,你仍然需要根据需求和所选用的实现来查看相关文档或源码,以了解它们的具体实现和用法。
下面是一个简单的 EventEmitter 类实现的基本示例:
class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件及其对应的回调函数列表
}
// 订阅事件
on(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}
// 发布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}
// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}
// 添加一次性的事件监听器
once(eventName, callback) {
const onceCallback = data => {
callback(data); // 执行回调函数
this.off(eventName, onceCallback); // 在执行后取消订阅该事件
};
this.on(eventName, onceCallback);
}
}
使用上述 EventEmitter 类,你可以执行以下操作:
const emitter = new EventEmitter();
const callback1 = data => {
console.log('Callback 1:', data);
};
const callback2 = data => {
console.log('Callback 2:', data);
};
// 添加一次性事件监听器
const onceCallback = data => {
console.log('Once Callback:', data);
};
// 订阅事件
emitter.on('event1', callback1);
emitter.on('event1', callback2);
emitter.once('event1', onceCallback);
// 发布事件
emitter.emit('event1', 'Hello, world!');
// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// Once Callback: Hello, world!
// 取消订阅事件
emitter.off('event1', callback1);
// 发布事件
emitter.emit('event1', 'Goodbye!');
// 输出:
// Callback 2: Goodbye!
在上述示例中,EventEmitter 类具有 on
、emit
、 off
和once
方法。on
方法用于订阅事件,emit
方法用于发布事件并触发相关的回调函数,off
方法用于取消订阅事件,once
方法用于添加一次性的事件监听器。你可以根据需求对 EventEmitter
类进行扩展,添加更多的功能,比如一次订阅多个事件、取消所有事件订阅等。
EventBus
和 EventEmitter
都是用于实现事件发布-订阅模式的工具,但它们在实现和使用上有一些区别。
EventBus
:EventBus
是一个全局的事件总线,通常是作为一个单例对象存在,用于在不同组件或模块之间传递事件和数据。在 Vue.js 中,Vue 实例可以充当 EventBus
的角色。EventEmitter
:EventEmitter
是一个基于类的模块,通常是作为一个实例对象存在,用于在单个组件或模块内部实现事件的发布和订阅。EventBus
:EventBus
的作用范围更广泛,可以跨越不同组件、模块或文件进行事件的发布和订阅。它可以实现多个组件之间的通信和数据传递。EventEmitter
:EventEmitter
主要用于单个组件或模块内部,用于实现内部事件的处理和通信。EventBus
:EventBus
通常需要一个中央管理的实例,因此需要在应用程序的某个地方进行创建和管理。在 Vue.js 中,Vue 实例可以用作全局的 EventBus
。EventEmitter
:EventEmitter
可以在需要的地方创建实例对象,并将其用于内部事件的发布和订阅。EventBus
:EventBus
可以使用不同的事件名称来进行事件的区分和分类,可以使用命名空间来标识不同类型的事件。EventEmitter
:EventEmitter
通常使用字符串作为事件的名称,没有直接支持命名空间的概念。总结起来,EventBus 主要用于实现跨组件或模块的事件通信和数据传递,适用于大型应用程序;而 EventEmitter 主要用于组件或模块内部的事件处理和通信,适用于小型应用程序或组件级别的事件管理。选择使用哪种工具取决于你的具体需求和应用场景。
要在浏览器中实现一天只能弹出一个弹窗的功能,可以使用本地存储(localStorage)来记录弹窗状态。下面是一种实现方案:
以下是示例代码:
// 检查弹窗状态的函数
function checkPopupStatus() {
// 获取当前日期
const currentDate = new Date().toDateString();
// 从本地存储中获取弹窗状态标记
const popupStatus = localStorage.getItem('popupStatus');
// 如果标记不存在或者标记表示上一次弹窗是在前一天
if (!popupStatus || popupStatus !== currentDate) {
// 显示弹窗
displayPopup();
// 更新本地存储中的标记为当前日期
localStorage.setItem('popupStatus', currentDate);
}
}
// 显示弹窗的函数
function displayPopup() {
// 在这里编写显示弹窗的逻辑,可以是通过修改 DOM 元素显示弹窗,或者调用自定义的弹窗组件等
console.log('弹出弹窗');
}
// 在页面加载时调用检查弹窗状态的函数
checkPopupStatus();
在这个实现中,checkPopupStatus
函数会在页面加载时被调用。它首先获取当前日期,并从本地存储中获取弹窗状态的标记。如果标记不存在或者表示上一次弹窗是在前一天,就会调用 displayPopup
函数显示弹窗,并更新本地存储中的标记为当前日期。
通过这种方式,就可以确保在同一天只能弹出一个弹窗,而在后续的页面加载中不会重复弹窗。
VueLazy
的懒加载。Promise.all
一次性并行的请求类似的数据,而不需要一个一个的请求,较少了请求时间。base64
的格式和webp
格式,这样可以使图片大小更小CDN
可以提高资源的访问速度,从而加快页面加载速度。我项目中的一些第三方资源有时需要请求,因此我会使用CDN
内容分发网络来提高访问速度。前端登录状态管理
isLogin
来判断有没有登录,当时由于没有深入了解vuex
,所以我一开始想着把这个isLogin
通过组件与组件的传值方法,把这个值传给相应的组件,然后在需要登录组件中进行判断,但后来发现这个方法太麻烦了vuex
这个全局状态管理的方法, 通过使用createStore
这个vuex
中的API创建了一个全局的登录状态,再通过actions mutations
实现登录判断和登录状态共享组件数据状态管理
store
上,虽然感觉有点乱,但实现了数据流和组件开发的分离,使得我更能够专注于数据的管理vuex
中可以使用 modules
来进行分模块,相应的页面放入相应的模块状态中,之后再用actions,mutations,state,getters
这四件套, 更好的模块化管理数据,能够知道哪些状态是全局共享的(登录), 哪些状态是模块共享的pinia
来管理,因为我发现它更简单(没有mutations
),模块化更好,让我对组件状态管理的更加得心应手,学习起来也更加的方便。node
写后端的时候,一堆错误,比如路由没配置,数据库报错。使得后面的代码都无法运行,写着写着就感觉写不下去,经常一个错误就需要反复的在脑海中想最后依靠那一丝的灵光一闪才解决app.js
这个后端入口文件的最后,添加一个统一的错误处理的中间件,向前端返回状态码和相应的信息后,直接使用next()
向后继续执行,这样虽然服务器报了错,但仍然可以执行后续的代码。script
,postMessage
,html本身的Websocket
Access-Control-Allow-Origin
来控制跨域请求的url地址,以及其他一些Access-Control-Allow
头来控制跨域请求方法等,然后跨域请求url的白名单我放入了.env这个全局环境变量中。data
这个需要返回的数据(代码如下),这导致我在获取接口里的数据时需要多.data
(引用一层data),当时我没意识到,结果一直获取不到数据。之后输出获取的数据才发现在数据外面包了一层,虽然这个时候解决了服务器那边数据返回的问题,但后面每次获取数据时都需要在往里再获取,非常的麻烦。这个就看个人情况了,但其中,你得展现出你的学习积极性和对前端的热爱,让面试官能够欣赏你
我大致说说我回答的,仅作参考
我从大二开始就对前端很感兴趣,当时正好学校也分了Web前端的方向,于是就跟着学校的课程开始学习基本的html,css,js三剑客,但之后感觉到老师教的很慢,就自己到B站上学习了,之后由于参加过一次蓝桥杯,就看了相关的基于html,css,js比较基础项目,接着我还学习了一些行内大牛写的一些博客文章,比如阮一峰,张鑫旭,廖雪峰等这些老师。之后又学习了vue并且在GitHub上学习相关的设计理念,根据GitHub上项目中不懂的东西又逐渐学习了各种UI组件库和数据请求方式,最后又学习了Nodejs中的Koa,用Vue和Koa仿写了一个全栈型项目,目前正在学习一些typescript的基本用法并尝试着运用到项目中,并在学习Vue的一些底层源码。
大厂的面试终归到底还是和我之前面的公司不一样,它们更加看重的是代码底层的实现和你的算法基础
,终归到底,这次面试只是一次小尝试,想要知道自己的水平到底在哪里,并且能够借此完善自己的能力,努力的提升自己,希望能够给大家带来一些正能量。