新人求关注,求内推,也欢迎浏览我的第一篇面经
?北京七年前端大专找工作竟如此坎坷?近N个月面试复盘(附总结答案),快来学习呀!系列更新中(一)
** 自从上次发了篇面经,也获得了很多掘友们的鼓励,我也没那么低落了,在此向他们表示感谢。**
我的历程
7 月中旬,从某著名程序员在线学习平台领了大礼包毕业后,一直没有马上找工作的想法。
休息了段时间,也做了很多事情,但人毕竟还是要工作的嘛,还年轻,没到退休呢。就国庆节前后开始准备刷题,看基础,学习,投简历,目前到发文的今天2023年12月1日
我也算找了有快两个月了。
算上之前面了两家加上这次面经的四家,一共才面了六家。面试机会不多,招聘软件上已读不回或者不读的更多一点,最多的还是外包,目前行情外包最多给20K
。
外包的 HR,我觉着态度比非外包的要好一些。但是外包也各有各的坑吧,大伙们注意分辨,还有五险一金大概率按最低基数的最低标准交。真要去外包的同胞们,我总结了个需要问清楚的问题,不全面,可以做个参考。
** 薪资最高可以给到多少?试用期是否不打折?
五险一金按什么基数交?百分比多少?
是长期项目还是短期项目?项目结束是裁员还是其他?
甲方带队还是项目组进场?
工作时间是几点到几点?是否经常加班?是否996呢?**
还有一部分不知道跟 HR 谈待遇的时候该问些啥,之前在大佬那看到的再加我总结的一些.
** 具体的工资(也就是合同上签订的工资),不要杂七杂八什么绩效加起来的那种,如果有年终奖,最好写在合同里
五险一金缴纳的比例,还有基数(一[da]部分公司不按你实际工资基数来交五险一金,这个需要问清楚)
加班是否有加班工资或者调休
是否是 996,我个人很不推荐 996 的公司
加薪升职的情况
其他各种福利,比如餐补、房补、交通补、节假日福利、另外的保险等等**
注意:如果加班多的,大小周的一定要考虑好,这种的就得算时薪了。
PS:建了个微信讨论群,如果你也最近在面试,或者有些心态崩了,或者有什么想探讨的可以加微信群,入口在文末。
PS2:最近考取了业余无线电操作证,沉迷业余无线电,有对业余无线电感兴趣的可以加我微信
PS3:我微信也在文末,有想加的可以加我。
PS4:如果二维码过期或者人满,就加我微信,拉你,二维码在文末。
PS5:最后可以在文末加我微信拉你进群,也可以顺便关注下我 N 年前注册的公众号
另:求内推机会,给个孩子个机会吧
求内推机会,给个机会吧,我打招呼刷岗位都没空刷八股文了。
灵X(现场面,11 月 16 日)
现场面,没有录音,记不太清楚都问啥了
一面
1. Local storage 和 Session storage 的区别,以及跨页面之后是否可以共享
localStorage
和 sessionStorage
都是Web Storage API提供的两种客户端存储数据的方式。它们之间有一些重要的区别:
区别:
- 数据生命周期:
localStorage
:数据永久存储,除非用户清除浏览器缓存或通过JavaScript代码删除。sessionStorage
:数据仅在当前会话期间存储。关闭浏览器标签页或窗口后,数据将被清除。
- 作用域:
localStorage
:数据在相同域名下的不同页面和标签页之间共享。sessionStorage
:数据仅在同一标签页或窗口中共享,不同标签页或窗口之间的数据不共享。
- 大小限制:
localStorage
:存储容量通常比sessionStorage
更大(通常为5MB),但具体大小也取决于浏览器。sessionStorage
:存储容量通常较小(通常为5MB),同样具体大小取决于浏览器。
是否可以跨页面共享:
**localStorage**
** 跨页面共享:** 是的,localStorage
的数据在同一域名下的不同页面和标签页之间共享。这使得它成为在应用程序中持久存储数据的一种有效方式。**sessionStorage**
** 跨页面共享:** 不是的,sessionStorage
的数据仅在同一标签页或窗口中共享。如果在一个标签页中设置了sessionStorage
的值,其他标签页无法直接访问这些值。
总体而言,选择使用localStorage
还是sessionStorage
取决于你的需求。如果需要在不同会话期间共享数据,使用localStorage
;如果只需要在当前会话中共享数据,使用sessionStorage
。
2. 封装一个自定义表单模块,有什么难点还有需要注意的点
封装自定义表单模块是一个常见但也有一些难点需要注意的任务。以下是一些建议以及可能涉及的一些难点:
2.1. 难点和注意点:
- 表单元素的多样性:
- 表单可能包含各种不同类型的输入元素(文本框、复选框、下拉框等),需要考虑如何统一管理和处理这些不同类型的元素。
- 表单校验:
- 实现表单的校验逻辑是一个挑战。需要考虑如何定义校验规则、在用户输入时进行实时校验、以及在提交时进行整体校验。
- 状态管理:
- 管理表单的状态,例如表单是否已被修改、是否处于提交中等。需要确保表单的状态能够被正确地同步和更新。
- 表单数据管理:
- 如何统一管理和获取表单数据,以便于提交或其他操作。考虑使用响应式数据或者状态管理库来管理表单数据。
- 动态表单项:
- 如果表单项是动态生成的,例如根据后端接口动态生成表单,需要考虑如何处理这种动态性。
- 自定义样式:
- 提供灵活的样式定制选项,使用户能够根据自己的设计需求调整表单样式。
- 国际化:
- 如果应用需要国际化支持,需要考虑如何处理表单中的文本、提示信息等的国际化。
- 性能优化:
- 对于大型表单,需要考虑性能优化,例如懒加载、懒校验等策略,以确保用户体验。
- 事件处理:
- 如何处理用户与表单交互的各种事件,例如输入、点击、提交等。需要确保事件处理的一致性和可扩展性。
2.2. 建议:
- 组件化设计:
- 将表单模块拆分为独立的组件,每个组件负责管理特定类型的表单元素或处理特定的功能,以提高代码的可维护性和可复用性。
- 清晰的 API 设计:
- 提供清晰、简单的 API 接口,使用户能够方便地使用表单模块。良好的API设计有助于提高模块的易用性。
- 文档和示例:
- 提供详细的文档和示例,帮助用户理解如何正确使用和配置表单模块。
- 测试:
- 编写充分的单元测试和集成测试,确保表单模块的稳定性和可靠性。
- 版本管理:
- 对于开源的表单模块,需要考虑版本管理,及时发布新版本以修复bug、添加新功能或适应新的Vue版本。
- 社区支持:
- 如果是一个开源项目,建议建立一个社区,接收用户反馈、解答问题,以及共同推动表单模块的发展。
在封装自定义表单模块时,不同项目的需求可能会有所不同,因此需要灵活地调整和优化设计。仔细考虑上述难点和建议,有助于构建出更加强大、易用和可维护的表单模块。
3. 如果让你重构一个前端项目,很老了,里边有 jQuery,Vue2,Vue3,以及一些 jsp、.net 这种未前后端分离的项目,你感觉难点在哪?
我回答,难点在于老项目里业务的梳理,业务才是最主要的,也是最难搞的,老业务具体的表现,以及想要的效果,肯定是最难梳理了,再加上年久,文档什么的肯定也遗失了。
技术方面是其次了,因为重构,肯定全推翻重写一遍,或者增量处理,切香肠一点点逐步替换。
回答完老哥似乎不满意,问我技术上,我说我都一点也不了解这个项目,我能出啥具体的方案啊,我只能整体的探讨一下。
老哥又说了,你不了解就问啊,学会自主提问会让面试官有很好的感受,我说,哦~,学习了,学习了,那咱们是个什么项目啊?
老哥说,这就对了,问才对,行,下一个问题吧!
4. 剩余的想不起来了,其实问了好多问题,忘了耶。最后我提问的时候,才知道是想招前端负责人
结果
** 说是下周 23 号给我结果,刚开始招聘,还得招招人比对比对。**
** 我 23 号问,人家老哥说崴脚了,这周招聘暂停了,暂时给不了结果,你要是着急就接其他 offer 或者下周再给消息,我就说您还是养伤要紧吧,早日康复。**
** 今天已经是下周了,我再问问,八成也是凉了。**
** 以后现场面也不能忘了录音。**
聚X外包(现场,11 月 16 号)
一面
1. 其实就记着问了一些 Vue 框架的使用以及 Vue2、3 的区别,还有 VueX 的使用(背 api 的那种),其他想不起来了
结果:没过
妙X科技(线上,11 月 23 号)
一面(线上,主要聊简历里的项目)
1. 自我介绍
2. 离职原因
裁员,公司收支不平衡。因为 21 年度融资后,开始扩张, 22 年中开始就收支不平衡,每个月亏钱。
从 22 年开始就启动了多波次的裁员,我部门上一波是在 22 年的 年底,离我这次间隔了半年多,研发部的规模被裁到其实已经很小了,这次呢,也是趁这个机会,选择了离开。
3. 主要问项目情况
3.1. 项目一
- 简历里第一个项目的历程,问我是不是一开始就是负责人,有没有好几个人一起协同开发的时候,我说我来了已经上线了,我是负责维护迭代项目以及新需求的开发。
- 看你简历老项目升级了 vite,具体的情况。我就回答了为什么有必要升级 vite,以及升级的必要性还有生产打包还是 webpack。
- 拆分了作业批改系统,子项目如何跟主项目进行基础数据交互。
3.2. 项目二
我简历里第二个写的是浏览器插件。
问我是不是你独立完成的,为什么开发浏览器插件,是你们业务线需要,还是你学习之后,产品觉着可以,才采纳了你做这个的意见呢?
是不是你自己主动提的,是不是需要频繁维护这个插件项目。
3.3. Q: Hybrid 开发情况,是怎么嵌套进去的。
A: webview 跳链接。
3.4. Q: Vue3,做的项目不多是吧,2 和 3 的差异。源码了解多吗。
3.5. Q: 上一家公司看你还是做维护啊,是不是闲暇时间多啊,闲暇时候会去学习东西扩展知识面吗?
3.6. Q: 最近看的技术书籍你能说出他的名字吗?举例子,印象深刻的,让你打开自己见识见解的观点啊一些技术栈博客,举一些例子,可以说 vue 相关或不相关。
3.7. Q: 有没有看到一些点,比方说自己工作没有遇到,别人分享出来后感觉非常有益,我举个例子啊,比如说性能优化,SEO 啊平常没用的但是也助力我们成为技术专家的这种。举一些例子这种核心知识点,最近看到的或者了解到的。
3.8. Q: Vue 对 SEO 有用的成型的框架你知道吗,有没有尝试过。
A: Nuxt,无
3.9. Q: 能够提升首屏加载的方案
** A: 在Vue中,为了提升首屏加载性能,可以采用一些优化方案。以下是一些建议:**
- 代码拆分 (Code Splitting):
- 使用Webpack等构建工具的代码拆分功能,将应用分割为多个小块(chunk)。这样,只有在需要时才加载相应的块,减少初始加载时间。
- 懒加载 (Lazy Loading):
- 对于不是首屏必须的组件或路由,可以使用懒加载,将它们按需加载。Vue的路由懒加载可以通过动态导入组件实现。
- 按需导入第三方库:
- 如果使用了一些较大的第三方库,可以考虑按需导入(Tree-shaking)。例如,使用
import { someFunction } from 'some-library';
,而不是直接导入整个库。
- 图片优化:
- 对于图片资源,使用适当的格式和大小,避免不必要的高分辨率图像。可以使用工具如ImageMin来压缩图片。
- 服务端渲染 (SSR):
- 使用Vue的服务端渲染(SSR)可以加速首屏加载。SSR可以在服务器端渲染初始HTML,并在客户端激活Vue,提供更快的首屏加载时间。
- Vue-Router的预加载 (Preload):
- 对于Vue-Router,可以使用
<router-link>
的prefetch
属性来预加载一些异步组件,以提前加载相关资源。
- 使用CDN:
- 将一些常用的第三方库,如Vue、Vue Router等,通过CDN(内容分发网络)加载,减少服务器负担,提高加载速度。
- SSG (静态站点生成):
- 对于不经常变化的内容,可以使用静态站点生成(SSG)的方式,将Vue应用预渲染成静态HTML文件。这可以提高加载速度并降低服务器压力。
- 缓存策略:
- 合理使用浏览器缓存策略,确保不经常变化的资源被缓存,从而减少请求时间。
- 组件优化:
- 在组件中使用
keep-alive
来缓存组件状态,避免重新渲染。优化组件的mounted
生命周期钩子,确保只有必要的操作在首次渲染时执行。
综合使用以上策略,可以有效提升Vue应用的首屏加载性能。具体的优化策略应根据项目的实际情况和需求进行调整。
3.10. Q: 在上上家公司干了些什么,都是一些小程序还有后台产品?
3.11. Q: 团队最多带多少人
A: 4 人
3.12. Q: 会去经常代码审查,代码规范检查,进度督促等?有没有辅助工具帮助做规范、代码审查之类的。
3.13. Q: 项目的时间排期是你自己定,还是按组员说的来
A: 这个要综合看待,每个人的开发能力效率还有技术熟练度都不同,需要结合他们自身情况以及项目情况来做综合考量。
3.14. Q: 是否会参与产品验收,还是只交给产品和测试。
3.15. Q: 对还原设计稿的把控,比如做完第一个版本,你的 UI 预期跟设计能达到多少的契合度。
A: 95 %以上,就说上家公司吧,我出完之后,UI 找我修改是最少的,除非是她看哪儿不顺眼了,想改改,其他都符合设计预期。设计稿完美还原。
3.16. Q: 有写一些总结文档吗,是为了找工作写的吗?
3.17. Q: 7 月份离职,是吗?
A: 谈了谈离职之后做的事情,思考职业规划啊,办之前没有机会办的事儿啊之类的,增驾 D 本,考取业余无线电操作证书
结果
** 可能是嫌我上一家公司里做的项目不行?**
** 这家公司我 10 月底投的简历,快 11 月底了才让我面试。**
** 说是又启动了第二波招人,刚开始招,一面先过项目,等人都过一遍,觉着我合适了再跟我联系。**
** 最后是前两天我问的才给我消息。**
** 说是没有通过,不懂,黑人问号脸,不知道我错在哪里。**
美X外包(线上,11 月 23 号)
一面(主要还是围绕简历以及基础知识来聊的)
1. 自我介绍
2. Q:为什么要开发浏览器插件,他有什么作用?
A:为主站导入流量,方便用户学习,也是一个工具
3. Q:怎么在浏览器插件里拿到用户信息的?
A: 直接跳转账户中心登录,登录后自然会留下 cookie,之后可以拿到登录态,等等等
4. Q:怎样区分插件来的流量以及主站自身流量,是否有做自己的埋点系统
A:使用第三方埋点系统
5. Q:上家公司目前前端多少人
6. Q:有做过 Vue3 项目是吧
7. Q:老项目拆分后,使用不同 Vue 版本的子项目该如何共存,以及与主项目共存
8. Q:箭头函数和普通函数有什么区别
JavaScript中,箭头函数(Arrow Function)和普通函数(Regular Function)有一些重要的区别。以下是它们之间的主要区别:
- 语法形式:
- 箭头函数: 使用箭头(
=>
)来定义函数,通常有简洁的语法形式。箭头函数没有自己的this
,arguments
,super
或new.target
,它们继承这些值来自执行上下文。
javascript
const arrowFunction = (param1, param2) => expression;
- 普通函数: 使用关键字
function
来定义函数,可以是函数声明或函数表达式。
javascript
function regularFunction(param1, param2) {
// 函数体
}
- this 的值:
- 箭头函数: 箭头函数没有自己的
this
,它继承自包含它的最近的非箭头函数父作用域的this
值。 - 普通函数: 函数的
this
值在运行时动态确定,取决于函数如何被调用。在全局作用域中,this
指向全局对象(在浏览器中通常是window
)。在对象方法中,this
指向调用方法的对象。
- arguments 对象:
- 箭头函数: 没有自己的
arguments
对象,但可以使用剩余参数(rest parameters)来达到相似的效果。 - 普通函数: 有自己的
arguments
对象,它是一个类数组对象,包含传递给函数的所有参数。
- 构造函数:
- 箭头函数: 不能用作构造函数,不能通过
new
关键字调用,否则会抛出错误。 - 普通函数: 可以用作构造函数,通过
new
关键字调用时会创建一个新对象,并将其绑定到函数的this
上。
- 绑定(Binding):
- 箭头函数: 不会创建自己的执行上下文,不能通过
call()
、apply()
或bind()
改变其this
。 - 普通函数:
this
可以通过call()
、apply()
或bind()
方法进行显式绑定。
总体而言,箭头函数通常更适用于简单的、无需使用 this
或 arguments
的情况,而普通函数则更灵活,可以适应更多的场景,包括构造函数和需要动态绑定 this
的情况。选择使用哪种类型的函数取决于特定的需求和语境。
9. 求 obj.getKey()
javascript
const obj = {
key: 'test',
getKey: function() {
console.log(this.key)
}
}
obj.getKey() // test
在这个例子中,obj.getKey()
是通过对象 obj
调用的,因此 this
将指向调用它的对象,即 obj
。所以,console.log(this.key)
将打印 'test'
。
所以,obj.getKey()
的打印结果将是 'test'
。
10. 求 fn()
javascript
const obj = {
key: 'test',
getKey: function() {
console.log(this.key)
}
}
const fn = obj.getKey
fn() // undefined
在这个例子中,fn
是从 obj.getKey
中提取出来的函数,并且它被独立地调用。由于 JavaScript 中函数的执行上下文与调用方式相关,此时 fn
被作为全局函数调用,因此 this
将指向全局对象(在浏览器环境中通常是 window
)。
由于 fn
的执行上下文不再与 obj
对象相关,因此在 fn
函数内部的 this.key
将无法访问到 obj
对象的 key
属性。这将导致在调用 fn()
时 this.key
为 undefined
。
所以,console.log(fn())
的打印结果将是 undefined
。
11. 接上一题,如果想让 fn()
结果打印 test
应该怎么做
要确保 console.log(fn())
打印结果为 'test'
,你可以在创建 fn
时使用 bind
方法,显式绑定 obj
作为执行上下文。这样,无论在哪里调用 fn
,它的 this
将始终指向 obj
。
javascript
const obj = {
key: 'test',
getKey: function() {
console.log(this.key);
}
};
const fn = obj.getKey.bind(obj);
console.log(fn()); // 打印结果 'test'
使用 bind(obj)
创建了一个新的函数,该函数与 obj.getKey
具有相同的函数体,但在执行时 this
将始终指向 obj
。因此,调用 fn()
就会正确地打印 'test'
。
12. 接上一题,还有其他办法吗
在这个情况下,console.log(fn())
打印的结果为 undefined
,因为在执行 fn()
时,this
的值指向的是全局对象(在浏览器环境中通常是 window
)。由于在全局对象中没有名为 key
的属性,因此会输出 undefined
。
要确保 console.log(fn())
打印结果为 'test'
,你可以使用 bind
方法或者箭头函数来显式指定函数执行时的 this
值。以下是两种方式的示例:
- 使用
**bind**
方法:
javascript
const obj = {
key: 'test',
getKey: function() {
console.log(this.key);
}
};
const fn = obj.getKey.bind(obj);
console.log(fn()); // 打印结果 'test'
- 使用箭头函数:
javascript
const obj = {
key: 'test',
getKey: function() {
console.log(this.key);
}
};
const fn = () => obj.getKey();
console.log(fn()); // 打印结果 'test'
这两种方式都能确保在调用 fn()
时,this
指向 obj
,从而打印出正确的结果 'test'
。
使用 call
或 apply
方法可以在调用函数时显式地指定函数执行时的 this
值。在这个例子中,你可以使用 call
或 apply
来确保 fn()
执行时 this
指向 obj
。以下是使用 call
和 apply
的示例:
- 使用
**call**
方法:
javascript
const obj = {
key: 'test',
getKey: function() {
console.log(this.key);
}
};
const fn = obj.getKey;
console.log(fn.call(obj)); // 打印结果 'test'
- 使用
**apply**
方法:
javascript
const obj = {
key: 'test',
getKey: function() {
console.log(this.key);
}
};
const fn = obj.getKey;
console.log(fn.apply(obj)); // 打印结果 'test'
这两种方法都允许你在调用函数时显式地设置 this
的值。在这里,fn.call(obj)
和 fn.apply(obj)
都会使 this
指向 obj
,从而使 console.log(this.key)
打印出 'test'
。
13. call``apply``bind
的区别
call
、apply
和 bind
是 JavaScript 中用于处理函数执行上下文的方法。它们有一些区别:
- 传递参数的方式:
**call**
: 以参数列表的形式传递参数。
javascript
func.call(context, arg1, arg2, ...);
**apply**
: 以数组的形式传递参数。
javascript
func.apply(context, [arg1, arg2, ...]);
**bind**
: 创建一个新的函数,并以参数列表的形式传递参数,但不会立即执行原函数。
javascript
const newFunc = func.bind(context, arg1, arg2, ...);
- 立即执行与返回新函数:
**call**
** 和**apply**
:** 立即执行原函数。**bind**
: 返回一个新的函数,不会立即执行原函数,而是返回一个新函数,你可以稍后调用。
- 返回值:
**call**
** 和**apply**
:** 返回原函数的执行结果。
javascript
const result = func.call(context, arg1, arg2, ...);
**bind**
: 返回一个新的函数。你需要调用这个新函数才能获取结果。
javascript
const newFunc = func.bind(context, arg1, arg2, ...);
const result = newFunc();
- 使用场景:
**call**
** 和**apply**
:** 主要用于借用其他对象的方法,或者在一个对象上调用一个函数,同时指定该函数内部的this
值。**bind**
: 主要用于创建一个与原函数拥有相同this
值的新函数,方便稍后调用。
- 性能:
**call**
** 和**apply**
:** 由于立即执行原函数,可能略微更高效,但在大多数情况下性能差异微乎其微。**bind**
: 创建了一个新的函数,性能可能稍差一些,但同样在大多数情况下不会引起明显的性能问题。
示例:
javascript
const obj = { value: 42 };
function getValue(arg) {
console.log(this.value + arg);
}
getValue.call(obj, 2); // 输出: 44
getValue.apply(obj, [2]); // 输出: 44
const boundFunc = getValue.bind(obj, 2);
boundFunc(); // 输出: 44
总体而言,call
、apply
和 bind
提供了一些灵活性,可以根据具体的使用场景选择适当的方法。
14. testFnA()
打印什么
javascript
const a = 'aaaaaa'
function testFn() {
const a = 'bbbbbb'
return function () {
console.log(a)
}
}
const testFnA = testFn()
testFnA() // bbbbbb
在这个例子中,存在两个变量 a
,一个是全局作用域下的 a
,值为 'aaaaaa'
,另一个是在 testFn
函数内部的局部作用域下的 a
,值为 'bbbbbb'
。
在 testFn
函数内部,返回了一个内部函数,并且内部函数引用了外部函数 testFn
中的变量 a
。当执行 testFn()
后,返回的内部函数被赋值给了 testFnA
,然后再调用 testFnA()
。
在调用 testFnA()
时,内部函数访问的变量 a
是通过闭包的方式捕获的,它引用的是外部函数 testFn
中的局部变量 a
,而不是全局作用域下的变量 a
。
因此,console.log(a)
打印的结果是 'bbbbbb'
,而不是全局作用域下的 'aaaaaa'
。这是因为 JavaScript 中函数在执行时会按照词法作用域的规则查找变量,找到最近的定义。
15. 跟第 9
题有什么区别
一个对象的取值,一个闭包,闭包不会被垃圾回收机制清理
16. 刚谈到闭包没有被垃圾回收,那怎么手动回收呢
JavaScript 中的垃圾回收是由 JavaScript 引擎自动进行的,开发者通常无需手动干预。垃圾回收器会负责检测不再使用的对象,并释放其占用的内存。
对于闭包,如果闭包中的函数引用了外部作用域的变量,那么这些变量可能会被保留在内存中,即使外部函数执行完毕。这时,垃圾回收器不会回收这些变量,因为闭包仍然在使用它们。
在一般情况下,我们无法手动回收闭包中的变量。垃圾回收是自动进行的,它会检测不再使用的对象,并在适当的时候将其回收。如果确实有需要释放某些资源,可以考虑解除对闭包的引用,以便让垃圾回收器能够回收相关的资源。
以下是一个简单的例子:
javascript
function createClosure() {
const data = 'some data';
const innerFunction = function() {
console.log(data);
};
return innerFunction;
}
// 创建闭包
const closure = createClosure();
// 执行闭包
closure();
// 现在解除对闭包的引用
closure = null;
// 这时,如果没有其他地方引用闭包,垃圾回收器可能会回收相关资源
在这个例子中,通过将 closure
设置为 null
,解除了对闭包的引用。这样,如果没有其他地方引用这个闭包,垃圾回收器可能会在适当的时候回收相关资源。需要注意的是,这种方式并不是手动回收,而是通过解除引用来让垃圾回收器自动进行回收。
17. 节流与防抖是什么,以及他们的区别
节流(throttling)和防抖(debouncing)是两种用于控制函数执行频率的前端优化技术。
- 节流(Throttling):
- 节流是指在一定时间间隔内,无论事件触发多少次,只执行一次相应的函数。它通过设置一个时间间隔,确保在这个时间间隔内只触发一次函数。这有助于减少函数的执行频率,特别在处理一些高频率触发的事件时,例如滚动事件或鼠标移动事件。
javascript
function throttle(func, delay) {
let timer;
return function() {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, arguments);
timer = null;
}, delay);
}
};
}
- 防抖(Debouncing):
- 防抖是指在一定时间内,如果事件持续触发,则不执行相应的函数,直到一定时间内没有触发事件为止。防抖主要用于处理一些频繁触发但只需执行一次的操作,例如搜索框输入事件或窗口大小调整事件。
javascript
function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
区别:
- 执行时机:
- 节流: 在一定时间间隔内,无论触发多少次事件,都只执行一次函数。
- 防抖: 在一定时间内连续触发事件,只执行一次函数,直到一定时间没有触发事件。
- 实现方式:
- 节流: 使用
setTimeout
控制函数执行的频率,确保在一定时间间隔内只执行一次。 - 防抖: 使用
setTimeout
控制函数的执行,每次触发事件都重新设置定时器,确保在一定时间内不会执行函数。
- 节流: 使用
- 应用场景:
- 节流: 适用于高频率触发的事件,如滚动事件、鼠标移动事件。
- 防抖: 适用于频繁触发但只需执行一次的事件,如搜索框输入事件、窗口大小调整事件。
选择使用节流还是防抖取决于具体的应用场景和需求。
18. 浏览器的宏任务与微任务是什么
在 JavaScript 中,浏览器执行异步任务时,任务分为宏任务(macro task)和微任务(micro task)。这两者的执行顺序和优先级有一定的区别。
- 宏任务(Macro Task):
- 宏任务是由浏览器发起的任务,通常包括整体代码、setTimeout、setInterval、I/O 操作等。宏任务会被添加到宏任务队列中,等待浏览器的执行。
- 宏任务执行完毕后,会清空微任务队列。
示例宏任务:
javascript
setTimeout(() => {
console.log('This is a macro task.');
}, 0);
- 微任务(Micro Task):
- 微任务是在宏任务执行完毕后立即执行的任务。微任务通常包括 Promise 的回调、MutationObserver 等。
- 微任务队列有自己的执行时机,在每个宏任务执行完毕后,会检查微任务队列并执行其中的所有任务。
示例微任务:
javascript
Promise.resolve().then(() => {
console.log('This is a micro task.');
});
执行顺序:
- 执行整体代码(同步任务)。
- 执行所有宏任务,每个宏任务执行完毕后,检查微任务队列并执行其中的所有微任务。
- 重复步骤 2 直到宏任务队列和微任务队列都为空。
执行顺序示例:
javascript
console.log('Start');
setTimeout(() => {
console.log('This is a macro task.');
}, 0);
Promise.resolve().then(() => {
console.log('This is a micro task.');
});
console.log('End');
上述代码输出顺序:
vbnet
Start
End
This is a micro task.
This is a macro task.
在执行过程中,首先输出同步任务 'Start' 和 'End',然后执行宏任务和微任务。微任务会在宏任务执行完毕后立即执行,因此先输出微任务 'This is a micro task.',再输出宏任务 'This is a macro task.'。
理解宏任务和微任务的执行顺序对于处理异步编程、避免回调地狱等都是非常重要的。
19. 如果一个 Promise
进行了两次 then
,那 then
是一个微任务还是两个微任务
如果一个 Promise 进行了两次 then
,它将产生两个微任务。每次调用 then
方法都会创建一个新的 Promise 对象,并返回该 Promise 对象。当 Promise 状态改变时(例如,由 pending 变为 resolved 或 rejected),相应的微任务队列中的任务就会被添加。
示例:
javascript
const promise = new Promise((resolve, reject) => {
resolve('First then');
});
promise.then((result) => {
console.log(result); // 输出: 'First then'
});
promise.then((result) => {
console.log(result); // 输出: 'First then'
});
console.log('End');
在这个例子中,promise
被解决为 'First then'
,并且有两个 then
方法被调用。每个 then
方法都会返回一个新的 Promise 对象,并且它们的回调函数都会被添加到微任务队列中。因此,输出顺序将是 'First then'
,然后两次 'End'
。每个 then
方法都产生了一个微任务。
20. Promise
内部原理
Promise 是 JavaScript 中用于处理异步操作的一种机制,它提供了更方便的方式来处理回调函数的问题。下面是 Promise 的简要内部原理:
- Promise 状态:
- Promise 有三种状态:
pending
(等待中)、fulfilled
(已成功)、rejected
(已失败)。 - 初始状态是
pending
,表示操作还在进行中。
- Promise 解决和拒绝:
- 当操作成功完成时,Promise 会变为
fulfilled
状态,并保存操作结果。 - 当操作失败时,Promise 会变为
rejected
状态,并保存错误信息。
- then 方法:
- Promise 提供
then
方法用于处理异步操作的结果。 then
方法接受两个参数:onFulfilled
(当 Promise 转为fulfilled
状态时调用)和onRejected
(当 Promise 转为rejected
状态时调用)。
- 微任务队列:
- Promise 的回调函数是异步执行的,它们会被放入微任务队列中。
- 在一个宏任务执行完毕后,会检查微任务队列并依次执行其中的任务。
- Promise 链:
then
方法返回一个新的 Promise,可以通过链式调用then
方法。- 这样的链式调用形成了 Promise 链,每个
then
方法都有机会处理上一个 Promise 的结果。
简化版 Promise 内部实现示例:
javascript
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.value = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
const result = onFulfilled(this.value);
resolve(result);
} else if (this.state === 'rejected') {
const result = onRejected(this.value);
resolve(result);
} else if (this.state === 'pending') {
this.onFulfilledCallbacks.push(value => {
const result = onFulfilled(value);
resolve(result);
});
this.onRejectedCallbacks.push(reason => {
const result = onRejected(reason);
resolve(result);
});
}
});
}
}
// 示例用法
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Hello, Promise!');
}, 1000);
});
promise.then(
result => console.log(result),
error => console.error(error)
);
这是一个简化版的 Promise 内部实现,实际的 Promise 还包含了一些额外的特性,例如 catch
方法、finally
方法等。在真正的实现中,还需要处理异步操作、链式调用等更复杂的场景。
21. 接口返回值如何使用 TS
来定义 interface
例:
javascript
{
code: 0,
data: {
list: [{ name: "张三", age: 14 }],
total: 100,
},
message: '返回成功',
}
答:
在 TypeScript 中,你可以使用接口(interface)来定义这个数据结构。根据你提供的 JSON 结构,可以定义一个包含 code
、data
和 message
的接口,其中 data
包含一个 list
数组和一个 total
数字。下面是一个可能的 TypeScript 接口定义:
typescript
interface ResponseData {
code: number;
data: {
list: {
name: string;
age: number;
}[];
total: number;
};
message: string;
}
// 使用示例
const responseData: ResponseData = {
code: 0,
data: {
list: [{ name: "张三", age: 14 }],
total: 100,
},
message: '返回成功',
};
console.log(responseData.code); // 输出: 0
console.log(responseData.data.list[0].name); // 输出: 张三
console.log(responseData.data.total); // 输出: 100
console.log(responseData.message); // 输出: 返回成功
在上述定义中:
**ResponseData**
** 是整个响应数据的接口。****data**
** 中包含了一个**list**
数组,其中每个元素都是一个包含**name**
和**age**
字段的对象。****total**
** 是一个数字。****code**
** 是一个数字。****message**
** 是一个字符串。**
你可以根据实际需要调整接口定义。上述示例提供了一个基本的结构,你可以根据你的实际数据结构进行扩展或修改。
22. 接上一题,如果 data
里返回数据格式不确定呢?
如果 data
下的数据格式不确定,你可以使用泛型(Generics)来定义一个更灵活的接口。在 TypeScript 中,泛型可以用于创建可重用的、适用于不同数据结构的接口。下面是一个示例:
typescript
interface ResponseData<T> {
code: number;
data: T;
message: string;
}
// 使用示例
const responseDataWithList: ResponseData<{ list: { name: string; age: number }[]; total: number }> = {
code: 0,
data: {
list: [{ name: "张三", age: 14 }],
total: 100,
},
message: '返回成功',
};
const responseDataWithString: ResponseData<string> = {
code: 0,
data: 'Some dynamic data',
message: '返回成功',
};
console.log(responseDataWithList.code); // 输出: 0
console.log(responseDataWithList.data.list[0].name); // 输出: 张三
console.log(responseDataWithList.data.total); // 输出: 100
console.log(responseDataWithList.message); // 输出: 返回成功
console.log(responseDataWithString.code); // 输出: 0
console.log(responseDataWithString.data); // 输出: Some dynamic data
console.log(responseDataWithString.message); // 输出: 返回成功
在这个示例中,ResponseData
接口使用了一个泛型 T
,它表示 data
的类型。在使用时,你可以通过给泛型传递不同的类型来适应不同的数据结构。
这种方式可以更灵活地适应不同的数据结构,但同时也需要在使用时确保数据的一致性。
23. TS
的泛型是什么
在 TypeScript 中,泛型(Generics)是一种用于创建可复用代码的工具,允许在定义函数、类、接口等时使用占位符,使其可以接受不同类型的参数。泛型在增强代码的灵活性和可重用性方面发挥了重要作用。
23.1. 泛型函数:
typescript
function identity<T>(arg: T): T {
return arg;
}
// 使用方式
let result = identity<string>('Hello, Generics!');
console.log(result); // 输出: 'Hello, Generics!'
在这个例子中,<T>
表示泛型参数,允许 identity
函数接受不同类型的参数,同时保持返回类型与输入类型一致。
23.2. 泛型接口:
typescript
interface Pair<T, U> {
first: T;
second: U;
}
// 使用方式
let pair: Pair<number, string> = { first: 1, second: 'two' };
这里,Pair
是一个泛型接口,可以用不同的类型实例化。
23.3. 泛型类:
typescript
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
// 使用方式
let numberBox = new Box<number>(42);
let stringBox = new Box<string>('Generics');
Box
是一个泛型类,它可以包装不同类型的值。
23.4. 泛型约束:
typescript
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// 使用方式
loggingIdentity('Hello'); // 输出: 5
这个例子中,T
被约束为拥有 length
属性的类型。
泛型在 TypeScript 中提供了一种强大的方式,使代码更加灵活,并能够在类型安全的同时实现可重用的抽象。
24. 如果返回的数据格式是 string
或者 number
,用 TS
该如何定义
24.1. 联合类型
如果一个函数返回的数据可能是 string
或 number
类型,可以使用联合类型(Union Types)来定义返回类型。在 TypeScript 中,使用 string | number
表示一个值可以是字符串或数字。
以下是一个示例:
typescript
function getStringOrNumber(): string | number {
// 一些逻辑,可能返回字符串或数字
const randomNumber = Math.random();
return randomNumber > 0.5 ? 'Hello' : 42;
}
// 使用方式
const result: string | number = getStringOrNumber();
// 根据实际情况判断返回类型
if (typeof result === 'string') {
console.log('It is a string:', result.toUpperCase());
} else {
console.log('It is a number:', result.toFixed(2));
}
在这个例子中,getStringOrNumber
函数的返回类型被定义为 string | number
,表示它可以返回字符串或数字。在使用函数返回值时,可以使用 typeof
运算符进行类型判断,确保正确处理不同类型的返回值。
除了使用联合类型之外,还可以使用类型断言(Type Assertion)或函数重载(Function Overloading)等方式来处理返回 string
或 number
类型的情况。
24.2. 类型断言:
typescript
function getStringOrNumber(): string | number {
const randomNumber = Math.random();
return randomNumber > 0.5 ? 'Hello' : 42 as string | number;
}
// 使用方式
const result: string | number = getStringOrNumber();
在这个例子中,通过 as string | number
进行类型断言,告诉 TypeScript 返回值可以是 string
或 number
。
24.3. 函数重载:
typescript
function getStringOrNumber(): string;
function getStringOrNumber(): number;
function getStringOrNumber(): string | number {
const randomNumber = Math.random();
return randomNumber > 0.5 ? 'Hello' : 42;
}
// 使用方式
const result: string | number = getStringOrNumber();
通过函数重载,可以为函数提供多个不同的签名,让 TypeScript 根据实际调用的情况来选择正确的返回类型。
每种方法都有其适用的场景,选择其中一种取决于代码的具体需求和设计。一般而言,使用联合类型是最简单和直观的方式。
25. Vue2、3 的区别
Vue 2.x 和 Vue 3.x 之间有一些显著的区别,其中 Vue 3.x 引入了一些新的特性和优化。以下是一些主要的区别:
- 性能优化:
- Vue 3.x 在性能方面进行了大幅度的优化。它引入了新的响应式系统(
Proxy
),用于替代 Vue 2.x 中的Object.defineProperty
,提高了响应式数据的效率。
- Composition API:
- Vue 3.x 引入了 Composition API,这是一种新的组织组件代码的方式。相比于 Vue 2.x 的选项式 API,Composition API 更灵活,使得组件的逻辑更容易复用和组合。
- Teleport:
- Vue 3.x 引入了 Teleport,用于在组件的模板中将内容传送(teleport)到另一个地方,使得更容易处理全局弹窗等场景。
- Fragments:
- Vue 3.x 支持 Fragments,允许组件返回多个根节点而无需包裹在额外的标签中。
- Custom Directives:
- Vue 3.x 提供了更灵活的自定义指令 API,使得创建和管理自定义指令变得更加简单。
- Tree-Shakable:
- Vue 3.x 被设计为更容易进行 Tree-Shaking,以减小应用的体积。
- 全局 API 重构:
- Vue 3.x 对全局 API 进行了重构,包括全局 API 的导入方式和一些方法的修改。例如,
Vue.component
变为app.component
。
- 事件修饰符:
- Vue 3.x 中的事件修饰符的语法进行了一些变化,更符合自然语言。
- Suspense 和异步组件:
- Vue 3.x 引入了 Suspense 和异步组件的新语法,使得处理异步数据更加优雅。
- 更好的 TypeScript 支持:
- Vue 3.x 对 TypeScript 的支持更加完善,包括更好的类型推导和更准确的类型提示。
需要注意的是,虽然 Vue 3.x 引入了许多新特性和改进,但也带来了一些不兼容的变化。在迁移时,开发者需要仔细阅读官方文档和迁移指南,以确保应用能够平滑过渡。
26. Vue2、3 响应式的区别
Vue 2.x 和 Vue 3.x 在响应式系统上有一些显著的区别。Vue 3.x 引入了新的响应式系统,使用 Proxy
替代了 Vue 2.x 中的 Object.defineProperty
,带来了性能和功能上的改进。
26.1. Vue 2.x 响应式系统:
- 基于
**Object.defineProperty**
:
- Vue 2.x 使用
Object.defineProperty
来实现响应式。这个方法会劫持对象的属性访问,当属性发生变化时,触发相应的更新。
- 数组的变异方法:
- 对于数组,Vue 2.x 通过重写数组的变异方法(例如
push
、pop
、splice
等)来实现响应式。这意味着只有这些方法能触发视图更新,直接通过索引修改数组的元素是不会触发响应式更新的。
- 性能问题:
- Vue 2.x 的响应式系统在大规模数据和频繁更新的情况下可能引起性能问题,因为
Object.defineProperty
的实现相对较为复杂。
26.2. Vue 3.x 响应式系统:
- 基于
**Proxy**
:
- Vue 3.x 使用了新的
Proxy
API 来实现响应式。Proxy
提供了更直观和强大的拦截器,使得对对象的访问和修改更加灵活。
- Reflect API:
- Vue 3.x 使用
Reflect
API 来进行一些操作,例如Reflect.set
、Reflect.get
等。
- 不同类型的 reactive:
- Vue 3.x 提供了
reactive
函数和ref
函数,分别用于创建对象和创建基本类型的响应式数据。这种方式更加直观和一致。
- 数组的处理:
- 在 Vue 3.x 中,通过
Proxy
可以对数组的所有操作进行拦截,不再需要特殊处理数组的变异方法。直接通过索引修改数组元素也能触发响应式更新。
- 性能优化:
- Vue 3.x 的响应式系统在性能上有较大优势,特别是在大规模数据和频繁更新的场景下。
- 逻辑拆分:
- Vue 3.x 的响应式系统逻辑更加清晰,拆分为模块,使得维护和扩展更容易。
总体而言,Vue 3.x 的响应式系统通过 Proxy
带来了性能上的提升,并使得 API 更加灵活和直观。在使用上,开发者可能会更容易理解和处理响应式数据。
27. 为什么 Vue2
嵌套的深层数据没法响应
在 Vue 2.x 中,嵌套的深层数据如果是在初始时已存在(即在响应式对象被创建之前就存在的属性),Vue 2.x 可能无法做到深层嵌套的响应式。这是因为 Vue 2.x 的响应式系统是基于 Object.defineProperty
实现的,而该方法只会在对象属性被访问时添加 getter 和 setter,因此对于初始时就存在的深层嵌套数据,Vue 2.x 可能无法捕捉到变化。
以下是一个示例:
html
<template>
<div>
<p>{{ deepData.nestedValue }}</p>
<button @click="updateData">Update Data</button>
</div>
</template>
<script>
export default {
data() {
return {
deepData: {
nestedValue: 'Initial Value'
}
};
},
methods: {
updateData() {
// 这里的数据更新可能无法触发深层嵌套数据的响应
this.deepData.nestedValue = 'Updated Value';
}
}
};
</script>
在上述示例中,如果 deepData
对象在组件创建时已经存在,那么在 updateData
方法中对 nestedValue
的修改可能无法触发视图的更新,因为初始时该属性已存在,不会被 Object.defineProperty
拦截。
为了解决这个问题,一种常见的做法是使用 Vue.set
或 this.$set
方法,手动触发深层嵌套数据的响应式。例如:
javascript
this.$set(this.deepData, 'nestedValue', 'Updated Value');
这样就能确保深层嵌套的数据也能够被 Vue 2.x 的响应式系统捕捉到变化,从而触发视图更新。在 Vue 3.x 中,由于采用了 Proxy
,这个问题已经得到了更好的解决。
28. 为什么 vue3
可以实现对象数据的深度响应
Vue 3.x 在实现深度响应式时,采用了新的响应式系统,其中使用了 Proxy
替代了 Vue 2.x 中的 Object.defineProperty
。这一变化带来了一些重要的优势,使得 Vue 3.x 能够更有效地实现对象数据的深度响应。
以下是一些原因:
- Proxy 拦截器:
Proxy
提供了一组强大的拦截器,允许对对象的访问、修改、删除等操作进行拦截。这意味着 Vue 3.x 可以更灵活地捕获对象上的操作,包括深层嵌套数据的变化。
- 递归代理:
- Vue 3.x 的响应式系统能够递归地对对象的每个属性进行代理。当访问或修改对象的嵌套属性时,系统能够动态地为嵌套属性创建新的
Proxy
实例,从而实现对深层嵌套数据的深度响应。
- 更高性能:
- 使用
Proxy
相对于Object.defineProperty
在性能上有很大的提升。Proxy
是 JavaScript 引擎提供的原生机制,它的实现更为高效,尤其在处理大规模数据和频繁更新时,性能优势更加明显。
- 更直观的 API:
Proxy
提供了一个更直观和强大的 API,比Object.defineProperty
更易于理解和使用。这使得 Vue 3.x 的响应式系统更加灵活,可以更容易地处理复杂的数据结构。
- Track 和 Trigger 阶段:
- Vue 3.x 引入了 Track 和 Trigger 阶段,分别用于跟踪依赖关系和触发更新。这个机制更加精确地追踪了数据的访问和修改,使得 Vue 3.x 能够更准确地知道何时以及如何触发视图的更新。
总的来说,Vue 3.x 利用了 Proxy
的灵活性和性能优势,通过递归代理等机制实现了更为强大和高效的深度响应式系统。这也是 Vue 3.x 在响应式方面相对于 Vue 2.x 的一个显著改进。
29. 请看例子,父组件传参name
变化的话,子组件 newName
也会跟着变吗
例:
javascript
setup(props) {
const { name } = props
const newName = computed(() => `姓名: ${name}`)
return {
newName
}
}
// 父级组件如果传参 name 变更的话,子组件的 newName 会跟着变吗
29.1. Vue3 中
在Vue 3中,setup
函数中使用的 props
是响应式的,因此当父级组件传递的 name
发生变更时,newName
会跟着变化。
在你提供的代码中,newName
使用了 computed
函数,它会依赖于 name
,这意味着当 name
发生变化时,newName
会自动更新。所以,如果父级组件传递的 name
发生变更,子组件中的 newName
也会相应地更新。
示例:
html
<!-- 父级组件 -->
<template>
<ChildComponent :name="parentName" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
data() {
return {
parentName: 'John',
};
},
methods: {
changeName() {
this.parentName = 'Doe'; // 改变父级组件的 name
},
},
components: {
ChildComponent,
},
};
</script>
html
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<p>{{ newName }}</p>
</div>
</template>
<script>
import { computed, ref, watch } from 'vue';
export default {
props: {
name: String,
},
setup(props) {
const { name } = props;
const newName = computed(() => `姓名: ${name}`);
return {
newName,
};
},
};
</script>
在这个示例中,当调用 changeName
方法时,父级组件的 parentName
发生变化,这会导致子组件中的 newName
重新计算,最终反映在子组件的视图中。
29.2. Vue2 中
在 Vue 2.x 中,computed
仅仅是计算属性,并不具备响应式更新传入的 props
的能力。当 props
发生变化时,computed
不会自动更新。
如果需要在 Vue 2.x 中实现类似的效果,你可以使用 watch
来手动监听 props
的变化,并在回调中更新相应的计算属性。以下是一个简单的示例:
javascript
export default {
props: {
name: String,
},
data() {
return {
// 使用 data 中的属性作为计算属性的缓存
cachedName: '',
};
},
computed: {
newName() {
return `姓名: ${this.cachedName}`;
},
},
watch: {
name(newName) {
// 监听 props 的变化,更新计算属性的缓存
this.cachedName = newName;
},
},
};
在这个示例中,我们使用了 watch
来监听 props
中 name
的变化,并在 watch
的回调中手动更新了计算属性 newName
使用的 cachedName
。这样就实现了在父级组件传入的 name
变化时,计算属性 newName
也能跟随变化。
30. 用过在 Vue2 中写 Vue3 的语法吗
在 Vue 2 中使用 Vue 3 的 Composition API 需要借助 @vue/composition-api
插件。这个插件提供了 Vue 3 的 Composition API 在 Vue 2 中的实现。
以下是一个简单的示例:
- 安装
@vue/composition-api
插件:
bash
npm install @vue/composition-api
- 在你的 Vue 2 项目的入口文件(通常是
main.js
)中使用 Composition API:
javascript
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';
import App from './App.vue';
// 使用插件
Vue.use(VueCompositionApi);
new Vue({
render: (h) => h(App),
}).$mount('#app');
- 在组件中使用 Composition API:
javascript
<template>
<div>
<p>{{ count.value }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref } from '@vue/composition-api';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
return {
count,
increment,
};
},
};
</script>
在这个示例中,我们引入了 @vue/composition-api
插件,并在组件的 setup
函数中使用了 Composition API 提供的 ref
函数。这使得我们能够在 Vue 2 中使用类似 Vue 3 Composition API 的语法。
请注意,虽然这样做可以让你在 Vue 2 中使用 Composition API,但并不意味着所有 Vue 3 Composition API 的特性都能在 Vue 2 中完全还原。一些高级特性可能需要更多的调整或不支持。在实际使用中,建议谨慎使用 Composition API 插件,并查阅相应文档以了解可能的限制和适用场景。
31. Vue2 自定义指令是什么
在 Vue 2 中,自定义指令是一种强大的扩展 Vue 的方式,它允许你注册全局或局部的指令,并在 DOM 元素上绑定、更新、解绑时执行相应的逻辑。以下是在 Vue 2 中创建自定义指令的简单示例:
31.1. 全局自定义指令
javascript
// main.js 或任何入口文件
import Vue from 'vue';
// 注册全局自定义指令 v-color
Vue.directive('color', {
// bind 钩子函数在指令绑定到元素时触发
bind(el, binding) {
// 设置元素的颜色为指令的值
el.style.color = binding.value;
},
});
在上述代码中,我们注册了一个全局自定义指令 v-color
,它在绑定时设置元素的文本颜色为指令的值。
31.2. 局部自定义指令
javascript
// 在组件中注册局部自定义指令
export default {
directives: {
// 注册局部自定义指令 v-font-size
'font-size': {
// bind 钩子函数在指令绑定到元素时触发
bind(el, binding) {
// 设置元素的字体大小为指令的值
el.style.fontSize = binding.value + 'px';
},
},
},
};
在上述代码中,我们在组件中注册了一个局部自定义指令 v-font-size
,它在绑定时设置元素的字体大小为指令的值。
31.3. 在模板中使用自定义指令
html
<template>
<div>
<!-- 使用全局自定义指令 v-color -->
<p v-color="'red'">This text is red.</p>
<!-- 使用局部自定义指令 v-font-size -->
<p v-font-size="20">This text has font size 20px.</p>
</div>
</template>
在模板中,我们可以使用自定义指令来修改元素的样式或行为。
以上是一个简单的例子,自定义指令还有更多的钩子函数(如 update
、componentUpdated
、unbind
等)和参数,可以根据需求进行定制。详细的文档可以参考 Vue 2 的自定义指令文档。
32. Monorepo 应用场景有哪些
Monorepo(单仓库)是指将多个项目(通常是相关联的项目)放置在同一个版本控制仓库中的做法。这种架构有许多潜在的应用场景,以下是一些常见的 Monorepo 应用场景:
- 代码共享:
- Monorepo 允许在不同的项目之间共享代码。这样,你可以创建共享的库、组件、工具函数等,确保它们在不同项目中保持同步,减少重复工作。
- 版本一致性:
- 所有项目共享相同的版本控制仓库,确保它们使用相同的依赖版本。这有助于维持整体系统的版本一致性,减少因为不同项目使用不同版本而导致的问题。
- 跨项目重构:
- 当需要进行大规模的重构或架构调整时,Monorepo 可以更容易地进行代码共享和迁移,因为所有相关代码都在同一个仓库中,可以更方便地协同变更。
- 单一构建和发布:
- Monorepo 允许进行单一构建和发布。你可以使用一个统一的构建系统和发布流程来管理所有的项目,简化了部署和维护流程。
- 统一的开发环境:
- 统一的开发环境可以简化配置,提高开发效率。开发者可以更轻松地切换不同项目,共享配置文件、脚本等。
- 依赖管理:
- Monorepo 简化了依赖管理,你可以通过工具(如 Yarn Workspaces 或 Lerna)来管理共享依赖,确保不同项目使用相同的依赖版本。
- 综合项目管理:
- 在一个仓库中综合管理多个项目,有助于更好地理解和维护系统的整体结构。这对于大型组织或复杂的项目来说尤为有益。
- 测试和持续集成:
- 在 Monorepo 中,可以更容易地设置统一的测试策略和持续集成(CI)管道,确保所有项目都通过相同的测试和构建流程。
需要注意的是,虽然 Monorepo 提供了一些明显的优势,但它也可能引入一些管理上的挑战,如合理的项目结构设计、团队协作和权限控制等。在决定是否使用 Monorepo 时,需要权衡其优势和挑战,并根据具体情况做出决策。
33. yarn 和 pnpm 有什么区别
Yarn、pnpm、以及npm都是 JavaScript 包管理工具,用于管理项目依赖。它们有一些区别,下面是它们之间的一些主要区别:
33.1. 安装依赖的方式:
- Yarn: Yarn 使用离线缓存(offline cache)来提高安装速度。它会下载所有依赖包到本地缓存,然后从缓存中拷贝到项目中。
- pnpm: pnpm 采用符号链接(symlink)的方式来安装依赖。它会在全局缓存中维护一个单一版本的每个包,并在项目中创建符号链接,避免了重复的文件拷贝。
33.2. 存储方式:
- Yarn: Yarn 使用一个全局存储目录,将所有版本的每个包都下载到这个目录中。
- pnpm: pnpm 使用一个分散的存储模型,每个项目都有自己的
node_modules
文件夹,但不同项目之间可以共享相同的包版本,以及共享硬链接,减少磁盘占用。
33.3. 硬链接:
- Yarn: Yarn 不使用硬链接,而是通过文件拷贝的方式安装依赖。
- pnpm: pnpm 使用硬链接,这意味着相同的依赖在磁盘上只存储一份,可以在不同项目之间共享。
33.4. 安装速度:
- Yarn: Yarn 的安装速度通常较快,特别是当使用离线缓存时。
- pnpm: pnpm 通过符号链接和硬链接的方式,也能提供较快的安装速度,尤其在多个项目之间共享依赖时。
33.5. 磁盘空间:
- Yarn: Yarn 使用的是全局存储,可能导致磁盘空间的浪费,因为每个项目都会有一份完整的依赖包。
- pnpm: pnpm 通过分散存储和硬链接的方式,可以减少磁盘空间的占用。
33.6. 并发安装:
- Yarn: Yarn 具有并发安装的能力,可以同时安装多个依赖。
- pnpm: pnpm 同样支持并发安装,这有助于提高安装速度。
总的来说,Yarn、pnpm 和 npm 都有各自的优势和适用场景。选择使用哪一个工具取决于项目的需求、团队的喜好,以及个人的使用习惯。
34. 在 package.json
中未定义的包,在 yarn 和 pnpm 中能引用吗
yarn 可以,pnpm 不可以
npm/yarn 安装依赖时,存在依赖提升,某个项目使用的依赖,并没有在其 package.json 中声明,也可以直接使用,这种现象称之为 "幽灵依赖";随着项目迭代,这个依赖不再被其他项目使用,不再被安装,使用幽灵依赖的项目,会因为无法找到依赖而报错。
35. 为什么 pnpm 不可以引用未定义的包
在 pnpm 中,一些设计原则导致它在安装时不会引用未定义的包(undeclared dependencies)。这是 pnpm 与其他包管理工具(如 npm 和 Yarn)的一个区别。
35.1. pnpm 的设计原则:
- 显式声明: pnpm 遵循显式声明的原则,即只有在
package.json
中显式声明的依赖才会被安装。未在package.json
中声明的包不会被引用。 - 安装原子性: pnpm 具有原子性的安装特性,即每个包都有独立的
node_modules
目录。这确保了每个包只有它所声明的依赖。 - 符号链接: pnpm 使用符号链接(symbolic links)来创建包之间的链接。这减少了磁盘占用和提高了安装速度。但同时,pnpn 的符号链接机制也要求每个包的依赖关系必须是明确定义的。
35.2. 为什么不引用未定义的包:
- 避免隐式依赖: 显式声明依赖关系可以避免隐式依赖。这样可以确保每个包的依赖是明确的,而不是依赖于其他包的隐式存在。
- 确保安全: 通过明确声明依赖关系,可以确保安装的包是可预测的。避免了引入未知或未定义的包,减少了项目的安全风险。
- 更容易维护: 显式声明依赖关系使得项目更容易维护。开发者可以清晰地知道每个包的依赖情况,降低了项目的复杂性。
虽然这些设计原则在某些情况下可能会引起一些不便,但它们有助于确保项目的可维护性、安全性和稳定性。如果你有确实需要引用未定义的包的情况,可能需要重新考虑项目的架构和依赖管理的策略。
36. 如果 pnpm 的 package.json 注册了 A 包,A 包内又引用了 B 包,那我能在项目中直接使用B包吗?如果不能,是因为什么
在 pnpm 中,"幽灵依赖"是指 node_modules
中存在但未在 package.json
中声明的依赖。pnpm 通过其设计原则,特别是显式声明依赖关系的原则,决定不允许使用未在 package.json
中声明的依赖。
以下是 pnpm 不允许使用幽灵依赖的原因:
- 显式声明的原则: pnpm 遵循显式声明的原则,只有在
package.json
中显式声明的依赖才会被安装。这种设计有助于确保依赖关系是明确的,降低了项目的复杂性。 - 安全性: 通过显式声明依赖关系,可以确保项目使用的依赖是可预测的和已审核的。避免了因为引入未知或未定义的依赖而导致的潜在安全风险。
- 维护性: 显式声明依赖关系使得项目更容易维护。开发者可以清晰地知道每个包的依赖情况,降低了项目的复杂性。
虽然 pnpm 在这方面可能会导致一些不便,但这是为了提高项目的可维护性、安全性和稳定性而作出的权衡。如果你有确实需要使用幽灵依赖的情况,可能需要重新考虑项目的架构和依赖管理的策略。
二面(线上,牛客网)
1. 自我介绍
2. 举例子,在前端负责人角色下,具体项目里怎样和同事协作
3. 举例,做了什么项目,框架搭建过程中是怎么样去思考的,怎样通过框架实现业务目标
4. 技术框架怎么选型的,为什么要做这个选型
5. 菲波那切数列,第 N 项的值
原题是力扣LCR 126.斐波那契数
下面是计算第 N 项斐波那契数列的值的 JavaScript 示例代码:
javascript
function fibonacci(n) {
if (n <= 1) {
return n;
}
let a = 0;
let b = 1;
for (let i = 2; i <= n; i++) {
const temp = a + b;
a = b;
b = temp;
}
return b;
}
// 示例
const n = 7; // 你想要计算的项数
const result = fibonacci(n);
console.log(`第 ${n} 项的斐波那契数列的值是: ${result}`);
这段代码中,fibonacci
函数接受一个整数参数 n
,并返回斐波那契数列的第 N 项的值。在循环中,我们使用两个变量 a
和 b
来迭代计算下一项的值,直到达到目标项数。这样可以有效地计算斐波那契数列,避免了递归的重复计算。
对于上述的迭代方法,时间复杂度为 O(n),其中 n 是目标项数。这是因为我们使用循环迭代计算直到达到目标项数,循环的次数与目标项数线性相关。
空间复杂度为 O(1),这是因为我们只使用了常量级的额外空间,即变量 a
、b
、temp
等,与目标项数无关。这种空间复杂度为常量的情况称为空间复杂度是 O(1)。
总体而言,迭代方法是一种效率较高且占用常量级空间的解决方案。